Eliminate Boilerplate with Go Generics: A Practical Guide
Learn how Go generics cut boilerplate while keeping type safety and performance. A step‑by‑step guide with a concrete Max example, benchmarks, and practical trade‑off considerations.
27 Oct 2025, 05:41 UTC

Problem: Repetitive Code for Simple Operations
In many Go codebases you’ll find several hand‑written functions that perform the same logic on different concrete types. For example, a Max function that returns the larger of two values may exist for int, float64, and string separately. Each copy adds maintenance overhead and increases the chance of subtle bugs.
Why keep writing identical code? Because before Go 1.18 the language didn’t support a generic way to express “this function works for any ordered type.” The result was duplicated boilerplate.
Thesis: Generics Keep Type Safety While Cutting Boilerplate
Go 1.18 introduced type parameters and constraints that let you write a single, type‑safe implementation that the compiler specializes for each concrete type you use. The generated code is as efficient as hand‑written variants, and the compiler guarantees that only compatible types can be passed.
Section 1 – Writing a Generic Max Function
Below is a minimal, self‑contained example that demonstrates the syntax, constraints, and usage of a generic Max function. The code can be placed in a file named max.go.
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
// Max returns the larger of a or b. The type T must be ordered.
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 7)) // int
fmt.Println(Max(3.14, 2.71)) // float64
fmt.Println(Max("apple", "banana")) // string
}
Run the program with:
# Ensure you’re on Go 1.18 or newer
$ go version
# Compile and execute
$ go run max.go
Expected output (actual values may vary if you change the arguments):
7
3.14
banana
Key points:
constraints.Orderedis a pre‑defined interface that includes all built‑in ordered types:int,float64,string, etc.- The compiler generates a separate
Max_int,Max_float64, andMax_stringfunction during build time. - Only types that satisfy the constraint can be substituted; attempting
Max([]int{1,2}, []int{3,4})will fail at compile time.
Section 2 – Performance & Binary Size
Because the compiler specializes code for each concrete type, the runtime cost is identical to manually written variants. A quick benchmark illustrates this. Create a file max_test.go:
package main
import "testing"
func BenchmarkGenericMax(b *testing.B) {
for i := 0; i < b.N; i++ {
Max(1000, 2000)
}
}
func BenchmarkIntMax(b *testing.B) {
for i := 0; i < b.N; i++ {
if 1000 > 2000 {
_ = 1000
} else {
_ = 2000
}
}
}
Run the benchmarks:
$ go test -bench .
Typical output shows no significant performance difference. The compiler inlines the comparison and produces identical machine code.
Binary size can grow if you instantiate a generic with many distinct types, because each instantiation is a separate function in the binary. For most applications the impact is negligible, but if you’re targeting very small binaries (e.g., embedded systems) consider limiting the number of instantiations.
Section 3 – Trade‑offs & Practical Tips
| Aspect | Benefit | Consideration |
|---|---|---|
| Readability | Single definition replaces many copies. | New syntax ([T constraints.Ordered]) may be unfamiliar to legacy Go developers. |
| Type Safety | Compile‑time enforcement of constraints. | Overly broad constraints (e.g., any) defeat the purpose and can hide bugs. |
| Binary Size | No duplication if few types used. | Large number of unique type arguments can inflate binary. |
| Interoperability | Works with existing code. | Third‑party libraries may not expose generic APIs; adapters or type assertions may be needed. |
When designing generic functions:
- Prefer the most specific constraint that satisfies your needs (e.g.,
constraints.Integerinstead ofconstraints.Orderedif only integers are required). - Avoid unconstrained type parameters unless the function truly accepts any type. Unconstrained parameters can accept unexpected types and lead to silent failures.
- Keep the number of distinct type arguments low to control binary size.
- Use
go test -run TestName -bench .to compare generic and non‑generic implementations in your own codebase.
Actionable Checklist
- Verify you’re on Go 1.18 or newer:
go version. - Identify repetitive functions that operate on ordered types.
- Replace them with a single generic implementation using
constraints.Orderedor a custom constraint. - Run
go vetandgolintto catch any misuse of type parameters. - Benchmark critical paths with
go test -bench .to confirm performance parity. - Monitor binary size; if it grows unexpectedly, refactor to reduce the number of unique type arguments.
- Document the generic function’s intent and constraints so future maintainers understand the allowed types.
By following this approach you’ll reduce boilerplate, maintain type safety, and keep the performance of your Go applications on par with hand‑written code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.