Type Aliasing
Reference index · Summary of this topic Go supports two kinds of type aliasing: a “type definition” and a “type alias declaration”.
Type Definitions
For a Go “type definition” the new type is a distinct type that shares an underlying type with its base. Because converted types are structs (no inheritance), the converter relies on the source generators (see Source Generators) to emit the bridging needed for these to be used interchangeably while remaining distinct: implicit conversion operators down to the underlying type (via ImplicitConvGenerator / TypeGenerator), and, where the base is a built-in like slice, the relevant interface (ISlice<T>, etc.) implementation. A named type also supports the extension methods (receiver functions) of its underlying types, which the generators surface as proxy/overload methods.
When a pointer conversion (*Target)(srcPtr) bridges two structurally-identical structs, the converter records an indirect (boxing) implicit conversion Source → ж<Target> and ImplicitConvGenerator emits implicit operator ж<Target>(Source src) => Ꮡ(new Target(<members>)). For a self-boxing conversion — Source and Target are the same struct (mspan → ж<mspan>), which arises from a self-referential struct’s recursive sub-struct conversions — that member-by-member reconstruction is both unnecessary and wrong: a pointer field whose target ctor parameter is itself a ж<…> was deref’d (src.f?.Value ?? default!), and a value cannot bind a pointer parameter (CS1503). The generator detects self-boxing (the boxed element type equals the source) and emits Ꮡ(src) instead — boxing a copy of the whole struct directly, identical in effect for a pointer-free struct and correct for one with pointer fields. (Validated by the green baseline build, which regenerates every .g.cs, plus the TypeConversion behavioral test for the non-self-boxing form; runtime exercised self-boxing for mspan, g, stackScanState, hmap, etc.)
Type Alias Declarations
For a Go “type alias declaration” the alias matches C# aliasing implemented with the using keyword. Since the alias may be exported and referenced across files, the converter emits a global using (C# 10’s Global Using Directive) into the package’s generated aliases. For example:
type P = *bool
type M = map[int]int
type table = map[string]int
global using P = go.ж<bool>;
global using M = go.map<nint, nint>;
global using table = go.map<go.@string, nint>;
A global using RHS renders csproj-alias numerics as C# keywords. C# resolves a using directive’s target without reference to other using directives — aliases are invisible to one another — so the golib csproj-level aliases (uint64, float64, any, …) that resolve everywhere else in the compilation are CS0246 inside global using X = …;. The alias-declaration emission (only) rewrites those names to their using-safe keyword/BCL equivalents: fiat’s type p224UntypedFieldElement = [4]uint64 emits global using p224UntypedFieldElement = go.array<ulong>;. Body code keeps the Go-visual alias names; already-safe names (byte, bool, nint, go.@string) are untouched, and the rewrite deliberately skips dot-qualified names so a package type sharing a builtin name is left alone. (Guarded by the AliasStructComposite extension words — an alias to [4]uint64 used as a parameter type, output-compared.)
An alias to an unnamed array/slice resolves through types.Unalias at type-switched decision points. An alias is a *types.Alias (Go 1.22+) — neither the AST’s ast.ArrayType nor the resolved *types.Array — so an emission that type-switches on the syntax node or the unresolved type misses it. Three sites resolve through types.Unalias: (1) the range operand dispatch — for _, e := range w on an alias-typed array would otherwise match no arm and emit the whole loop as a C# comment, a silent behavioral hole; unaliased, it emits the array-value range form, foreach (var (_, e) in w.ΔRangeSnapshot()) (a range over an array value iterates a copy, see range over an ARRAY VALUE iterates a COPY). (2) the composite-literal dispatch — words{10, 20, 30, 40} emits the same element-array projection the unnamed literal uses, new uint64[]{10, 20, 30, 40}.array(); the alias name renders as an Ident (not an ast.ArrayType), so it cannot take the composite-initializer bracket rewrite, and keeping it produced a C# collection initializer on the alias (new words{…} — CS1061, array<T> has no Add). (3) var declarations — a local or package-level var w words allocates the fixed-size backing (words z = new(4); / internal static words gw = new(4);) instead of default!/uninitialized, whose null backing array throws NRE on the first element write. An alias to a named type is unaffected: unaliasing lands on *types.Named and the existing wrapper-struct arms apply, with the alias name preserved. (Guarded by the AliasStructComposite extensions — alias-typed range loop, composite literal, and local + global var with element writes, values vs Go.)
A same-package alias TARGET carries the package’s FULL namespace, not just its class. An exported alias whose target is lifted into the package class — type CorpusEntry = struct{…} lifts its anonymous struct to a nested CorpusEntryᴛ1 (and the same for an alias to a same-package named type) — must qualify that target with the package’s whole namespace. For a package in a nested namespace (internal/fuzz → namespace go.@internal, class fuzz_package; net/http → go.net / http_package) the lifted type lives at go.@internal.fuzz_package.CorpusEntryᴛ1. A qualifier built from the bare <pkg>_package class alone would drop the intervening namespace segment, so global using CorpusEntry = go.fuzz_package.CorpusEntryᴛ1; — and the matching [assembly: GoTypeAlias("CorpusEntry", "go.fuzz_package.CorpusEntryᴛ1")] that every consumer replays verbatim through its <ImportedTypeAliases> block — would name a namespace that does not exist → CS0234 at the using-alias line and at every use. The qualifier is taken from the same packageNamespace that emits the namespace …; declaration (minus the root, plus the class), so the target and the declaration always agree: go.@internal.fuzz_package.CorpusEntryᴛ1. A top-level package’s namespace is exactly the root (go), leaving no intervening segment, so its target stays go.<pkg>_package.… — the emission is byte-for-byte unchanged there. (Guarded by the NestedAliasUser behavioral test — a top-level package main that imports its own nested inner subpackage, whose C# namespace is go.NestedAliasUser; inner exports an anon-struct alias Entry, and both inner’s own global using and the consumer’s imported global using innerꓸEntry resolve to go.NestedAliasUser.inner_package.Entryᴛ1, values vs Go.)
The whole RHS is namespace-ROOTED, at every nesting depth — the alias resolves at COMPILATION scope. The two paragraphs above each close one hole in this wall; this is the wall. C# resolves a using alias’s target as if the immediately containing compilation unit had no using directives, which puts it outside the file’s namespace go;, outside the emitted <pkg>_package class, and with none of the csproj-level golib aliases in effect. Every other rendering in the converter elides the root namespace from nested names, because every other rendering lands inside namespace go — where the elision is legal and is what makes the emitted C# read like Go. Rooting only the outermost name would leave each type ARGUMENT under it naming nothing: type names = []string would emit global using names = go.slice<@string>;, CS0246 on @string. The same holds for slice, error, complex64 and ж; for a same-package Header (which is go.main_package.Header at that scope); for a cross-package io_package.Reader (go.io_package.Reader); and for the BCL Func/Action of a func-type alias, since System is not in scope there either.
The alias emission therefore renders in a rooted-nesting mode, in which the target and everything it nests carry full qualification:
type Header struct{ Name string }
type nested = map[string][]Header
type fn = func(string) int
type rdrs = []io.Reader
global using nested = go.map<go.@string, go.slice<go.main_package.Header>>;
global using fn = System.Func<go.@string, nint>;
global using rdrs = go.slice<go.io_package.Reader>;
Four qualifiers are in play and they are not interchangeable: golib types root to go., the BCL delegates to System., golib’s variadic delegate FAMILY (Actionꓸꓸꓸ/Funcꓸꓸꓸ) back to go., and a same-package name to go.<ns>.<pkg>_package. — the mechanism of the paragraph above, applied at every depth rather than to the target alone (so the target needs no separately computed prefix). The csproj-alias names are the exception that proves the rule: uint64, float64, any and friends are not members of go at all, so they are substituted with the keyword or BCL type they stand for rather than rooted — the first paragraph’s rewrite, moved inward. An alias whose target is ITSELF an alias resolves through types.Unalias before rendering, since a C# using alias may not name another using alias.
This is a user-code defect rather than a corpus one, and the standard library shows it for the type-ARGUMENT arm: its package-level aliases with type arguments are fiat’s p224/p256/p384/p521 field-element aliases, each [4]uint64, which take a C# keyword as their argument, so that arm moves nothing there. An end-user package that aliases a slice, map, channel or func type — the ordinary type Handlers = map[string]Handler — needs the rooted mode on its first build, as does a -recurse module conversion over a third-party type.
The substitution arm is the one with corpus sites. A csproj-alias name reaching the alias RHS as the WHOLE target must be substituted, not rooted — go.int32 is CS0234, which the compiler rejects with “the type or namespace name int32 does not exist in the namespace go”, since int32 is a <Using Alias=…> for System.Int32 and not a member of go at all. getUsingAliasSafeTypeName could not catch it because that sweep deliberately skips dot-qualified names, exactly so a package type sharing a builtin name is left alone. The sites were all cgo _C_* typedefs in darwin-exclusive files (os/user/darwin/cgo_lookup_syscall.cs, net/darwin/cgo_unix_syscall.cs), which a default build never compiles (the default $(GoTargetOS) is windows). type _C_int = int32 emits global using _C_int = int;, and the neighbours that were already right are unmoved — _C_char = byte (a C# keyword) and _C_size_t = go.uintptr (uintptr IS a real golib struct, so rooting it is correct). (Guarded by the PackageAliasRootedTypeArgs behavioral test — twenty-five package-level aliases covering golib element types, keyword element types that must NOT be rooted, same-package named types at one and two levels of nesting, a lifted anonymous struct and interface, a cross-package interface, both directional channel forms, both delegate spellings, and an alias to an alias, output-compared vs Go. Also pinned by TestRecurseChannelOfHyphenatedModulePath, whose assertion holds the rooted cross-package form.)
Rooting is IDEMPOTENT: an already-global::-rooted target is not rooted again. The rooted mode
above prefixes the root namespace onto every name it renders, and one caller hands it names that are
already rooted — a white-box test conversion, whose test-alias qualifiers build a reference to a
production declaration with an explicit global:: (global::go.net.netip_package.uint128). Prefixed
a second time that becomes go.global::go.net.netip_package.uint128, which is CS7000 “unexpected
use of an aliased name”: global:: is the root, so anything in front of it is by construction not a
name. net/netip’s export_test.go re-exports two unexported production types this way
(type Uint128 = uint128, type AddrDetail = addrDetail); rooted twice, both alias lines would fail
to parse, and the package’s tests with them. The renderer returns a global::
target unchanged, stated at the renderer rather than at the one caller, because every future caller
wants the same answer. (Guarded by mixedKeyedComposite_test.go’s
TestRootedUsingAliasKeepsGlobalQualifier, which asserts the rooted and unrooted renders both
leave such a name alone.)
Two imported packages that share a name keep their own aliases, and a reference names its own package’s target. a/foo and b/foo are both package foo, so an alias name both publish (Alias, or the rename a method-colliding type is published under, Kind for ΔKind) has no single global using: neither is declared, and each reference resolves through the package go/types says it belongs to. Where a reference can only be written with the file’s import qualifier, it spells the TARGET’s member, the collision rename included, in every position: a declaration, a func type’s parameter and result, a struct field of func type, and a conversion. logrus’ hooks/slog test imports log/slog and logrus’ own hooks/slog, both package slog, whose Level types are both renamed ΔLevel, so a field mapper func(logrus.Level) slog.Level emits Func<logrus.Level, slog.ΔLevel>; spelling the Go name, slog.Level, names no type (CS0426). (Guarded by TestSameNamedImportsKeepTheirOwnTypeAliases and the SameNameImportAlias behavioral test.)
Generic Type Aliases
A Go 1.24 generic alias (type A[T any] = Box[T]) cannot be a C# using alias: a using directive cannot declare type parameters (using A<T> = Box<T>; is CS1002), and a closed alias cannot take type arguments at a use (CS0307). A Go alias is its target (identity, method set, assignability), so the converter renders the target wherever the alias is named:
type P[K comparable, V any] = Pair[K, V]
type StrPair[V any] = Pair[string, V]
type IntMap[V any] = map[int]V
func swap[T comparable](p P[T, T]) P[T, T] { return P[T, T]{Key: p.Val, Val: p.Key} }
m := IntMap[string]{1: "one"}
// type P[K comparable, V any] = Pair[K, V]
// type StrPair[V any] = Pair[string, V]
// type IntMap[V any] = map[int]V
internal static Pair<T, T> swap<T>(Pair<T, T> p) {
return new Pair<T, T>(Key: p.Val, Val: p.Key);
}
var m = new map<nint, @string>{[1] = "one"};
- Declaration. The alias emits a one-line comment holding its Go text, and nothing else: no
global usingand noGoTypeAliasrecord. A record would reach every importer as aglobal usingover an unboundgo.T, breaking packages that never name the alias. - Uses.
genericAliasTarget(typeNameResolution.go) answerstypes.Unalias(t)for a*types.Aliaswith type arguments or type parameters.getAliasQualifiedTypeNameandgetFullyQualifiedTypeNamecall it first, and a composite literal whose type syntax names a generic alias renders the resolved target instead of the syntax. An alias of an alias resolves all the way (SP[bool]→Pair<@string, bool>), and a cross-package target takes the file’s import alias (ga.Box<nint>). - Plain aliases are untouched. An alias without type parameters, including one over an instantiated generic (
type IntBox = Box[int]), keeps itsglobal using, its record and its name at every use. - Not supported: a generic alias whose target is an anonymous struct or interface type (
type Cell[T any] = struct{ Row, Col T }). The converter reports it with a conversion warning and does not yet convert it: the declaration emits only the comment, and a use does not compile. Its target is a different anonymous type per instantiation, and lifting it as a generic type of its own would makeCell[int]a different type fromstruct{ Row, Col int }.
Guarded by the GenericTypeAlias behavioral test (with GenericTypeAliasLib and GenericTypeAliasBystander, a consumer that never names an alias) and by TestGenericTypeAliasRendersItsTarget and TestPlainTypeAliasKeepsItsName.
A func-type alias target is built from its signature. type DurFn = func(time.Duration) int emits global using DurFn = System.Func<go.time_package.Duration, nint>;, and a variadic type Var = func(...int) int emits global using Var = go.Funcꓸꓸꓸ<nint, nint>;. usingAliasDelegateType renders each element in its rooted form and roots the delegate family at System. (or go. for the variadic family). Named results keep their names in the tuple ((nint n, bool ok)). An importer reads the record’s System.-rooted target as it is, never qualifying it into the declaring package’s class. (Guarded by the AliasImport behavioral test, with AliasImportLib.)
An exported alias of an unexported type makes that type public. Go lets any importer name the target through the alias: logrus declares type MutexWrap = mutexWrap, and testify declares type CompareType = compareResult. Every importer declares each exported alias of an import as a global using over the target, whether it uses the alias or not. An internal target would therefore fail in every importing assembly (CS0122). collectPublicizedTypes treats an exported alias like any other exported surface. It publicizes the unexported types the alias’s target exposes, and the usual cascade then makes their exported methods public. An alias declared in a _test.go file is the exception, because only the package’s own tests can see it, and they already have internal access. runtime’s export_test.go (type G = g) is the standard library’s case: widening g there would contradict production’s internal declaration (CS0262). The standard library has no exported alias of this kind in production code. (Guarded by the ExportedAliasUnexportedTarget behavioral test, with one used and one unused alias across assemblies, and by TestAnExportedAliasPublicizesItsUnexportedTarget, TestAnUnexportedAliasPublicizesNothing and TestATestFileAliasPublicizesNothing.)
← Generic Constraints · Index · Delegates to Value Receiver Instances →