Why Go Generics Hurt Performance: A Deep Dive into gcshape and Type Parameters
After a year of using Go's generics in production and open‑source projects, this article reveals the hidden performance penalties, pointer handling quirks, and best‑practice recommendations caused by the gcshape implementation and type‑parameter semantics.
Go's generics have been available for over a year, and after using them extensively in production and open‑source projects, I share a candid review of the experience.
Conclusion: generics are useful but come with many problems, especially severe performance regressions.
Implementation of Generics
Common generic implementations in other languages are:
Template instantiation (C++ style) – type parameters are placeholders replaced by concrete types, generating separate code for each.
Type erasure (TypeScript, Java) – type parameters are erased to a generic Object or interface, producing a single implementation.
Hybrid (C#) – appears as erasure but actually instantiates code per concrete type.
Go uses a different approach called
gcshape. Types that share the same underlying type belong to the same "shape". Pointers share a shape, but otherwise identical underlying types are considered different.
Example code demonstrating shape‑based instantiation:
<code>func Output[T any]() {
var t T
fmt.Printf("%#v\n", t)
}
type A struct{ a,b,c,d,e,f,g int64; h,i,j string; k []string; l, m, n map[string]uint64 }
type B A
func main() {
Output[string]()
Output[int]()
Output[uint]()
Output[int64]()
Output[uint64]()
Output[*string]()
Output[*int]()
Output[*uint]()
Output[*A]()
Output[A]()
Output[*B]()
Output[B]()
Output[[]int]()
Output[*[]int]()
Output[map[int]string]()
Output[*map[int]string]()
Output[chan map[int]string]()
}</code>Inspecting the symbol table (image) shows the generated shapes.
The motivation behind
gcshapeis to avoid code bloat and reduce GC overhead, because types with the same shape have identical memory layout. However, the implementation introduces serious issues.
Performance Problems
Benchmarks comparing generic code, generic‑with‑interface code, and non‑generic code show that pure generic functions are only ~10 % slower, but mixing interfaces with generics can be up to 100 % slower. Direct interface calls are fastest because the compiler optimises them.
Result image:
The slowdown stems from the fact that a type parameter
Tis not the concrete type; the compiler must look up the actual type in a type‑dictionary and perform an extra conversion. When the type parameter is an interface, an additional method‑lookup adds further overhead.
Only code that uses built‑in operators on the type parameter (e.g., arithmetic) avoids the penalty.
Creating Instances
When the constraint is
any, you can create a zero value with:
<code>func F[T any]() T {
var ret T
// use new(T) for a pointer if needed
return ret
}</code>If the constraint is an interface with methods, attempting to instantiate
Tdirectly leads to a panic because the underlying type is unknown.
Passing Pointers to Type Parameters
Never use a type parameter that is itself a pointer type. The following generic function fails to compile because the pointer base types differ:
<code>func Set[T *int | *uint](ptr T) {
*ptr = 1
}</code>The fix is to make the pointer explicit:
<code>func Set[T int | uint](ptr *T) {
*ptr = 1
}</code>Method Sets and Type Parameters
Calling a method on a type parameter requires the constraint to include that method. Using a pointer constraint
~*A | ~*Bwithout the method leads to "a.Hello undefined". Adding the method to the constraint solves the issue:
<code>type API[T any] interface {
*T
Hello()
}
func SayHello[PT API[T]](a PT) {
a.Hello()
}</code>Copying Objects
Shallow copy works with
b := a. For generic pointer types you can dereference, copy, and re‑wrap:
<code>func DoCopy[T any, PT API[T]](a PT) {
b := *a // shallow copy of the value
(PT(&b)).Set(222222) // modify through the interface
fmt.Println(a, b)
}</code>Deep copy requires a custom
Clonemethod defined in an interface such as
Cloneable[T any] interface{ Clone() T }.
Summary
The main pain points of Go generics are:
Use
*Texplicitly instead of letting
Trepresent a pointer type.
Use
[]Tor
map[K]Vinstead of letting
Tstand for a slice or map.
Prefer generic structs over generic functions.
Be aware that the core type of a constraint determines which operations are allowed.
For now, stick to well‑tested generic libraries that operate on slices and maps, and avoid complex generic tricks that involve pointer creation or method sets.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.