Limitations of Go generics
You are building a high-performance data structure. You want a Stack that works for int, string, and a custom Point struct. You write type Stack[T any] struct and implement Push and Pop. It compiles. It runs fast. You feel the power of type-safe code reuse.
Then you hit the wall. You try to use the stack with a type from a C library via cgo. The compiler rejects it. You try to add a generic method to an interface so any type can be processed generically. The compiler rejects that too. You try to constrain a type parameter to "must have a field ID" without defining an interface. The compiler says no.
Go generics are a compile-time feature designed for pure Go code. They bring significant power, but they come with hard boundaries. Understanding these limits prevents wasted time and points you toward idiomatic workarounds.
The Go/C boundary
Generics do not cross the cgo boundary. You cannot use a C type as a type parameter, and you cannot pass a generic type parameter directly to a C function.
C types like C.int, C.char, or structs defined in C headers are opaque to the Go type system in the context of generics. The Go compiler handles generics by generating specialized code for each type used. C types are managed by the C compiler and the cgo glue layer. Their memory layout and semantics are determined by the C toolchain, not the Go type checker. Mixing the two would require the Go compiler to understand C type details during generic instantiation, which breaks the separation of concerns and adds complexity the language designers chose to avoid.
Here's a generic cache that works for Go types but fails when you try to pass a C type.
// Package main demonstrates the cgo limitation.
package main
import (
"fmt"
)
// #cgo LDFLAGS: -lm
// #include <math.h>
import "C"
// Cache stores a value and returns it.
func Cache[T any](val T) T {
// Return the value immediately for demonstration.
return val
}
func main() {
// Works with Go types.
s := Cache("hello")
fmt.Println(s)
// Fails with C types.
// cVal := Cache(C.int(42)) // Error: C.int is not a valid type parameter
}
The compiler rejects the commented line with C.int is not a valid type parameter. The error is immediate. The Go compiler simply does not allow C types in the type parameter slot.
You also cannot pass a generic type parameter to a C function. If you try to write a wrapper that forwards a generic value to C, the compiler stops you.
// Package main shows passing generics to C.
package main
import "C"
// CallC tries to pass a generic type to C.
// func CallC[T any](t T) {
// // Error: cannot use generic type T in C call
// _ = C.some_function(t)
// }
The compiler complains with cannot use generic type T in C call. The C side needs concrete types. It cannot accept a placeholder that gets resolved later by the Go compiler.
Generics stay in Go. C stays in C. Convert at the door.
Interfaces and generics
Go forbids generic methods on interfaces. You can define a generic function, and you can define a generic struct with methods, but you cannot declare a method inside an interface that has type parameters.
This limitation preserves the simplicity of Go's interface model. Interfaces in Go are about behavior, not type parameters. An interface says "any type that implements these methods satisfies me." Adding generics to interfaces would require the type system to track type parameters through interface satisfaction, which complicates the rules for when a type implements an interface. The language designers prefer to keep interfaces as plain contracts of methods.
Here's an attempt to define a generic interface method.
// Package main shows interface limitations.
package main
// Processor defines a behavior for processing data.
type Processor interface {
// Process handles the data.
Process(data string) error
}
// GenericProcessor tries to add a generic method.
// This is invalid Go syntax.
// type GenericProcessor interface {
// Process[T any](data T) error // Error: generic methods not supported
// }
The compiler rejects the interface definition with generic methods not supported. If you need generic behavior, you define a generic function or a generic struct that implements a non-generic interface.
The community convention "accept interfaces, return structs" applies here. You define an interface for the behavior you need. You return a concrete struct that implements it. If you need generics, you put the type parameter on the struct or the function, not on the interface.
Interfaces define behavior. Generics define types. Don't mix them.
Constraints and type parameters
Type parameters must be constrained by interfaces or concrete types. You cannot constrain a type parameter to arbitrary properties like "has a field X" or "supports the + operator" without using an interface.
This forces explicit design. If you want a type parameter to support addition, you must define an interface with an Add method. You cannot just say T must be numeric. Go does not have a built-in "numeric" constraint that covers int, float64, and custom types automatically. You have to write the interface yourself.
Here's a generic sum function that requires an interface constraint.
// Package main shows constraint requirements.
package main
import "fmt"
// Adder defines a type that can be added.
type Adder interface {
// Add returns the sum with another Adder.
Add(other Adder) Adder
}
// Sum adds two values of the same type.
func Sum[T Adder](a, b T) T {
return a.Add(b)
}
// IntVal wraps an int to satisfy Adder.
type IntVal int
// Add implements Adder.
func (i IntVal) Add(other Adder) Adder {
// Cast other to IntVal to perform addition.
o := other.(IntVal)
return IntVal(i + o)
}
func main() {
// Sum works with IntVal.
result := Sum(IntVal(10), IntVal(20))
fmt.Println(result)
}
The receiver name i follows the convention of using one or two letters matching the type. IntVal starts with I, so i is appropriate. The function Add takes other Adder, accepting the interface. It returns Adder, but the implementation returns IntVal, which satisfies the interface. This aligns with "accept interfaces, return structs."
If you try to use the + operator on a type parameter without a constraint, the compiler rejects it.
// Package main shows operator limitations.
package main
// BadSum tries to use + on a generic type.
// func BadSum[T any](a, b T) T {
// // Error: operator + not defined on T
// return a + b
// }
The error operator + not defined on T tells you that the compiler doesn't know how to add T. You must provide the logic via an interface method or restrict T to specific concrete types that support the operation.
Don't fight the type system. Wrap the value or change the design.
Workarounds
The limitations are real, but the workarounds are straightforward. You keep generics on the Go side and handle the boundaries explicitly.
For cgo, write a non-generic wrapper that converts Go types to C types. Use your generic code with the Go types, then convert at the boundary.
// Package main shows a cgo workaround.
package main
import (
"fmt"
)
// #include <stdio.h>
// void print_int(int x) { printf("%d\n", x); }
import "C"
// IntWrapper converts a Go int to a C int.
func IntWrapper(val int) C.int {
// Convert Go int to C int for the boundary.
return C.int(val)
}
// GenericCache works with Go types only.
func GenericCache[T any](val T) T {
return val
}
func main() {
// Cache the Go value first.
cached := GenericCache(42)
// Convert to C type and call C.
C.print_int(IntWrapper(cached))
}
The generic function GenericCache works with int. You cache the Go value, then convert it to C.int using IntWrapper before calling the C function. This keeps the generic logic pure and isolates the C interaction.
For interfaces, use a generic struct that holds an interface, or pass a function parameter. If you need a generic method, define a generic struct and add methods to it.
// Package main shows an interface workaround.
package main
import "fmt"
// Processor defines a behavior for processing data.
type Processor interface {
// Process handles the data.
Process(data string) error
}
// GenericProcessor holds a processor and adds generic behavior.
type GenericProcessor[P Processor] struct {
// proc holds the underlying processor.
proc P
}
// NewGenericProcessor creates a new GenericProcessor.
func NewGenericProcessor[P Processor](p P) GenericProcessor[P] {
return GenericProcessor[P]{proc: p}
}
// RunGeneric processes data with type information.
func (gp GenericProcessor[P]) RunGeneric(data string) error {
// Delegate to the processor.
return gp.proc.Process(data)
}
// StringProcessor implements Processor.
type StringProcessor struct{}
// Process implements Processor.
func (p StringProcessor) Process(data string) error {
fmt.Println("Processing:", data)
return nil
}
func main() {
// Create a generic processor with a concrete implementation.
gp := NewGenericProcessor(StringProcessor{})
_ = gp.RunGeneric("test")
}
GenericProcessor is a generic struct. It holds a type parameter P constrained to Processor. It adds a method RunGeneric that uses the type parameter. This achieves the goal of generic behavior without putting generics on the interface.
Workarounds exist. Wrap the boundary, keep the core generic.
Pitfalls and errors
Watch for these common mistakes when working with generics near the limits.
If you forget to constrain a type parameter and try to use a method, the compiler rejects the code with T has no field or method X. You must add an interface constraint.
If you try to use a generic type as a map key with a C type, you get invalid map key type T. Map keys must be comparable, and C types are not comparable in Go.
If you try to define a method on a type parameter directly, you get type parameter T cannot be used as a method receiver. Methods must be defined on named types, not on type parameters.
If you try to use any as a constraint and then use type-specific operations, you get errors like operator + not defined on T. any is equivalent to interface{}. It gives you no guarantees about the underlying type.
The compiler errors are plain text. They tell you exactly what went wrong. Read them carefully. They point to the missing constraint or the invalid usage.
Trust the compiler. It catches these issues before runtime.
Decision matrix
Use generics when you need type-safe collections or algorithms that work across multiple Go types without code duplication.
Use any with type assertions when the set of types is small and known, or when you need to store heterogeneous data in a single structure.
Use a thin wrapper struct when you need to pass data across the cgo boundary, converting Go types to C types explicitly.
Use interface composition when you need to define behavior without generics, relying on "accept interfaces, return structs" to keep dependencies loose.
Use reflection when you must inspect types dynamically at runtime, accepting the performance cost and loss of compile-time safety.
Generics are a tool for Go code. Use them where they fit. Wrap the rest.