Go 1.18 was still months away when the design was finalised, but the proposal was public and we were already prototyping. After building several services at the European fintech firm with the experimental toolchain, the pattern of when generics help versus when they don’t was becoming clear.
The answer is not “always use generics” or “avoid them.” It’s more specific than that.
Cases Where Generics Pay Off
1. Type-Safe Result/Option Types
Before generics, every function that could fail returned (T, error) and callers checked for nil. For functions that return “value or nothing” without an error, the pattern was ugly:
| |
With generics, a proper Optional type:
| |
No pointers for non-pointer types, no nil dereferences, explicit “absent” semantics.
2. Generic Map/Filter/Reduce
The most common complaint about Go before generics: writing the same filter function for every type.
| |
This is genuinely cleaner than either interface{} versions or code generation. The type inference means you usually don’t need to write the type parameters explicitly.
3. Typed Event Bus
Before generics:
| |
With generics:
| |
The subscriber callback is typed — no runtime assertion, compiler catches mismatches.
4. Concurrent-Safe Generic Cache
| |
Write once, use for Cache[string, Price], Cache[int, User], etc. Previously you’d either use map[string]interface{} (no type safety) or write a separate cache implementation per type.
Cases Where Generics Add Complexity Without Benefit
Forcing generics on simple functions
| |
If a function works with exactly one type in practice, don’t make it generic. The constraint adds noise without enabling anything.
Generic wrappers around interfaces
| |
If you have a single concrete implementation, a plain interface is clearer. Generics for interfaces are only valuable when you have multiple implementations and the generic constraint captures something useful.
Premature abstraction
Generics make it easy to build abstract utility types. The risk: building a generic Pipeline[T] before you have more than one type flowing through a pipeline, or a generic Result[T, E] before you have multiple error types.
Build the concrete version first. Extract the generic version when you have two concrete implementations and can see the pattern clearly. Don’t start with the generic.
The Practical Guidance
Use generics when:
- You have the same logic implemented multiple times for different types
- You’re using
interface{}with type assertions where the type is known at the call site - The function or type genuinely works for all types (or a constrained subset)
Don’t use generics when:
- There’s one concrete type and no clear future need for others
- The constraint logic is more complex than the code it saves
- The team isn’t familiar with the generic type parameter syntax (readability matters)
Go’s generics are conservative by design — they solve the concrete annoyances (typed containers, utility functions) without trying to express complex type-theoretical relationships. That’s the right trade-off for the language’s goals.