Sinds Go 1.18 zijn generics in Go officieel onderdeel van de taal. Veel developers vroegen er jaren om, anderen vonden ze juist in strijd met de simpelheid van Go. De waarheid ligt ergens in het midden: generics zijn krachtig, maar niet altijd de juiste oplossing.
In dit artikel leer je hoe type parameters werken, wanneer je ze écht nodig hebt, en wanneer je beter bij interfaces of concrete types kunt blijven. Je krijgt praktische voorbeelden die je direct kunt toepassen.
Wat zijn generics in Go precies
Generics laten je functies en types schrijven die met meerdere types werken, zonder dat je de logica voor elk type opnieuw hoeft te schrijven. Voorheen loste je dit op met interface{} (nu any) en type assertions, of met code generation.
Een simpel voorbeeld dat zonder generics niet netjes te schrijven is:
func Max[T int | float64 | string](a, b T) T {
if a > b {
return a
}
return b
}
Dit werkt voor int, float64 en string. De compiler genereert achter de schermen efficiënte code per type dat je gebruikt.
Als je nieuw bent met Go, lees dan eerst variabelen en types in Go en functions en error handling om de basis te begrijpen.
Type parameters en constraints
Een type parameter staat tussen vierkante haken direct na de functienaam. De constraint bepaalt welke types toegestaan zijn.
func Sum[T int | float64](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
Hier is int | float64 een type union die als constraint dient. Je kunt constraints ook als aparte interface definiëren voor herbruikbaarheid:
type Number interface {
int | int32 | int64 | float32 | float64
}
func Average[T Number](values []T) float64 {
var sum T
for _, v := range values {
sum += v
}
return float64(sum) / float64(len(values))
}
Built-in constraints
Go levert een paar standaard constraints:
any, hetzelfde alsinterface{}, accepteert elk typecomparable, types die je met==en!=kunt vergelijken (handig voor map keys)constraints.Ordered, uitgolang.org/x/exp/constraints, voor types die<en>ondersteunen
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
De officiële Go tutorial over generics geeft een uitgebreide introductie met stap-voor-stap uitleg.
Generieke types definiëren
Je kunt ook hele types generiek maken. Dit is vooral nuttig voor datastructuren zoals stacks, queues of sets.
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) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
Gebruik:
stack := &Stack[int]{}
stack.Push(1)
stack.Push(2)
value, ok := stack.Pop() // 2, true
Let op de var zero T truc: dit geeft je de zero value voor het concrete type. Zonder generics had je dit met interface{} en type assertions moeten doen, met alle risico's van dien.
Wanneer gebruik je generics wel
Generics zijn bijzonder waardevol in een paar specifieke scenario's.
1. Collection utilities
Functies zoals Map, Filter en Reduce zijn het klassieke voorbeeld:
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func Filter[T any](slice []T, pred func(T) bool) []T {
var result []T
for _, v := range slice {
if pred(v) {
result = append(result, v)
}
}
return result
}
2. Datastructuren
Stacks, queues, linked lists, trees en sets worden veel netter met generics. Je schrijft één implementatie en gebruikt 'm voor elk type.
3. Herbruikbare algoritmes
Sorteren, zoeken, minimum, maximum, alles wat puur over data werkt zonder domeinkennis.
4. Type-safe wrappers
Bijvoorbeeld een generieke cache, een result type, of een sync.Map alternatief met type safety:
type Cache[K comparable, V any] struct {
mu sync.RWMutex
data map[K]V
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
Dit sluit mooi aan op concurrency patterns die we eerder behandelden in goroutines en concurrency basics en channels diep uitgelegd.
Wanneer gebruik je generics juist niet
Net zo belangrijk: wéét wanneer je ze moet laten staan. De Go team richtlijn is helder: gebruik generics pas als je ze echt nodig hebt.
Business logic met één type
Als je functie alleen met User werkt, maak 'm dan niet generiek "voor het geval dat". YAGNI geldt ook hier.
// Prima zo. Geen generics nodig.
func ActivateUser(u *User) error {
u.Active = true
return userRepo.Save(u)
}
Als een interface voldoende is
Interfaces zijn al decennia lang het Go-antwoord op polymorfisme. Als je verschillend gedrag per type wilt (bijvoorbeeld Read, Write, Close), is een interface de juiste keuze.
type Notifier interface {
Notify(message string) error
}
Generics zijn voor hetzelfde gedrag over meerdere types, interfaces voor verschillend gedrag achter één contract. Lees meer over dat verschil in structs en interfaces.
Als het de code onleesbaar maakt
Vier type parameters met vijf constraints is vaak een teken dat je abstractie te ver gaat. Als niemand in je team de signature nog snapt, is simpelere code met een beetje duplicatie beter.
Performance overwegingen
Generics in Go gebruiken een techniek die GC shape stenciling heet. De compiler genereert niet voor elk type een aparte kopie (zoals C++ templates), maar deelt code tussen types met dezelfde memory layout.
In de praktijk:
- Voor primitieve types (int, float64) zijn generics vaak sneller dan interfaces, omdat er geen boxing is.
- Voor pointer types is het verschil klein, ze delen vaak dezelfde stencil.
- Voor hot paths kan benchmarken alsnog waardevol zijn.
Meer over het meten van performance lees je in performance tuning in Go.
Praktijkvoorbeeld: een generieke result type
Een veelgebruikt patroon is een Result[T] type voor functies die óf een waarde óf een error teruggeven:
type Result[T any] struct {
Value T
Err error
}
func Ok[T any](value T) Result[T] {
return Result[T]{Value: value}
}
func Err[T any](err error) Result[T] {
var zero T
return Result[T]{Value: zero, Err: err}
}
Handig voor channels waar je zowel succes- als foutgevallen wilt doorsturen:
results := make(chan Result[User])
go func() {
user, err := fetchUser(id)
if err != nil {
results <- Err[User](err)
return
}
results <- Ok(user)
}()
Pas op: in idiomatisch Go blijft (T, error) als return values vaak de juiste keuze. Gebruik Result[T] alleen waar het écht meerwaarde heeft, zoals bij channels of pipelines.
Beperkingen waar je tegenaan loopt
Generics in Go zijn bewust conservatief ontworpen. Een paar dingen die niet kunnen:
- Geen type parameters op methods, alleen op top-level functies en types.
- Geen switch op type parameter, je kunt niet
switch Tdoen binnen een generieke functie. - Geen specialisatie, je kunt geen aparte implementatie voor een specifiek type definiëren.
- Constraints moeten interfaces zijn, union types werken, maar alleen binnen een interface declaratie.
Voor deze gevallen pak je terug op reflection, code generation of gewoon meerdere concrete functies.
Generics combineren met andere features
Generics werken prima samen met andere Go-features. Een paar voorbeelden:
- Met context: een generieke worker pool die
context.Contexten eenfunc(ctx, T) erroraccepteert. - Met JSON handling: een generieke
Decode[T any](r io.Reader) (T, error)helper. - Met testing: generieke test helpers zoals
AssertEqual[T comparable].
Zie ook worker pools en pipelines voor concurrency patterns die uitstekend generiek gemaakt kunnen worden.
Best practices samengevat
- Start zonder generics. Schrijf eerst concrete code, refactor pas naar generics als het patroon zich herhaalt.
- Houd constraints simpel. Eén of twee type parameters is bijna altijd genoeg.
- Documenteer je constraints. Geef type parameters betekenisvolle namen (
K,V,Tis OK;TNumericValueniet nodig). - Benchmark in hot paths. Generics zijn meestal snel, maar meten blijft beter dan aannemen.
- Volg de community. Pakketten als
golang.org/x/exp/slicesenmapslaten zien hoe generics idiomatisch gebruikt worden.
Veelgestelde vragen
Wat zijn generics in Go?
Generics zijn type parameters waarmee je functies en types schrijft die met meerdere concrete types werken zonder code te duplicatie. Ze zijn sinds Go 1.18 onderdeel van de taal.
Wanneer moet je generics in Go gebruiken?
Gebruik generics wanneer je dezelfde logica schrijft voor meerdere types, zoals bij collecties, slice-helpers of map-utilities. Voor business logic met één concreet type zijn ze meestal overbodig.
Zijn generics sneller of langzamer dan interfaces?
Generics zijn vaak sneller dan interfaces omdat er geen runtime type assertions of boxing nodig is. De compiler genereert efficiëntere code per type-instantiatie, maar het verschil is in de praktijk vaak klein.
Wat zijn type constraints in Go?
Type constraints zijn interfaces die beperken welke types je als type parameter mag gebruiken. Voorbeelden zijn any, comparable of een zelf gedefinieerde constraint zoals Ordered uit het constraints pakket.
Kan ik methods met generics definiëren op bestaande types?
Nee, je kunt geen type parameters toevoegen aan methods van bestaande types. Generics werken alleen op top-level functies en op nieuwe generieke types die je zelf definieert.
Conclusie
Generics zijn een waardevolle toevoeging aan Go, maar geen wondermiddel. Ze excelleren in herbruikbare datastructuren en collection utilities, terwijl interfaces en concrete types vaak beter passen bij business logic.
De vuistregel: gebruik generics als je ze nodig hebt, niet omdat ze er zijn. Schrijf eerst duidelijke, concrete code en abstraheer pas wanneer duplicatie echt pijn doet. Zo behoud je de eenvoud waar Go om bekend staat.