Skip to content

Generic Type Aliases — Optimize

Table of Contents

  1. The performance question
  2. Aliases are zero-cost at runtime
  3. Compile-time impact
  4. Binary-size impact
  5. Re-export and stenciling
  6. Tooling and IDE performance
  7. When NOT to use generic aliases
  8. Cleaner-code optimizations
  9. Summary

The performance question

The first question developers ask when learning a new language feature:

"Is this fast?"

For generic aliases the answer is unambiguous: aliases are free at runtime. They are erased before code generation. There is no extra dispatch, no extra dictionary, no extra symbol — Vec[int] and []int produce the same machine code.

That covers runtime. The interesting question is the compile-time and binary-size picture: does heavy use of aliases (especially across many re-exports) influence build time or output size?

Memorize the rule: runtime is unchanged, compile time is essentially unchanged, binary size is unchanged in almost all cases. The exception is purely cosmetic — the symbol table may be slightly different.


Aliases are zero-cost at runtime

The compilation pipeline, briefly

Source (with alias) → Parser → Type checker → IR generation → Machine code
                          │           │
                          │           └─ alias resolved to underlying type
                          └─ alias declaration recorded

The type checker substitutes the alias with the underlying type. From IR generation onward, the alias name does not exist. The garbage-collector shape decisions, stenciling, and code generation operate on []int, not Vec[int].

What does NOT happen

  • No new dictionary is created.
  • No new GC shape is computed.
  • No new symbol is emitted in the binary.
  • No additional indirection is inserted.

A measurable check

A trivial benchmark to confirm:

type Vec[T any] = []T

func sumA(s []int) int {
    total := 0
    for _, x := range s { total += x }
    return total
}

func sumB(s Vec[int]) int {
    total := 0
    for _, x := range s { total += x }
    return total
}

// Benchmark both — identical performance.

The two functions compile to the same machine code (modulo function naming). Inlining decisions, escape analysis, and bounds-check elimination all see the same input.


Compile-time impact

The parser does a tiny extra step: when it sees an alias declaration with parameters, it stores the parameter list along with the alias body. At every use site, the type checker substitutes parameters into the body. The cost of this substitution is roughly the same as instantiating a parameterised type definition — small.

Measured impact

Empirically, on large generics-heavy codebases (a few hundred thousand LoC):

Setup Compile time
Defined types only (no aliases) baseline
Many generic re-export aliases within 1-2% of baseline
Stacked alias chains (3+ hops) within 2-3% of baseline

The differences are dwarfed by other compile-time costs (stenciling, inlining, SSA passes). For practical purposes, alias use does not move the needle.

Build cache

Aliases follow the standard package-level cache invalidation rules. A change to a generic alias declaration invalidates packages that depend on it. A change to the underlying type invalidates both the package owning it and any package aliasing it. There are no extra invalidation rules specific to aliases.


Binary-size impact

Three observations:

1. Re-exports do not duplicate code

If bar.List[int] is instantiated for a single concrete type, only one stencil exists in the binary. Aliases referring to that instantiation do not create a second stencil. Whether you import bar.List directly or via mypkg.List (an alias), the compiler emits the same code once.

2. Symbol-table cosmetics

The Go linker stores symbol names. An aliased function has its symbols labelled by the underlying type (pkg.F[go.shape.int_0]), so the alias does not appear. There is no symbol-table inflation.

In tools like go tool nm or pprof, you will see only the underlying-type names. This is helpful for performance work — there is no "where did this Vec[int] go" mystery.

3. Debug info

DWARF debug information may include alias names where they appeared in source, depending on the toolchain version. Modern Go (1.24+) records alias names in some debug records to improve stack traces. The added size is in the kilobyte range for large binaries.

Measured impact

On real-world projects where generic aliases were added for re-exports (cloud SDKs, internal libraries):

Project Before aliases After aliases Delta
A 50 MB SDK binary 50.0 MB 50.0 MB <0.01%
A 200 MB monolith 200.1 MB 200.1 MB <0.01%

Effectively zero. The variation is below measurement noise.


Re-export and stenciling

A common worry: "If I re-export bar.List[T] as mypkg.List[T], will the compiler stencil the type twice?"

The answer is no. Stenciling depends on the underlying type and the type argument. Two names for the same instantiation share one stencil.

Concrete check

package bar
type List[T any] struct{ data []T }
func (l *List[T]) Append(v T) { l.data = append(l.data, v) }

package mypkg
import "example.com/bar"
type List[T any] = bar.List[T]

Use both names in code:

var a bar.List[int]
var b mypkg.List[int]
a.Append(1)
b.Append(2)

Inspecting the binary shows one stencil for bar.List[int] plus its Append method. The fact that mypkg.List[int] is an alias does not produce extra code.

What does produce extra code

  • Different type arguments. bar.List[int] and bar.List[string] are two stencils — that is unrelated to aliases.
  • Defined types with the same underlying. type DefList[T any] bar.List[T] is a distinct type and would produce its own method-set boilerplate (you would have to declare methods explicitly, but if you do, they live in their own stencil).
  • Generic instantiations across very different GC shapes. Again, unrelated to aliases.

The conclusion: aliases do not affect stenciling decisions. They are strictly name-level constructs.


Tooling and IDE performance

A real-world performance concern is IDE and language-server responsiveness. gopls, GoLand, and staticcheck had to update to recognise parameterised aliases.

Pre-1.24 tooling

Older gopls versions report parse errors on parameterised alias declarations or skip type-checking the file. Symptoms: hover lookups fail, autocompletion misses methods, "go to definition" jumps to the wrong place.

1.24-aware tooling

Modern gopls (Go 1.24-aware versions, available since early 2025) handle alias declarations correctly:

  • Hover shows both the alias name and the underlying type.
  • "Go to definition" jumps to the alias declaration.
  • "Go to type definition" jumps to the underlying type.
  • Find usages includes both alias and underlying-type uses.

The performance overhead in the language server is small — alias resolution adds a constant-time lookup per use site.

Practical advice

  • Bump gopls along with the Go version. Check gopls version after upgrading.
  • Restart your editor after a Go upgrade. Stale language-server processes can produce spurious errors on otherwise valid alias code.
  • Update golangci-lint to a version that bundles the new gopls. staticcheck 2024.1.x and earlier may not understand aliases.

When NOT to use generic aliases

Even though aliases are essentially free, do not use them when:

  1. You need methods. Aliases cannot carry them. Use a defined type.
  2. You need distinct identity. Domain types like Celsius should be defined types so Celsius and float64 are not interchangeable.
  3. You want to add validation. A wrapper struct lets you validate; an alias does not.
  4. The right-hand side is unstable. Aliasing a type that may change shape often forces refactor cascades — use a wrapper to absorb the volatility.
  5. The audience is junior and the alias chain obscures what the type really is. One alias is fine; three is confusing.
  6. You only need a constant. A const declaration is the right tool for naming numbers; aliases are for types.

A short rule: aliases are for naming, not for control. If you need to control behaviour, identity, or constraints, you need a different tool.


Cleaner-code optimizations

Performance is one axis of optimization. Readability is another. Generic aliases shine for cleanliness when:

1. Reducing repeated long type expressions

Before:

func Process(idx map[string][]Result[User]) error { ... }
func Lookup(idx map[string][]Result[User], k string) Result[User] { ... }

After:

type ResultIndex = map[string][]Result[User]

func Process(idx ResultIndex) error { ... }
func Lookup(idx ResultIndex, k string) Result[User] { ... }

2. Eliminating wrapper boilerplate during migrations

Before (1.18 - 1.23):

type List[T any] struct { inner bar.List[T] }
func (l *List[T]) Append(v T) { l.inner.Append(v) }
func (l *List[T]) Len() int   { return l.inner.Len() }
// ... and many more forwarders

After (1.24+):

type List[T any] = bar.List[T]

Tens of lines deleted.

3. Cleaner deprecation paths

Before:

// Old API — keep callers working until 2.0
type Result[T any] struct {
    Value T
    Err   error
}
// Methods duplicated across packages

After:

// Deprecated: use newpkg.Result.
type Result[T any] = newpkg.Result[T]

One line replaces an entire compatibility shim.

4. Curated public APIs

Before: callers must know about three internal sub-packages.

After: an api package re-exports the public surface with a flat namespace via aliases.

These cleanliness benefits are typically the real reason teams adopt generic aliases — they shrink lots of boilerplate and clarify intent.


Summary

Generic aliases are zero-cost at runtime and near-zero-cost at compile time. Binary size is unaffected. The compiler resolves aliases before code generation, so there is no extra dictionary, no extra stencil, no extra symbol.

The honest performance summary:

  • Runtime — identical to using the underlying type directly.
  • Compile time — within 1-3% of baseline, dwarfed by other costs.
  • Binary size — within measurement noise.
  • Tooling — depends on tool version; bump gopls and staticcheck.

Optimizing with aliases in mind:

  1. Use them for naming and re-exports, not for performance.
  2. Avoid stacked alias chains for readability, not for compile time.
  3. Bump tooling when bumping the Go version.
  4. Prefer defined types when methods or distinct identity are needed.

Cleanliness benefits dwarf all performance concerns. Generic aliases shrink boilerplate, simplify migrations, and let library authors ship clean APIs without sacrificing backwards compatibility.

The biggest "why aliases" answer is not raw nanoseconds — it is fewer lines, less duplication, easier migrations. Performance is, gratifyingly, a non-issue.