Generics: From interface{} to Type Parameters
Generics: From interface{} to Type Parameters
On March 15, 2022, Go 1.18 was officially released, bringing the largest syntax change since Go's inception — Type Parameters, commonly known as generics. This feature had been discussed since Go was open-sourced in 2009, went through 12 years of design iteration, multiple rejected proposals, and was finally accepted based on the design proposed by Ian Lance Taylor and Robert Griesemer in 2019.
Why did a seemingly "fundamental" feature require such a long wait? Because the Go team's design principle for generics was: it must not compromise Go's simplicity. They would rather have no generics than introduce a generic system that complicates the language. The final design indeed increased expressiveness while maintaining Go's simplicity — at the cost of being more restrictive than Rust's or C++'s generics.
This chapter starts from basic generic syntax, dives into compiler implementation strategies, reviews the historical evolution of proposals, and concludes with correct usage scenarios and known limitations.
Level 1: What You Need to Know
Go Before Generics
Before Go 1.18, handling "multi-type generic logic" primarily used three approaches:
Approach 1: Code Duplication
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func MaxFloat64(a, b float64) float64 {
if a > b {
return a
}
return b
}
func MaxString(a, b string) string {
if a > b {
return a
}
return b
}
One function per type, identical logic. Maintenance cost grows linearly with the number of types.
Approach 2: interface{} + Type Assertions
func Max(a, b interface{}) interface{} {
switch a := a.(type) {
case int:
if a > b.(int) {
return a
}
return b
case float64:
if a > b.(float64) {
return a
}
return b
default:
panic("unsupported type")
}
}
Problems:
- Type safety lost: The compiler cannot check type consistency;
Max(1, "hello")won't error at compile time - Performance overhead: interface{} boxing/unboxing involves heap allocation
- Verbose code: Each type needs a case
Approach 3: Code Generation
Using go generate and template tools (like genny, gen) to auto-generate type-specific code. But this increases build complexity, and generated code is hard to debug.
Generic Functions
With Go 1.18's type parameters, the Max function needs to be written only once:
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Usage
result := Max[int](3, 5) // explicitly specify type argument
result2 := Max(3.14, 2.71) // compiler infers float64
result3 := Max("hello", "world") // compiler infers string
Syntax breakdown:
[T constraints.Ordered]: Type parameter list declaring T constrained toconstraints.Orderedconstraints.Ordered: Standard library constraint representing types supporting<><=>=Tcan be used like a regular type in the function body
Multiple type parameters:
func Map[T any, R any](slice []T, fn func(T) R) []R {
result := make([]R, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Usage
strings := Map([]int{1, 2, 3}, func(n int) string {
return fmt.Sprintf("%d", n)
})
// strings = ["1", "2", "3"]
Generic Types
Not just functions — type definitions can also have type parameters:
// Generic stack
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
// Usage
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, ok := intStack.Pop() // val = 2, ok = true
strStack := &Stack[string]{}
strStack.Push("hello")
Generic Set type:
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{m: make(map[T]struct{})}
}
func (s *Set[T]) Add(item T) {
s.m[item] = struct{}{}
}
func (s *Set[T]) Contains(item T) bool {
_, ok := s.m[item]
return ok
}
func (s *Set[T]) Remove(item T) {
delete(s.m, item)
}
Type Constraints
Type constraints determine what operations a type parameter can perform. Constraints are essentially interfaces:
The any Constraint
any is an alias for interface{} (introduced in Go 1.18), meaning "no constraint":
func Print[T any](val T) {
fmt.Println(val)
}
The comparable Constraint
A built-in constraint indicating the type supports == and !=:
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
comparable includes all basic types (int, string, bool, etc.), pointers, channels, arrays (with comparable elements), and structs (with all comparable fields), but excludes slices, maps, and functions.
Custom Constraints
Define constraints using interfaces, which can contain method sets and type sets:
// Method constraint
type Stringer interface {
String() string
}
func PrintAll[T Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}
// Type constraint (using ~ for underlying type)
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
func Sum[T Number](numbers []T) T {
var total T
for _, n := range numbers {
total += n
}
return total
}
// Meaning of ~: underlying type matching
type Celsius float64 // underlying type is float64
type Fahrenheit float64
// Sum([]Celsius{20.0, 30.0}) works because ~float64 includes Celsius
The constraints Package (Experimental)
golang.org/x/exp/constraints provides common constraints:
import "golang.org/x/exp/constraints"
// constraints.Ordered: types supporting < > <= >=
// constraints.Integer: all integer types
// constraints.Float: all floating-point types
// constraints.Complex: all complex types
// constraints.Signed: all signed integers
// constraints.Unsigned: all unsigned integers
Note: Go 1.21 added min and max as built-in functions, partially reducing dependence on constraints.Ordered.
Common Mistakes and Fixes
Mistake 1: Adding new type parameters on methods
type Container[T any] struct {
items []T
}
// Error: methods cannot introduce new type parameters
func (c *Container[T]) Convert[R any](fn func(T) R) []R { // compile error!
// ...
}
// Correct: use a top-level function
func Convert[T any, R any](c *Container[T], fn func(T) R) []R {
result := make([]R, len(c.items))
for i, item := range c.items {
result[i] = fn(item)
}
return result
}
Go methods cannot introduce type parameters beyond those of the receiver type — this is a design limitation, not a bug.
Mistake 2: Insufficient constraints causing compilation failure
// Error: any doesn't support the + operation
func Add[T any](a, b T) T {
return a + b // compile error: operator + not defined on T
}
// Correct: use appropriate constraint
type Addable interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~complex64 | ~complex128 |
~string
}
func Add[T Addable](a, b T) T {
return a + b
}
Mistake 3: Forgetting the ~ prefix
type MyInt int
// Error: only matches int itself, not MyInt
type IntOnly interface {
int
}
// Correct: matches all types with int as underlying type
type IntLike interface {
~int
}
Mistake 4: Attempting type assertion on generic parameter
// Error: cannot type-assert on type parameter
func Process[T any](val T) {
if s, ok := val.(string); ok { // compile error!
fmt.Println(s)
}
}
// Correct: convert to interface{} first
func Process[T any](val T) {
if s, ok := any(val).(string); ok {
fmt.Println(s)
}
}
Level 2: How It Works Under the Hood
Compilation Implementation of Generics
The Go compiler needs to transform generic code into executable machine code. There are two main strategies:
Strategy 1: Monomorphization
Generate a separate copy of function code for each concrete type. C++ templates and Rust generics use this strategy.
Max[int] → MaxInt(a, b int) int
Max[float64] → MaxFloat64(a, b float64) float64
Max[string] → MaxString(a, b string) string
Pros: Generated code is optimized for concrete types, optimal performance. Cons: Code bloat — N type instantiations mean N copies of code.
Strategy 2: Type Erasure + Dictionary Passing
Generate only one copy of generic code, look up type information at runtime through a "dictionary." Java generics use type erasure.
Pros: Small code size. Cons: Performance overhead from indirect calls, prevents inlining optimization.
Go's Choice: GC Shape Stenciling
Go 1.18 adopted a hybrid strategy called GC Shape Stenciling (described by Keith Randall in a 2021 design document). Core idea:
- Group by GC Shape: Types with the same memory layout (size and pointer bitmap) share one code copy
- Dictionary passing: Runtime obtains type-specific information through a dictionary parameter
What is a GC Shape? It's the type's "shape" from the garbage collector's perspective — including size, alignment, and which offset positions contain pointers. For example:
int,int64,uint64,*stringon 64-bit systems are all 8 bytes with or without pointers — may be different shapes- All pointer types (
*int,*string,*MyStruct) share the same GC Shape (a pointer-sized value containing one pointer)
Actual behavior:
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Compiler might generate:
// Max_shape_int(a, b int, dict *typeDict) int — for int, int64, etc.
// Max_shape_ptr(a, b unsafe.Pointer, dict *typeDict) unsafe.Pointer — for pointer types
// Max_shape_string(a, b string, dict *typeDict) string — string has unique GC shape
Information stored in the dictionary:
- Concrete size and alignment of the type
- Method table (when constraint includes methods)
- Instantiation info (for reflection)
Performance Impact
GC Shape Stenciling performance characteristics (based on Go 1.18-1.22 benchmarks):
// Benchmark: generic vs concrete type vs interface{}
func BenchmarkMaxGeneric(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Max(i, i+1)
}
}
func BenchmarkMaxConcrete(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = MaxInt(i, i+1)
}
}
func BenchmarkMaxInterface(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = MaxIface(i, i+1)
}
}
Typical results (Go 1.22, amd64):
| Method | ns/op | Allocations |
|---|---|---|
| Concrete type | ~1.5 | 0 |
| Generic | ~2.0 | 0 |
| interface{} | ~5.0 | 1 alloc |
The generic version's extra overhead comes from dictionary lookup and indirect calls — typically only 20-50% overhead, far less than interface{}'s boxing overhead. But in extremely hot paths, this difference can matter.
Compiler Optimization Evolution
Go 1.18's initial implementation was conservative; subsequent versions continue to optimize:
- Go 1.19: Improved inlining decisions for generic functions
- Go 1.20: Reduced dictionary passing overhead
- Go 1.21: Full monomorphization for single-GC-Shape generic instances
- Go 1.22+: Ongoing improvements targeting generic code performance approaching hand-written concrete type code
How Type Inference Works
Go's type inference allows omitting explicit type arguments in many cases:
// No need to write Max[int](3, 5)
result := Max(3, 5) // compiler infers T = int
The inference algorithm is based on Unification — implemented by Robert Griesemer in Go 1.18. The process:
- Collect types of all function arguments
- Unify actual types with formal parameter type expressions
- Solve for concrete types of type parameters
func Map[T any, R any](s []T, fn func(T) R) []R
// Call: Map([]int{1,2,3}, strconv.Itoa)
// Inference process:
// s has type []int → T = int
// fn has type func(int) string → R = string
// Result: Map[int, string]
Inference isn't omnipotent — some cases require explicit specification:
// When type parameter doesn't appear in function arguments, cannot infer
func New[T any]() *T {
return new(T)
}
p := New[int]() // must specify explicitly
Internal Representation of Constraints
Type constraints are internally represented as Type Sets. Go 1.18 redefined interface semantics:
- Traditional interfaces define method sets
- Interfaces with type elements define type sets
// This constraint's type set = {all types with underlying type int or string}
type IntOrString interface {
~int | ~string
}
// Constraint's type set = method set ∩ type set
type OrderedStringer interface {
~int | ~string
String() string
}
// Type set = {underlying type int or string} ∩ {types implementing String()}
// = {types with underlying type int or string that implement String()}
This means:
type MyInt int
func (m MyInt) String() string { return fmt.Sprintf("%d", m) }
// MyInt satisfies OrderedStringer:
// 1. Underlying type is int (satisfies ~int)
// 2. Implements String() string (satisfies method constraint)
Generics and Reflection
Generic types still have complete type information at runtime:
func TypeName[T any]() string {
var zero T
return reflect.TypeOf(&zero).Elem().String()
}
fmt.Println(TypeName[int]()) // "int"
fmt.Println(TypeName[string]()) // "string"
Note that the reflection type of an instantiated generic type differs from the "generic type with type parameters" concept — the reflect package sees the instantiated concrete type, not a parameterized generic type.
Level 3: What the Specification Says
History of Go Generics Proposals (12 Years of Evolution)
Discussion about generics in Go started when Go was open-sourced in 2009. Here are the key milestones:
2010: Ian Lance Taylor's First Proposal
Ian Lance Taylor submitted the first generics proposal less than a year after Go was open-sourced. It used C++-template-like syntax:
// 2010 proposal syntax (never implemented)
gen [T] func Max(a, b T) T {
if a > b {
return a
}
return b
}
Rejected because: inconsistent with Go's existing syntax style, unclear compilation strategy.
2013: Type Functions Proposal
Proposed "type functions" as constraints:
// 2013 proposal (never implemented)
type Addable(T) interface {
+(T, T) T
}
Rejected because: too complex, introduced operators as interface methods.
2016: GopherCon Generics Survey
The Go team released a user survey at GopherCon 2016. "Generics" ranked as the #1 most-wanted feature for multiple consecutive years. But Rob Pike emphasized:
"The generic dilemma is this: do you want slow programmers, slow compilers and bloated binaries, or slow execution times?"
2018: Draft Design with Contracts
Ian Lance Taylor proposed the "Contracts" design — using separate contract declarations to define constraints:
// 2018 Contracts proposal (never implemented)
contract Ordered(T) {
T int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64,
float32, float64, string
}
func Max(type T Ordered)(a, b T) T {
if a > b {
return a
}
return b
}
Rejected because: Contracts added an entirely new language concept with heavy learning burden; type keyword in parameter position looked strange.
June 2019: Ian Lance Taylor's Final Proposal
Ian Lance Taylor and Robert Griesemer proposed a simplified design — using interfaces as constraints — which became the accepted proposal. Core changes:
- No new keywords (
contract→ existinginterface) [T Constraint]instead of(type T Constraint)- Interfaces can contain type lists (later evolved into
~Tand|syntax)
// 2019 proposal evolved into final syntax
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
March 2022: Go 1.18 Official Release
After 3 years of prototype implementation, community feedback, and iteration, Go 1.18 officially shipped generics.
Comparison with Other Languages' Generic Systems
C++ Templates (1990s)
template <typename T>
T Max(T a, T b) {
return (a > b) ? a : b;
}
C++ templates are a purely compile-time text substitution mechanism ("duck typing at compile time") — no constraint declarations needed; if an operation isn't supported during template instantiation, it errors.
Differences from Go:
- C++ has no explicit constraints (C++20 introduced Concepts to improve this)
- C++ templates can do metaprogramming (Turing-complete), Go generics cannot
- C++ compile error messages are extremely verbose, Go's constraints make errors clearer
Java Generics (2004, Java 5)
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
Java uses type erasure — generic information is erased after compilation, only Object remains at runtime:
// After compilation, equivalent to
public Comparable max(Comparable a, Comparable b) {
return a.compareTo(b) > 0 ? a : b;
}
Differences from Go:
- Java has complete type erasure, cannot get type parameter at runtime (
new T()is illegal) - Java supports wildcards (
? extends T,? super T), Go does not - Java supports new type parameters on generic methods, Go does not
Rust Generics (2015, Rust 1.0)
fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
Rust uses full monomorphization — one code copy per concrete type:
Differences from Go:
- Rust fully monomorphizes, Go uses GC Shape Stenciling (hybrid strategy)
- Rust traits support Associated Types, Go constraints do not
- Rust supports specialization via impl blocks, Go does not
- Rust compile times are longer due to monomorphization explosion, Go compilation stays fast
Key Definitions in Go Generics Specification
The Go Language Specification (Go 1.18+) core definitions for generics:
Type Parameter Lists:
A type parameter list declares type parameters for a generic function or type definition. The type parameter list looks like an ordinary function parameter list except that the type parameter names must all be present and the list is enclosed in square brackets rather than parentheses.
Type Constraints:
A type constraint is an interface that defines the set of permissible type arguments for the respective type parameter and controls the operations supported by values of that type parameter.
Type Sets:
Every type has a type set. The type set of a non-interface type T consists of just T itself. The type set of an interface type is defined by the interface's elements.
Instantiation:
A generic function or type is instantiated by substituting type arguments for the type parameters. Instantiation proceeds in two steps: (1) each type argument is substituted for its corresponding type parameter; (2) each type argument is verified to satisfy its constraint.
Design Decision Background
Go generics design has several key "non-goals" that help explain its limitations:
- No template metaprogramming: Go doesn't want to repeat C++'s mistake — template metaprogramming makes code unreadable
- No specialization: Cannot provide special implementations of generic functions for specific types
- No variance: No Java-like
? extends T/? super T - No higher-kinded types: Cannot pass type constructors as parameters
- No method type parameters: Methods cannot declare new type parameters
Rob Pike explained the reasoning in an interview:
"Complexity is multiplicative. Every feature you add interacts with every other feature. By keeping generics simple, we keep the total complexity of the language manageable."
Level 4: Edge Cases and Pitfalls
When to Use Generics
Generics are most suitable for these scenarios:
Scenario 1: Generic Data Structures
// Generic linked list
type Node[T any] struct {
Value T
Next *Node[T]
}
type LinkedList[T any] struct {
Head *Node[T]
Len int
}
func (l *LinkedList[T]) Prepend(val T) {
l.Head = &Node[T]{Value: val, Next: l.Head}
l.Len++
}
Scenario 2: Generic Algorithms
// Generic filter
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// Generic reduce
func Reduce[T any, R any](slice []T, initial R, fn func(R, T) R) R {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
Scenario 3: Type-Safe Containers/Pools
// Generic sync.Pool
type Pool[T any] struct {
pool sync.Pool
}
func NewPool[T any](newFunc func() T) *Pool[T] {
return &Pool[T]{
pool: sync.Pool{
New: func() interface{} {
return newFunc()
},
},
}
}
func (p *Pool[T]) Get() T {
return p.pool.Get().(T)
}
func (p *Pool[T]) Put(val T) {
p.pool.Put(val)
}
When NOT to Use Generics
Unsuitable Scenario 1: Only one type will ever be used
// Unnecessary generic — only string will use this
func ToUpper[T ~string](s T) T {
return T(strings.ToUpper(string(s)))
}
// Just write it directly
func ToUpper(s string) string {
return strings.ToUpper(s)
}
Unsuitable Scenario 2: Behavior differs drastically by type
// Wrong: completely different logic per type
func Process[T int | string | []byte](val T) {
switch v := any(val).(type) {
case int:
// completely different logic
case string:
// completely different logic
case []byte:
// completely different logic
}
}
// Right: write three separate functions
func ProcessInt(val int) { ... }
func ProcessString(val string) { ... }
func ProcessBytes(val []byte) { ... }
If you're writing a type switch inside a generic function, you're using generics wrong.
Unsuitable Scenario 3: Need runtime dispatch
// Interfaces are better: need runtime polymorphism
type Handler interface {
Handle(ctx context.Context) error
}
// No generics needed — different Handlers' behavior is decided at runtime
func RunHandlers(handlers []Handler) {
for _, h := range handlers {
h.Handle(context.Background())
}
}
Known Limitations of Generics
Limitation 1: No Method Type Parameters
type Container[T any] struct{ items []T }
// Won't compile: methods cannot have their own type parameters
func (c *Container[T]) Map[R any](fn func(T) R) *Container[R] {
// ...
}
Workaround: Use top-level functions:
func MapContainer[T any, R any](c *Container[T], fn func(T) R) *Container[R] {
result := &Container[R]{items: make([]R, len(c.items))}
for i, item := range c.items {
result.items[i] = fn(item)
}
return result
}
Limitation 2: No Specialization
// Cannot do: provide a more efficient implementation for specific types
func Sort[T constraints.Ordered](s []T) {
// Want: if T is int, use radix sort
// Reality: can only use generic comparison sort
slices.Sort(s)
}
Limitation 3: Cannot Use Generic Parameter as type switch case
func Check[T any](val any) {
switch val.(type) {
case T: // compile error: cannot use type parameter in type switch
fmt.Println("match")
}
}
Limitation 4: Constraint Interfaces Cannot Be Used as Regular Interface Types
type Number interface {
~int | ~float64
}
// Error: interfaces with type elements cannot be used as variable types
var n Number // compile error
// Can only be used as constraints
func Add[T Number](a, b T) T { return a + b }
Interfaces with type elements (~int | ~float64) can only be used as generic constraints, not as regular interface types. This is because such interfaces' type sets cannot be effectively represented at runtime.
Limitation 5: Generic Type Aliases (before Go 1.23)
type Pair[T any] struct{ First, Second T }
// Not legal before Go 1.23
type IntPair = Pair[int] // Go 1.24+ supports generic type aliases
Interview Questions
Question 1: Does the following code compile?
func Swap[T any](a, b *T) {
*a, *b = *b, *a
}
Answer: Yes, it compiles. The any constraint allows all types, and assignment is legal for all types.
Question 2: Why can't you write func (s Stack[T]) Pop[R any]() R?
Answer: The Go specification explicitly forbids methods from introducing new type parameters. Two reasons:
- Interface compatibility: If methods have extra type parameters, interface type set definitions become extremely complex
- Compilation complexity: Requires vtable support for parameterized method dispatch, conflicting with Go's simple runtime model
Question 3: What's the trap with the comparable constraint and ==?
func Equal[T comparable](a, b T) bool {
return a == b
}
// This compiles but might panic at runtime:
type I interface{ comparable }
var a I = []int{1, 2, 3} // can't — interface constraints are compile-time
Since Go 1.20, the comparable constraint's behavior is stricter: only types that provably support == at compile time satisfy it. Types containing potentially-panicking == (like interfaces containing slices) don't satisfy comparable.
Question 4: When does generic function type inference fail?
// 1. Type parameter only appears in return value:
func Zero[T any]() T { var zero T; return zero }
// Must write Zero[int](), cannot infer
// 2. Type parameter in nested position:
func MakeSlice[T any](fn func() T) []T { return []T{fn()} }
// MakeSlice(func() int { return 1 }) — can infer
// 3. Cannot infer from nil:
func First[T any](s []T) T { return s[0] }
// First(nil) — cannot infer, nil has no type information
Real-World Case: Standard Library Genericization
Go 1.21 introduced the slices and maps packages — the standard library's first large-scale use of generics:
// slices package
func Sort[S ~[]E, E cmp.Ordered](x S)
func Contains[S ~[]E, E comparable](s S, v E) bool
func Index[S ~[]E, E comparable](s S, v E) int
func Compact[S ~[]E, E comparable](s S) S
// maps package
func Keys[M ~map[K]V, K comparable, V any](m M) []K
func Values[M ~map[K]V, K comparable, V any](m M) []V
func Clone[M ~map[K]V, K comparable, V any](m M) M
These packages demonstrate Go generics best practices:
- Constraints as precise as possible (
cmp.Orderedrather thanany) - Using
~to allow custom types (~[]Erather than[]E) - Clear function signatures, immediately understandable
Performance Pitfall: Interface Constraints vs Type Constraints
// Approach A: Interface constraint (dispatch through methods)
type Hasher interface {
Hash() uint64
}
func HashAll[T Hasher](items []T) []uint64 {
result := make([]uint64, len(items))
for i, item := range items {
result[i] = item.Hash() // may involve indirect call through itab
}
return result
}
// Approach B: Type constraint (inline expansion)
type IntOrString interface {
~int | ~string
}
func Size[T IntOrString](val T) int {
// Compiler can directly generate code for int/string
return int(unsafe.Sizeof(val))
}
Method calls in Approach A may not be inlined (depending on compiler optimization), while type constraints in Approach B give the compiler more optimization room.
Best Practices Summary
- Use interfaces first, generics when interfaces aren't enough — interfaces are Go's core abstraction mechanism; generics supplement them
- Be precise with constraints — don't use
anyfor all parameters; give the compiler enough information - Methods cannot have new type parameters — hard limitation; work around with top-level functions
- Beware comparable traps —
==on interface types might panic - Watch for dictionary overhead in performance-critical paths — in extreme cases, consider hand-written concrete type versions
- Don't over-genericize — if only 2-3 types will use it, writing concrete functions may be clearer