Go Generics vs interface{}: A Decision Guide for Library Authors
Should a new Go library use generics, interface{}, or reflection? Compare type safety, allocations, and version constraints, with a concrete generic example and benchmark validation.
11 Sept 2026, 07:21 UTC

The decision: generics, interface{}, or reflection?
You are starting a new Go library module and need to handle values of varying types. The practical takeaway: if your module targets Go 1.18 or later and you care about type safety and allocation-free hot paths, use generics. If you must support Go 1.17 or earlier, generics are unavailable and you need interface{} with type assertions or code generation.
Constraints to check first
- Minimum Go version. Generics shipped in Go 1.18. The
godirective in go.mod and your users' toolchains set the floor. Code using type parameters fails to compile on older releases. - Team familiarity. Complex constraint expressions can produce dense compiler errors that are harder for new contributors to read.
- Build cost. Generic instantiation can increase binary size and compile time. Measure before committing to a heavily generic API.
Comparing the supported options
| Option | Type safety | Runtime cost | Readability | Go version |
|---|---|---|---|---|
| Generics (type parameters) | Compile-time checked | Avoids boxing for concrete types; no assertions | Intent expressed in the signature | 1.18+ |
| interface{} (any) + type assertion | Runtime only; bad assertions panic | Boxing of values into interfaces; assertion checks | Callers must document expected types | All |
| reflect package | Runtime only | Highest overhead; hard to inline | Verbose and indirect | All |
Generics move mistakes from runtime panics to compile errors and, for concrete types, avoid the boxing that interface{} forces. Reflection remains appropriate only when the shape of the data is genuinely unknown at compile time, such as marshalers.
Concrete implementation: a generic Min
The following is a minimal generic function using the built-in constraints-style comparison via cmp.Ordered (Go 1.21+) or a hand-rolled constraint for 1.18–1.20:
package sliceutil
// Min returns the smallest element of s.
// Requires Go 1.18+ (type parameters).
func Min[T cmp.Ordered](s []T) (T, bool) {
var zero T
if len(s) == 0 {
return zero, false
}
m := s[0]
for _, v := range s[1:] {
if v < m {
m = v
}
}
return m, true
}
Run go build ./... from the module root (no special permissions needed). On Go 1.18+ it compiles; on 1.17 or earlier the toolchain rejects type-parameter syntax, which is itself a clear signal of the version floor.
Validating the performance claim
- Write a benchmark in the same package comparing the generic
Minagainst an interface{} version that takes[]interface{}and asserts each element. - Run
go test -bench=. -benchmemfrom the module directory. Check theB/opandallocs/opcolumns: the generic version should show zero allocations for a slice of ints, while the interface{} version allocates to box each element. - Confirm the toolchain with
go versionbefore drawing conclusions; results vary by release and architecture.
Limitations and fallback strategy
Generics do not cover every case. Methods cannot introduce their own type parameters (only the receiver's declared ones), and generic code can enlarge binaries when instantiated for many types. If you must ship to Go 1.17 users, keep the generic implementation behind a //go:build go1.18 file and provide an interface{}-based fallback in a //go:build !go1.18 file, or generate per-type code with go generate. Verify the split by building with both toolchains in CI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.