Skip to content

gotypesalias=1 does not seem to get used correctly for deps on Go 1.23

Recently, I have help to troubleshooting the Go type alias issue inside packages.Load, which relates x/tools, go list and cmd/compile. My original CL is correct, but I got confused when I found the exportdata contains all aliases regardless gotypealias setting. Then, tim has shepherded my CL by another CL to fix it.

Feel a bit frustrated when I saw the email when I got up. But it helps me a lot to learn how go toolchain and cmd/compile works. Think twice before you act!

Issue Details

When using pacakges.Load to load packages, the GODEBUG=gotypesalias=1 won't take effects for dep package pkg2. The type from pkg2 should be alias instead of the original type.

    cfg := &packages.Config{Mode: mode}
    pkgs, err := packages.Load(cfg, "./pkg1")
    if err != nil {
        panic(err)
    }
    if packages.PrintErrors(pkgs) > 0 {
        os.Exit(1)
    }

    pkg := pkgs[0]
    scope := pkg.Types.Scope()
    fmt.Println(scope.Lookup("Var1"))
    fmt.Println(scope.Lookup("Var2"))
-- go.mod --
module example.com/ssa-example

go 1.23

require golang.org/x/tools v0.27.0

require (
    golang.org/x/mod v0.22.0 // indirect
    golang.org/x/sync v0.9.0 // indirect
)

-- pkg1/pkg1.go --
package pkg1

import "example.com/ssa-example/pkg2"

type Alias1 = int32

var Var1 Alias1

var Var2 pkg2.Alias2

-- pkg2/pkg2.go --
package pkg2

type Alias2 = int64
var test/pkg1.Var1 example.com/ssa-example/pkg1.Alias1
var test/pkg1.Var2 int64

Packages.Load relies on 'go list'

The packages.Load relies on 'go list' to get pacakge information. It's reasonable because resuing code from go toolchain. When loading packages, it will run the following 'go list' command(depends on the modes).

go list -e -json=Name,ImportPath,Error,Dir,GoFiles,IgnoredGoFiles,IgnoredOtherFiles,CFiles,CgoFiles,CXXFiles,MFiles,HFiles,FFiles,SFiles,SwigFiles,SwigCXXFiles,SysoFiles,CompiledGoFiles,DepOnly,Imports,ImportMap,Export,Module -compiled=true -test=false -export=true -deps=true -find=false -pgo=off -- ./pkg1
Remember to clean GOCACHE to verify your change otherwise go list tries to reuse the exported data.
# don't use 'rm $(go env GOCACHE)/*'  
# as it might delete your /*
go env GOCACHE
rm -r ~/Library/Caches/go-build/*

The -json field information could be found by go help list. The Export filed is the file path which contains exportdata of the package.

go list output example
{
    "Dir": "/Users/yuchen.xie/workspace/CD/ssa-example/pkg2",
    "ImportPath": "example.com/ssa-example/pkg2",
    "Name": "pkg2",
    "Export": "/Users/yuchen.xie/Library/Caches/go-build/00/007812fbe54f26bd3164a1006097bc8e585ca6d7e2c2dc09d000699ebae90327-d",
    "Module": {
        "Path": "example.com/ssa-example",
        "Main": true,
        "Dir": "/Users/yuchen.xie/workspace/CD/ssa-example",
        "GoMod": "/Users/yuchen.xie/workspace/CD/ssa-example/go.mod",
        "GoVersion": "1.23"
    },
    "DepOnly": true,
    "GoFiles": [
        "pkg2.go"
    ],
    "CompiledGoFiles": [
        "pkg2.go"
    ],
    "Imports": [
        "example.com/ssa-example/pkg3"
    ]
}
{
    "Dir": "/Users/yuchen.xie/workspace/CD/ssa-example/pkg1",
    "ImportPath": "example.com/ssa-example/pkg1",
    "Name": "pkg1",
    "Export": "/Users/yuchen.xie/Library/Caches/go-build/27/2763ca28b8e616cc654df2738afcbfd5429187ed2422061bb197d1726b8c3ffe-d",
    "Module": {
        "Path": "example.com/ssa-example",
        "Main": true,
        "Dir": "/Users/yuchen.xie/workspace/CD/ssa-example",
        "GoMod": "/Users/yuchen.xie/workspace/CD/ssa-example/go.mod",
        "GoVersion": "1.23"
    },
    "GoFiles": [
        "pkg.go"
    ],
    "CompiledGoFiles": [
        "pkg.go"
    ],
    "Imports": [
        "example.com/ssa-example/pkg2"
    ]
}

The exportdata is the source of truth for packages loading, so there might be something inside it. By parsing them, I found the alias type information is missing in exportdata generated by go1.23. Hence, the issue is caused by the component who generates the exportdata file.

!<arch>
__.PKGDEF       0           0     0     644     609       `
go object darwin arm64 go1.23.3 GOARM64=v8.0 X:regabiwrappers,regabiargs,coverageredesign
build id "AApY-RhG_Ow7G2a6MwG8/gEGDXHQZLVRsYsnuUtfx"


$$B
u   

         $@v|?????????????????
                              kg1example.com/ssa-example/pkg2pkg2example.com/ssa-example/pkg1/Users/yuchen.xie/workspace/CD/ssa-example/pkg1/pkg.goAlias1Var1Var2       ?k????
$$
_go_.o          0           0     0     644     1067      `
go object darwin arm64 go1.23.3 GOARM64=v8.0 X:regabiwrappers,regabiargs,coverageredesign
build id "AApY-RhG_Ow7G2a6MwG8/gEGDXHQZLVRsYsnuUtfx"


!
go120ld?k????~??????$NNNNj???"??example.com/ssa-example/pkg2go:cuinfo.producer.example.com/ssa-example/pkg1go:cuinfo.packagename.example.com/ssa-example/pkg1example.com/ssa-example/pkg1.Var1go:info.int32example.com/ssa-example/pkg1.Var2go:i2!96H!?`4<autogenerated>/Users/yuchen.xie/workspace/CD/ssa-example/pkg1/pkg.go`?SԻ!?
,22drv???-??????-?????????????????
example.com/ssa-example/pkg1.Var1   
example.com/ssa-example/pkg1.Var2   -shared regabipkg1
!<arch>
__.PKGDEF       0           0     0     644     799       `
go object darwin arm64 devel go1.24-d20a4c2 Thu Oct 10 20:07:10 2024 +0000 GOARM64=v8.0 X:regabiwrappers,regabiargs,coverageredesign,aliastypeparams
build id "Q--gnzNdp1xe3K8whwha/w_XeWYiN9Jhbq_XEO-mM"


$$B
u
!%% $@v|??????????
#),7BMXY[]^fnv~pkg1example.com/ssa-example/pkg2pkg2example.com/ssa-example/pkg1/Users/yuchen.xie/workspace/CD/ssa-example/pkg1/pkg.goAlias1Var1/Users/yuchen.xie/workspace/CD/ssa-example/pkg2/pkg2.goAlias2Var2
        ̊v- ?
$$
_go_.o          0           0     0     644     1189      `
go object darwin arm64 devel go1.24-d20a4c2 Thu Oct 10 20:07:10 2024 +0000 GOARM64=v8.0 X:regabiwrappers,regabiargs,coverageredesign,aliastypeparams
build id "Q--gnzNdp1xe3K8whwha/w_XeWYiN9Jhbq_XEO-mM"


!
go120ld̊v-  ?????999c???????=a??example.com/ssa-example/pkg2go:cuinfo.producer.example.com/ssa-example/pkg1go:cuinfo.packagename.example.com/ssa-example/pkg1example.com/ssa-example/pkg1.Var1go:info.int32example.com/ssa-example/pkg1.Var2go:info.int64<autogenerated>/Users/yuchen.xie/workspace/CD/ssa-example/pkg1/pkg.go/Users/yuchen.xie/workspace/CD/ssa-exa2!le/pkg2/pkg2.go`??l?q<E`96H7~!?`
,22drv???-??????-?????????????????
example.com/ssa-example/pkg1.Var1   
example.com/ssa-example/pkg1.Var2   -shared regabipkg1

cmd/compile

By checking the code, I found it is go tool compile who generates the exportdata file, as the readme reads:

In addition to writing a file of object code for the linker, the compiler also writes a file of "export data" for downstream compilation units

// src/cmd/compile/internal/gc/obj.go
func dumpCompilerObj(bout *bio.Writer) {
    printObjHeader(bout)
    noder.WriteExports(bout)
}

writePkgStub

Then as I don't understand how go tool compile works, I tried to insert some println.

// src/cmd/compile/internal/noder/unified.go
func writePkgStub(m posMap, noders []*noder) string {
    pkg, info, otherInfo := checkFiles(m, noders)
+   names := pkg.Scope().Names()
+   for _, name := range names {
+       elem := pkg.Scope().Lookup(name)
+       print(elem.Type().String(), " ", elem.Name(), "\n")
+   }
# example.com/ssa-example/pkg2
example.com/ssa-example/pkg2.Alias2 Alias2
# example.com/ssa-example/pkg1
example.com/ssa-example/pkg1.Alias1 Alias1
example.com/ssa-example/pkg1.Alias1 Var1
int64 Var2

Then I have found the Var2 is resolved as int64 instead of Alias2. It means that the issue happened before writing.

collectObjects

Then, I tried to print the variables during typecheck, which checks the types for the AST parsing from the original files.

func (check *Checker) collectObjects() {
                values := syntax.UnpackListExpr(s.Values)
                for i, name := range s.NameList {
                    obj := NewVar(name.Pos(), pkg, name.Value, nil)
+                   fmt.Printf("resolver: declare var %s, type %s\n", name.Value, obj.Type())
@ @
                    check.declarePkgObj(name, obj, d)
+                   fmt.Printf("resolver: check type %s\n", obj.Type())
# example.com/ssa-example/pkg2
resolver: Alias2 Alias2 pkg2example.com/ssa-example/pkg2.Alias2 Alias2
# example.com/ssa-example/pkg1
resolver: Alias1 Alias1 pkg1resolver: declare var Var1, type %!s(<nil>)
resolver: check type %!s(<nil>)
resolver: declare var Var2, type %!s(<nil>)
resolver: check type %!s(<nil>)
example.com/ssa-example/pkg1.Alias1 Alias1
example.com/ssa-example/pkg1.Alias1 Var1
int64 Var2

The result shows the Var2 has int64 type, which means when associating type with var, it doesn't respect the type alias.

varDecl

Then, I tried to insert a println during variable declaration.

if typ != nil {
    obj.typ = check.varType(typ)
+       if tp, ok := typ.(*syntax.Name); ok {
+           fmt.Println("decl: syntax.Name", tp.Value)
+       }
+       fmt.Println("decl: varDecl", obj.name, obj.typ.String())
+       if se, ok := typ.(*syntax.SelectorExpr); ok {
+           info := se.X.GetTypeInfo()
+           tps := ""
+           if info.Type != nil {
+               tps = info.Type.String()
+           }
+           fmt.Println(se.Sel.Value, tps, info.Value, "end")
+       }
+       if obj.name == "Var2" {
+           fmt.Println(reflect.TypeOf(typ))
+       }
# test/pkg1
decl: syntax.Name Alias1
decl: varDecl Var1 test/pkg1.Alias1
decl: varDecl Var2 int64
Alias2  <nil> end
*syntax.SelectorExpr
test/pkg2
test/pkg1

The println change in varDecl doesn't convey new useful information as the type here is wrong as well. Hence, I need to check the typeDecl to see whether there is something wrong.

typeDecl

func (check *Checker) typeDecl(obj *TypeName, tdecl *syntax.TypeDecl, def *TypeName) {
    assert(obj.typ == nil)

    // Only report a version error if we have not reported one already.
    versionErr := false

    var rhs Type
    check.later(func() {
        if t := asNamed(obj.typ); t != nil { // type may be invalid
            check.validType(t)
        }
        // If typ is local, an error was already reported where typ is specified/defined.
        _ = !versionErr && check.isImportedConstraint(rhs) && check.verifyVersionf(tdecl.Type, go1_18, "using type constraint %s", rhs)
    }).describef(obj, "validType(%s)", obj.Name())
+   fmt.Println("typeDecl: ", obj.Name(), reflect.TypeOf(tdecl.Type), tdecl.Alias)
+   if strings.Contains(obj.Name(), "Alias") {
+       if n, ok := tdecl.Type.(*syntax.Name); ok {
+           fmt.Println(n.Value)
+       }
+   }
# example.com/ssa-example/pkg2
typeDecl:  A *syntax.Name false
typeDecl:  Alias2 *syntax.Name true
int64
# example.com/ssa-example/pkg1
typeDecl:  Alias1 *syntax.Name true
int32
1.obj type:  Var1 example.com/ssa-example/pkg1.Alias1
decl:  &{Alias1 {{{0x14000109ec0 7 10}} {{0x140004229c0 <nil> 2}}}} <nil>
1.obj type:  Var2 int64
decl:  &{0x14000424aa0 0x14000424b40 {{{0x14000109ec0 9 14}} {{0x1034de140 <nil> 2}}}} <nil>
example.com/ssa-example/pkg2
example.com/ssa-example/pkg1

The output shows during type declaration , the type is wrong as well. As a result, we need to find its caller.

selector

The Alias2 is accessed via pkg2.Alias2, so it's a selector type at the beginning. Try to add some println in selecotr part sounds great.

@@ -712,6 +713,19 @@ func (check *Checker) selector(x *operand, e *syntax.SelectorExpr, def *TypeName
                                check.objDecl(exp, nil)
                        } else {
                                exp = pkg.scope.Lookup(sel)
+                               if strings.Contains(exp.Name(), "Alias") {
+                                       if tn, ok := exp.(*TypeName); ok {
+                                               fmt.Printf(`selector: 
+       from pkg scope: %s
+       loop up sel name: %s
+       exp name: %s
+       isAlias: %t
+       found obj type: %s
+       found obj underlying type: %s\n`,
+                                                       pkg.name, sel, exp.Name(),
+                                                       tn.IsAlias(), tn.typ, tn.typ.Underlying())
+                                       }
+                               }
assign alias type to object Alias1
selector:
from pkg scope: pkg2
loop up sel name: Alias2
exp name: Alias2
isAlias: true
found obj type: int64
found obj underlying type: int64\n

From here we can know that during selector, the type is wrong as well. The value should come from the caller side as well.

newAlias

When I tried to add println in alias, I have found that both Alias1 and Alias2 have been defined as alias type correctly. This really confused me, if the Alias2 is defined as an alias, why in the following usages(as the output shows above) ALL use a wrong type(underlying type instead of alias)?

func (check *Checker) newAlias(obj *TypeName, rhs Type) *Alias {
                check.needsCleanup(a)
        }

+       println("assign alias type to object", obj.name)
+       println("newAlias", reflect.TypeOf(rhs).String())
+       if tn, ok := rhs.(*Basic); ok {
+               fmt.Printf(`newAlias: 
+       obj.type: %s
+       name: %s,
+       info: %d,
+       kind: %d
+       `,
+                       obj.typ.String(),
+                       tn.name,
+                       tn.info,
+                       tn.kind,
+               )
assign alias type to object Alias2
newAlias *types2.Basic
newAlias: 
        obj.type: example.com/ssa-example/pkg2.Alias2
        name: invalid type,
        info: 0,
        kind: 0

assign alias type to object Alias1
newAlias *types2.Basic
newAlias: 
        obj.type: example.com/ssa-example/pkg1.Alias1
        name: invalid type,
        info: 0,
        kind: 0

As a result, there must be something wrong between type check and resolving variable types. Let's verify what's the Alias2 type after typecheck, and it shows that the types are all resolved correctly.

        pkg, err := conf.Check(base.Ctxt.Pkgpath, files, info)
+       {
+               println(pkg.String())
+               a2 := pkg.Scope().Lookup("Alias2")
+               if a2 != nil {
+                       if a, ok := a2.(*types2.TypeName); ok {
+                               println("++ Alias2 is", a.IsAlias(), a.Type().String(), a.Type().Underlying().String())
+                       }
+                       println("++ check Alias2 type after check", a2.Type().String())
+               }
+               a1 := pkg.Scope().Lookup("Alias1")
+               if a1 != nil {
+                       if a, ok := a1.(*types2.TypeName); ok {
+                               println("++ Alias1 is", a.IsAlias(), a.Type().String(), a.Type().Underlying().String())
+                       }
+                       println("++ check Alias1 type after check", a1.Type().String())
+               }
+       }
++ Alias1 is true example.com/ssa-example/pkg1.Alias1 int32
++ check Alias1 type after check example.com/ssa-example/pkg1.Alias1
++ Alias2 is true example.com/ssa-example/pkg2.Alias2 int64
++ check Alias2 type after check example.com/ssa-example/pkg2.Alias2

This is quite weird, why the type is correct after typecheck but wrong during type resolving. This shouldn't happen, and I feel very confused.

Root Cause

By chance, I have checked the log and found that the order of printed messages are not expected. Then, I released the compile tool is triggered twice to compile pkg1 and pkg2! The correct type of Alias2 happens during the compilation for pkg2, and the failure happens during the compilation for pkg1 because it doesn't read the exportdata of pkg2 correctly.

Then I found inside the reader the enableAlias field is disabled for previous issue. Enabling it fixes the issue.

func ReadPackage(ctxt *types2.Context, imports map[string]*types2.Package, input pkgbits.PkgDecoder) *types2.Package {
    pr := pkgReader{
        PkgDecoder: input,

        // Currently, the compiler panics when using Alias types.
        // TODO(gri) set to true once this is fixed (issue #66873)
-       enableAlias: true,
+       enableAlias: true,

What confused me and haven't persisted the correct fix?

I have changed my fix because I found that the writer(types2) generates the alias inside exportdata regardless gotypesalias setting. Then I feel it's necessary to check it in reader, otherwise the reader won't respect the gotypesalias when it's disabled. Tool compile will generate exportdata with alias from imported packages as well.

However, it doesn't matter for this issue because x/tools packages.Load is the final reader and it resolves the gotypesalias properly, so it will decide to respect or skip the alias inside the exportdata. The correct direction is that the exportdata contains information to reflect the details of original packages as many as possible. Package loader could load the package information based on its configuration.

I'm a bit frustrated that I missed this chance to submit a fix commit, and this should blame my own knowledge. I should always focus on the original goal, and then step further to see how x/tools works, and I could win.

Good luck and be pataient next time!