What version of Go are you using (go version)?
go version output
$ go version
go version devel go1.24-8191cd8868 Mon Apr 28 16:42:17 2026 +0000 darwin/arm64
Does this issue reproduce with the latest release?
Yes.
What operating system and processor architecture are you using (go env)?
go env output
GOOS="darwin"
GOARCH="arm64"
What did you do?
I encountered a data race when multiple goroutines concurrently call types.Identical on *types.Alias instances produced by golang.org/x/tools/go/ssa with InstantiateGenerics.
The issue occurs because SSA's substituter calls types.Instantiate (without a checker and without cleanup) when specializing generic types. The resulting *types.Alias keeps .actual = nil. When concurrent goroutines call types.Identical on these aliases, it triggers Unalias, which races on the path-compression write at $GOROOT/src/go/types/alias.go:121 versus the read at alias.go:112.
Minimal reproducer:
// Minimal reproducer: data race in go/types when concurrent goroutines
// call types.Identical on alias instantiations produced by
// golang.org/x/tools/go/ssa.
//
// This file is fully self-contained: it materializes the two GOPATH
// packages (p1, p2) from embedded source strings into a tempdir at
// startup, then loads them with golang.org/x/tools/go/packages, builds
// SSA with InstantiateGenerics, and races types.Identical on every
// alias the program touches. SSA's substituter calls types.Instantiate
// (no checker, no cleanup) when it specializes p1.New[T1]; the
// resulting *types.Alias for p1.A1[p2.T1] keeps .actual = nil. The
// concurrent Identical calls race on the path-compression write at
// $GOROOT/src/go/types/alias.go:121 vs the read at alias.go:112.
//
// Run:
// GODEBUG=gotypesalias=1 go run -race ./main.go
package main
import (
"go/types"
"os"
"path/filepath"
"runtime"
"sync"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
)
const p1Src = `package p1
type A1[T any] = func(T)
type S1[T any] struct{ F A1[T] }
func New[T any]() *S1[T] { return &S1[T]{} }
`
const p2Src = `package p2
import "p1"
type T1 struct{}
var V1 = p1.New[T1]()
`
func main() {
gopath, err := os.MkdirTemp("", "alias-race-")
if err != nil {
panic(err)
}
defer os.RemoveAll(gopath)
for _, f := range []struct{ rel, src string }{
{"src/p1/p1.go", p1Src},
{"src/p2/p2.go", p2Src},
} {
full := filepath.Join(gopath, f.rel)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
panic(err)
}
if err := os.WriteFile(full, []byte(f.src), 0o644); err != nil {
panic(err)
}
}
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedFiles |
packages.NeedImports | packages.NeedDeps |
packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo,
Dir: gopath,
Env: append(os.Environ(), "GO111MODULE=off", "GOPATH="+gopath),
}
pkgs, _ := packages.Load(cfg, "p2")
prog, _ := ssautil.AllPackages(pkgs, ssa.InstantiateGenerics)
prog.Build()
// Walk every SSA function signature and collect every *types.Alias
// reachable from it.
seen := map[types.Type]bool{}
var aliases []*types.Alias
var walk func(t types.Type)
walk = func(t types.Type) {
if t == nil || seen[t] {
return
}
seen[t] = true
switch x := t.(type) {
case *types.Pointer:
walk(x.Elem())
case *types.Struct:
for i := 0; i < x.NumFields(); i++ {
walk(x.Field(i).Type())
}
case *types.Tuple:
for i := 0; i < x.Len(); i++ {
walk(x.At(i).Type())
}
case *types.Signature:
walk(x.Params())
walk(x.Results())
case *types.Named:
walk(x.Underlying())
case *types.Alias:
aliases = append(aliases, x)
walk(x.Rhs())
}
}
for fn := range ssautil.AllFunctions(prog) {
walk(fn.Type())
}
// Workaround: single-threaded warm-up. Calling types.Unalias on each
// *Alias from one goroutine populates .actual; after this pass every
// subsequent Unalias call hits the a.actual!=nil early-return at
// alias.go:113 and skips the racy write at alias.go:121.
if os.Getenv("WARMUP") == "1" {
for _, a := range aliases {
_ = types.Unalias(a)
}
}
// Race: many goroutines call types.Identical on every collected
// alias. types.Identical calls Unalias on its operands; for aliases
// whose .actual is still nil (those produced by SSA's substituter
// via types.Instantiate without cleanup), the first call performs a
// path-compression write at alias.go:121 while concurrent calls
// read at alias.go:112 — a data race.
var wg sync.WaitGroup
for i := 0; i < runtime.GOMAXPROCS(0); i++ {
wg.Add(1)
go func() {
defer wg.Done()
for _, a := range aliases {
_ = types.Identical(a, a)
}
}()
}
wg.Wait()
}
How to reproduce:
GODEBUG=gotypesalias=1 go run -race main.go
What did you expect to see?
No data race detected.
What did you see instead?
==================
WARNING: DATA RACE
Read at 0x... by goroutine ...:
go/types.Unalias()
$GOROOT/src/go/types/alias.go:112
go/types.Identical()
$GOROOT/src/go/types/predicates.go:...
Previous write at 0x... by goroutine ...:
go/types.Unalias()
$GOROOT/src/go/types/alias.go:121
go/types.Identical()
$GOROOT/src/go/types/predicates.go:...
==================
The race occurs because types.Instantiate (when called without cleanup by SSA) creates aliases with .actual = nil, and the path-compression logic in Unalias is not thread-safe for this case.
Workaround
Setting WARMUP=1 before running the reproducer populates .actual in a single-threaded pass, avoiding the race:
GODEBUG=gotypesalias=1 WARMUP=1 go run -race main.go
This demonstrates that pre-warming the .actual field eliminates the race.
Potential fixes
- Add synchronization (mutex) to the path-compression write in
Unalias
- Ensure SSA (or other callers of
types.Instantiate) always call cleanup to populate .actual
- Make the path-compression logic thread-safe via atomic operations
cc @golang/compiler @golang/types
What version of Go are you using (
go version)?go versionoutputDoes this issue reproduce with the latest release?
Yes.
What operating system and processor architecture are you using (
go env)?go envoutputWhat did you do?
I encountered a data race when multiple goroutines concurrently call
types.Identicalon*types.Aliasinstances produced bygolang.org/x/tools/go/ssawithInstantiateGenerics.The issue occurs because SSA's substituter calls
types.Instantiate(without a checker and without cleanup) when specializing generic types. The resulting*types.Aliaskeeps.actual = nil. When concurrent goroutines calltypes.Identicalon these aliases, it triggersUnalias, which races on the path-compression write at$GOROOT/src/go/types/alias.go:121versus the read atalias.go:112.Minimal reproducer:
How to reproduce:
What did you expect to see?
No data race detected.
What did you see instead?
The race occurs because
types.Instantiate(when called without cleanup by SSA) creates aliases with.actual = nil, and the path-compression logic inUnaliasis not thread-safe for this case.Workaround
Setting
WARMUP=1before running the reproducer populates.actualin a single-threaded pass, avoiding the race:This demonstrates that pre-warming the
.actualfield eliminates the race.Potential fixes
Unaliastypes.Instantiate) always call cleanup to populate.actualcc @golang/compiler @golang/types