Skip to content

cmd/compile: clarify prose/examples regarding struct identity when embedded types match equally named non-embedded fields #69472

@leabit

Description

@leabit

Go version

go version go1.23.0 linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/leabit/.cache/go-build'
GOENV='/home/leabit/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/leabit/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/leabit/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/leabit/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/leabit/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.23.0'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/leabit/.config/go/telemetry'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/leabit/goprojects/exeexe/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1849554432=/tmp/go-build -gno-record-gcc-switches'

What did you do?

Golang specification for type identity states the following for the struct types:

Two struct types are identical if they have the same sequence of fields, and if corresponding fields have the same names, and identical types, and identical tags. Non-exported field names from different packages are always different.

Thus, it has 4 requirements:

  1. same sequence of fields
  2. same corresponding names
  3. identical types of corresponding fields
  4. identical tags of corresponding fields

However, there are cases related to embedding aliases of unnamed types where, despite meeting all four previous requirements, the compiler still fails to compile.

I will explain the problem through few examples. The following notes holds for all examples:

  • three packages are used: pac1, pac2 and main
  • declared alias types in pac and pac2 are unnamed types struct{} (thus, they are identical)
  • struct tags aren't used, so they are ignored

(1) This example successfully compiles (as expected). M1 and M2 have the same sequence of fields. Names, even thought not explicitly defined, are indirectly given by embedding and thus they are same (MyType1). Types of the fields (field MyType1) are also same (struct{}).

pac1:

package pac1

type MyType1 = struct{}

pac2:

package pac2

type MyType1 = struct{}

main:

package main

import (
	"exeexe/pac1"
	"exeexe/pac2"
)

type M1 struct {
	pac1.MyType1
}

type M2 struct {
	pac2.MyType1
}

func main() {
	_ = M2(M1{})
}

(2) In this example MyType1 = struct{} is replaced with MyType2 = struct{} in pac2. Also, pac2.MyType2 is now embedded in M2 (instead of pac2.MyType1) in main package. As expected, it doesn't compile. Even thought the types of the fields are the same, the names are not (MyType1 != MyType2), thus requirement 2 doesn't hold.

pac1:

package pac1

type MyType1 = struct{}

pac2:

package pac2

type MyType2 = struct{}

main:

package main

import (
	"exeexe/pac1"
	"exeexe/pac2"
)

type M1 struct {
	pac1.MyType1
}

type M2 struct {
	pac2.MyType2
}

func main() {
	_ = M2(M1{})
}

(3) This example shows the actual problem. The only difference compared to the previous example is that embedded field (pac2.MyType2) is now replaced by non-embedded (ordinary) field with the name MyType1 (type is the same). Since specification for type identity doesn't make a distinct between embedded and ordinary field in the struct this should be valid and compile (all requirements are met).

pac1:

package pac1

type MyType1 = struct{}

pac2:

package pac2

type MyType2 = struct{}

main:

package main

import (
	"exeexe/pac1"
	"exeexe/pac2"
)

type M1 struct {
	pac1.MyType1
}

type M2 struct {
	MyType1 pac2.MyType2
}

func main() {
	_ = M2(M1{})
}

(4) This example just shows that even when MyType1 pac2.MyType2 is replaced with MyType1 pac1.MyType1 in M2 (so just the name is added), it still doesn't compile.

pac1:

package pac1

type MyType1 = struct{}

pac2:

package pac2

type MyType2 = struct{}

main:

package main

import (
	"exeexe/pac1"
)

type M1 struct {
	pac1.MyType1
}

type M2 struct {
	MyType1 pac1.MyType1
}

func main() {
	_ = M2(M1{})
}

What did you see happen?

Inconsistency between specification and implementation

What did you expect to see?

Since I am following specification, I expect examples (3) and (4) to compile, or to modify the specification to address the difference.

Metadata

Metadata

Assignees

Labels

DocumentationIssues describing a change to documentation.FrozenDueToAgecompiler/runtimeIssues related to the Go compiler and/or runtime.

Type

No type

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions