Als je vanuit talen als PHP, JavaScript of Python naar Go komt, valt één ding meteen op: de manier waarop je met functions en error handling omgaat is fundamenteel anders. Geen try/catch, geen exceptions, geen verborgen controle-flows. In plaats daarvan krijg je expliciete errors als return value, en dat blijkt in de praktijk een verademing.
In dit artikel leer je hoe je functies schrijft in Go, hoe multiple return values werken, en waarom Go's idiomatische error handling je code robuuster maakt. We bouwen voort op eerdere artikelen uit onze serie, zoals variabelen en types in Go en structs en interfaces.
De anatomie van een Go functie
Een functie in Go begint altijd met het sleutelwoord func, gevolgd door de naam, parameters en eventueel return types. De syntaxis is compact maar expliciet.
func optellen(a int, b int) int {
return a + b
}
Wat meteen opvalt: het type staat achter de parameternaam, niet ervoor zoals in C of Java. Dit past bij Go's filosofie dat code van links naar rechts gelezen moet worden, net als een natuurlijke zin.
Als meerdere parameters hetzelfde type hebben, kun je dit verkorten:
func optellen(a, b int) int {
return a + b
}
Parameters en arguments
Parameters worden standaard by value doorgegeven. Dat betekent dat Go een kopie maakt van de waarde. Wil je een variabele kunnen aanpassen binnen de functie? Dan gebruik je een pointer.
func verdubbel(x *int) {
*x = *x * 2
}
getal := 5
verdubbel(&getal)
// getal is nu 10
Voor slices, maps en channels hoef je geen pointer te gebruiken: die zijn intern al referentie-achtig. Dit is een veelgemaakte fout bij beginners die uit talen komen waar alles een object-referentie is.
Multiple return values: Go's superpower
Waar veel talen één return value ondersteunen en tuples nodig hebben voor meerdere, heeft Go dit native ingebouwd. Dit is geen gimmick, het vormt de basis van heel het error handling model.
func deel(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("delen door nul")
}
return a / b, nil
}
De aanroepende code gebruikt beide waarden:
resultaat, err := deel(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(resultaat)
Named return values
Go laat je return values een naam geven. Dit werkt als een soort voorgedefinieerde variabele binnen de functie.
func splits(volledig string) (voornaam, achternaam string) {
delen := strings.SplitN(volledig, " ", 2)
voornaam = delen[0]
if len(delen) > 1 {
achternaam = delen[1]
}
return
}
De return zonder argumenten heet een naked return. Handig voor korte functies, maar gebruik het met mate, in langere functies wordt de code moeilijker te volgen.
Variadic functions
Soms weet je niet vooraf hoeveel argumenten een functie krijgt. Denk aan fmt.Println of een eigen log-helper. Met het ... syntax maak je een variadic function.
func som(getallen ...int) int {
totaal := 0
for _, g := range getallen {
totaal += g
}
return totaal
}
som(1, 2, 3) // 6
som(1, 2, 3, 4, 5) // 15
Binnen de functie gedraagt getallen zich als een slice van int. Wil je een bestaande slice doorgeven? Gebruik dan som(mijnSlice...) met drie puntjes achter de variabele.
Functions als first-class citizens
In Go zijn functies gewone waarden. Je kunt ze aan variabelen toewijzen, als parameter doorgeven en als return value terugsturen. Dit opent de deur naar functionele patronen zoals higher-order functions.
func pasToe(getallen []int, bewerking func(int) int) []int {
resultaat := make([]int, len(getallen))
for i, g := range getallen {
resultaat[i] = bewerking(g)
}
return resultaat
}
verdubbeld := pasToe([]int{1, 2, 3}, func(x int) int {
return x * 2
})
Anonieme functies (closures) kunnen variabelen uit hun omgeving vasthouden. Dit is krachtig voor bijvoorbeeld middleware of event handlers, maar let op geheugengebruik bij langlopende processen.
Error handling: expliciet boven impliciet
Dit is waar Go écht afwijkt van veel andere talen. Er is geen try/catch, geen exceptions die door de callstack bubbelen. In plaats daarvan retourneert elke functie die kan falen een error als laatste return value.
De error interface
Een error in Go is simpelweg een interface met één methode:
type error interface {
Error() string
}
Dat betekent dat elk type dat een Error() string methode heeft, als error gebruikt kan worden. Meer over hoe interfaces werken lees je in ons artikel over structs en interfaces.
Het if err != nil pattern
Je zult deze drie regels code honderden keren schrijven in een Go project:
resultaat, err := doeIets()
if err != nil {
return nil, err
}
Ja, het is repetitief. Nee, dat is geen bug, het is een feature. Elke foutpad is zichtbaar, je kunt niet per ongeluk een error negeren, en code reviews worden makkelijker omdat de controleflow expliciet is.
Errors aanmaken
Er zijn meerdere manieren om een error te creëren:
// Eenvoudig via errors.New
err1 := errors.New("bestand niet gevonden")
// Met formatting via fmt.Errorf
err2 := fmt.Errorf("gebruiker %s bestaat niet", naam)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (v *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", v.Field, v.Message)
}
Custom error types zijn vooral handig wanneer je calling code wilt laten reageren op specifieke error-situaties.
Error wrapping: context zonder verlies
Sinds Go 1.13 kun je errors wrappen met %w in fmt.Errorf. Dit voegt context toe zonder de originele error te verliezen.
func leesConfig(pad string) (*Config, error) {
data, err := os.ReadFile(pad)
if err != nil {
return nil, fmt.Errorf("config lezen van %s: %w", pad, err)
}
// ...
}
Later in de callstack kun je met errors.Is en errors.As de oorspronkelijke error inspecteren:
if errors.Is(err, os.ErrNotExist) {
// bestand bestaat niet, gebruik defaults
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Printf("padfout op %s", pathErr.Path)
}
Dit pattern is cruciaal voor productiecode. Meer achtergrond vind je in de officiële Go blog post over error handling.
Sentinel errors en error types
Naast gewone errors kun je sentinel errors definiëren: vaste error-waarden die callers kunnen herkennen.
var ErrGebruikerNietGevonden = errors.New("gebruiker niet gevonden")
func zoekGebruiker(id int) (*User, error) {
if id == 0 {
return nil, ErrGebruikerNietGevonden
}
// ...
}
Callers kunnen dan precies op deze error checken:
user, err := zoekGebruiker(id)
if errors.Is(err, ErrGebruikerNietGevonden) {
// toon 404
}
Een bekend voorbeeld is sql.ErrNoRows uit de standard library. Gebruik sentinels spaarzaam, ze creëren een publieke API waarop code kan leunen.
Panic en recover: de noodrem
Voor situaties waarin verdergaan geen zin heeft, bestaat panic. Dit stopt de normale uitvoering en unwinds de stack. Met recover (binnen een defer) kun je een panic opvangen.
func veiligeOperatie() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic hersteld: %v", r)
}
}()
// potentieel gevaarlijke code
return nil
}
Belangrijk: gebruik panic niet voor gewone fouten. Reserveer het voor programmeerfouten (zoals een onmogelijke state) of het opstarten van een applicatie. Voor gewone foutafhandeling gebruik je altijd het error pattern.
Defer: cleanup gegarandeerd
defer zet een functie-aanroep uit tot het moment dat de omringende functie terugkeert, ook als er een panic optreedt. Dit is perfect voor resource cleanup.
func leesBestand(pad string) ([]byte, error) {
f, err := os.Open(pad)
if err != nil {
return nil, err
}
defer f.Close()
return io.ReadAll(f)
}
Meerdere defer statements worden in LIFO-volgorde uitgevoerd. Denk aan het als een stack: de laatste defer loopt als eerste.
Best practices voor error handling
Een paar vuistregels die je code professioneler maken:
- Handle errors direct: laat ze niet in variabelen rondslingeren
- Voeg context toe bij het doorgeven met
fmt.Errorf("... %w", err) - Log één keer: op het hoogste niveau waar je de error afhandelt
- Gebruik
errors.Isenerrors.Asin plaats van string-vergelijkingen - Vermijd naakte
panicin library code, laat de caller beslissen
Voor wie dieper wil graven, is de Effective Go gids verplichte kost. Hierin staat hoe de Go-community zelf denkt over idiomatische foutafhandeling.
Praktijkvoorbeeld: alles samen
Laten we alles combineren in een realistisch voorbeeld:
var ErrOngeldigBedrag = errors.New("bedrag moet positief zijn")
func verwerkBetaling(ctx context.Context, bedrag float64) (string, error) {
if bedrag <= 0 {
return "", ErrOngeldigBedrag
}
txID, err := startTransactie(ctx, bedrag)
if err != nil {
return "", fmt.Errorf("transactie starten voor %.2f: %w", bedrag, err)
}
defer cleanupTransactie(txID)
if err := bevestigBetaling(ctx, txID); err != nil {
return "", fmt.Errorf("betaling bevestigen %s: %w", txID, err)
}
return txID, nil
}
Dit toont het complete plaatje: validatie, wrapping, defer voor cleanup, en duidelijke return paths. Geen verborgen exceptions, geen verrassingen.
Hoe nu verder
Functions en error handling vormen het skelet van elke Go-applicatie. Als je deze concepten beheerst, kun je serieuze backend-code schrijven. Begin met kleine projecten: een CLI tool, een HTTP handler, een simpele parser. De Go by Example website is een uitstekende bron voor korte, werkende voorbeelden.
Wil je weten waarom Go zo geliefd is bij backend-developers? Lees dan waarom Go populair is voor backends. Nog niet gestart? Check dan eerst installatie en eerste Go programma.
Veelgestelde vragen
Waarom heeft Go geen try/catch zoals andere talen?
Go kiest bewust voor expliciete error handling via return values. Dit maakt foutpaden zichtbaar in de code, voorkomt verborgen exceptions en dwingt ontwikkelaars om bewust na te denken over wat er mis kan gaan. Het resultaat is voorspelbaardere en beter onderhoudbare code.
Wat is het verschil tussen error en panic in Go?
Een error is een verwachte fout die je netjes afhandelt, zoals een bestand dat niet bestaat of ongeldige input. Een panic is voor onverwachte situaties waarbij het programma niet verder kan, zoals een nil pointer dereference. In productiecode gebruik je bijna altijd errors, zelden panics.
Kan een Go functie meerdere waarden teruggeven?
Ja, Go ondersteunt multiple return values standaard. Dit wordt veel gebruikt om zowel een resultaat als een error terug te geven, bijvoorbeeld result, err := doeIets(). Je kunt zoveel waarden teruggeven als nodig, al is meer dan drie vaak een teken dat je een struct moet gebruiken.
Moet ik altijd errors.Is of errors.As gebruiken?
Gebruik errors.Is wanneer je wilt controleren op een specifieke sentinel error zoals sql.ErrNoRows. Gebruik errors.As wanneer je een error wilt uitpakken naar een specifiek type om meer details te krijgen. Voor eenvoudige checks volstaat vaak if err != nil.
Wat betekent het om een error te wrappen?
Error wrapping voegt context toe aan een oorspronkelijke error zonder de informatie te verliezen. Met fmt.Errorf("context: %w", err) behoud je de originele error, zodat je later met errors.Is of errors.As de root cause kunt achterhalen. Dit maakt debuggen in productie een stuk makkelijker.