Lessen uit Go in productie: wat je écht leert

Ontdek de belangrijkste lessen uit Go in productie. Van observability en graceful shutdown tot error handling en deploys, praktische inzichten.

31 augustus 20267 min leestijdDoor We Develop Communication

Na jaren Go-services draaien in productie blijven er een paar lessen hangen die je uit geen enkel boek leert. Lessen uit Go in productie gaan vaak niet over de taal zelf, maar over de gewoonten die je ontwikkelt als je om 3 uur 's nachts gebeld wordt. In dit artikel deel ik de belangrijkste inzichten die we hebben opgedaan met Go-applicaties in productie, van observability tot dependencies, van shutdown tot team-dynamiek.

Of je nu je eerste service live zet of al jaren Go-code draait, deze lessen helpen je voorkomen dat je dezelfde pijn moet ervaren als wij.

Observability is geen feature, maar een fundament

De eerste les die iedereen te laat leert: observability moet vanaf dag één in je service zitten. Niet erbij bedacht worden na het eerste incident.

Zonder goede logs, metrics en traces sta je in het donker. Je weet níét of een request langzaam is door de database, een trage downstream service of een lock contention. Je gokt. En in productie gokken kost geld en slaap.

Begin met structured logging via slog, voeg Prometheus metrics toe voor request rates en latencies, en implementeer distributed tracing met OpenTelemetry. Zie onze gids over observability in Go services voor een complete opzet.

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
logger.Info("request processed",
    "method", r.Method,
    "path", r.URL.Path,
    "duration_ms", time.Since(start).Milliseconds(),
    "status", ww.Status(),
)

De tijd die je in observability investeert, verdien je binnen één incident terug.

Graceful shutdown is niet optioneel

Een van de meest onderschatte onderdelen van een Go-service is graceful shutdown. Als je SIGTERM niet netjes afhandelt, verlies je requests, laat je database-connecties open staan en krijg je onverklaarbare 502-errors tijdens elke deploy.

Een correcte shutdown flow:

srv := &http.Server{Addr: ":8080", Handler: router}

go func() {
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("server error: %v", err)
    }
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
    log.Printf("shutdown error: %v", err)
}

Combineer dit met Kubernetes' preStop hook en correcte terminationGracePeriodSeconds. Lees onze diepgaande analyse in Go in Kubernetes: patterns en valkuilen voor de volledige context.

Context is het zenuwstelsel van je service

Context doorgeven lijkt ceremonie, totdat je productie ziet zonder. Zonder context-propagatie kun je niet cancellen, geen deadlines afdwingen en geen trace-IDs doorgeven.

De regel is simpel: elke functie die I/O doet, accepteert context.Context als eerste parameter. Geen uitzonderingen. Lees meer in Context package in Go diep uitgelegd.

func (s *Service) FetchUser(ctx context.Context, id string) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    return s.repo.GetByID(ctx, id)
}

Als je dit patroon consequent volgt, krijg je cancellation, timeouts en tracing cadeau.

Errors zijn data, geen ruis

Go's expliciete error handling is vaak bekritiseerd, maar in productie blijkt het een zegen. Je weet precies waar een fout ontstaat en hoe die doorrolt.

Drie lessen die we hebben geleerd:

  1. Wrap errors met context via fmt.Errorf("fetching user %s: %w", id, err). De stacktrace zelf is niet genoeg, je wilt weten wélke gebruiker.
  2. Gebruik errors.Is en errors.As in plaats van string-vergelijkingen. Dit voorkomt brittle code als error-messages veranderen.
  3. Log alleen op het hoogste niveau. Als elke laag dezelfde fout logt, krijg je duplicaat-noise die oncall gek maakt.

Meer hierover in functions en error handling in Go.

Concurrency is krachtig, maar genadeloos

Go maakt concurrency makkelijk, té makkelijk. Een go statement is zo getypt, maar een goroutine leak vind je pas weken later terug in een OOM-kill.

De harde lessen:

var wg sync.WaitGroup
sem := make(chan struct{}, 10) // max 10 concurrent

for _, item := range items {
    wg.Add(1)
    sem <- struct{}{}
    go func(it Item) {
        defer wg.Done()
        defer func() { <-sem }()
        process(ctx, it)
    }(item)
}
wg.Wait()

Dependencies zijn toekomstige bugs

Elke externe dependency is een onderhoudslast. De Go-community heeft gelukkig een cultuur van minimale dependencies, maar dat moet je bewust bewaken.

Onze vuistregels:

  • Gebruik de standaard library waar mogelijk. net/http, database/sql en encoding/json dekken 80% van je behoefte.
  • Pin versies bewust. Auto-updaten in productie is vragen om problemen.
  • Review wat je binnenhaalt. Een package van 200 regels is beter dan een framework met 50 transitive dependencies.
  • Overweeg vendoring voor kritische services. Zie Go modules en vendoring uitgediept.

De Go Proverbs zeggen het treffend: "A little copying is better than a little dependency."

Testing is niet onderhandelbaar

Services zonder tests zijn services die je niet durft te deployen op vrijdag. En als je niet op vrijdag durft te deployen, heb je een probleem.

Wat werkt in de praktijk:

  • Tabletests voor pure functies en business logic
  • Integration tests met een echte database (via testcontainers of een testing-docker-compose)
  • Benchmark tests voor hot paths, zodat regressies zichtbaar worden
  • Coverage als signaal, niet als doel. 80% coverage met slechte tests is waardeloos.

Lees testing in Go: unit tests, tabletests en coverage voor concrete patronen.

Performance tuning komt later, meestal

Een van de meest waardevolle lessen: premature optimization werkt ook in Go niet. Go is snel genoeg voor vrijwel alles wat je bouwt. Meet eerst, optimaliseer dan.

De workflow die werkt:

  1. Haal baseline metrics uit productie (latency percentielen, throughput, allocations).
  2. Profileer met pprof als er een concrete klacht is.
  3. Fix de top-3 hotspots, meet opnieuw.
  4. Stop zodra het goed genoeg is.

Meer tactische tips in performance tuning in Go: sneller maken in 7 stappen.

Deploys moeten saai zijn

Een spannende deploy is een slechte deploy. Het doel is dat je meerdere keren per dag kunt deployen zonder dat iemand zijn adem inhoudt.

De basis:

  • Health checks die écht zeggen of je gezond bent, niet alleen of het proces draait.
  • Readiness probes die wachten op database-connectie en warmup.
  • Versie-tagging in logs en metrics zodat je weet welke versie het probleem veroorzaakt.
  • Rollback als first-class capability, niet als paniek-procedure.

Zie production deployment van Go apps: complete gids voor een diepere duik.

Eenvoud wint altijd

De laatste en belangrijkste les: Go's kracht is eenvoud, en die moet je bewaken. Het is verleidelijk om abstracties te bouwen, interfaces te stapelen en generics overal in te proppen. Doe het niet.

De Go-community heeft een gezegde: "Clear is better than clever." Een junior die jouw code binnen vijf minuten begrijpt, is meer waard dan een elegante abstractie die alleen jij nog snapt over zes maanden.

Concreet betekent dit:

  • Schrijf code voor de lezer, niet voor de compiler.
  • Gebruik interfaces waar ze helpen, niet preventief. Zie structs en interfaces.
  • Vermijd generics tenzij je concrete herhaling elimineert. Lees generics in Go: wanneer gebruik je ze praktisch.
  • Kleine pakketten met duidelijke verantwoordelijkheid verslaan grote framework-achtige abstracties.

Team-lessen: Go maakt onboarding makkelijker

Een onderschat voordeel van Go in productie is teamdynamiek. Nieuwe developers zijn binnen een week productief. De taal heeft weinig verrassingen, de standaard tooling (go fmt, go vet, go test) elimineert discussies over stijl, en de code is leesbaar zonder IDE-magie.

Dat betekent dat je senior-engineers zich kunnen focussen op architectuur en niet op code-reviews over tabs vs spaces.

Veelgestelde vragen

Wat zijn de belangrijkste lessen uit Go in productie?

Investeer vroeg in observability, hanteer strakke context-propagatie, bouw altijd graceful shutdown in en behandel errors als first-class citizens. Deze fundamenten voorkomen 80% van de productie-incidenten.

Hoe voorkom je goroutine leaks in productie?

Geef elke goroutine een duidelijke exitstrategie via context cancellation of een done-channel. Monitor het aantal goroutines via pprof of runtime metrics zodat je lekken vroeg detecteert.

Waarom is graceful shutdown zo belangrijk?

Zonder graceful shutdown verlies je in-flight requests, corrupte je databases en krijg je flappende deploys. Een schone shutdown geeft handlers de tijd om af te ronden en connecties netjes te sluiten.

Moet je altijd de standaard library gebruiken?

Voor de meeste productiescenario's is de standaard library prima. Pak alleen een externe library als je een concreet probleem oplost dat de stdlib niet dekt, en weeg onderhoudsrisico's mee.

Hoe ga je om met dependencies in productie?

Pin versies in go.mod, review updates bewust en overweeg vendoring voor reproducible builds. Minimaliseer je dependency tree, elke afhankelijkheid is een toekomstig onderhoudsrisico.

Tot slot

Go in productie draaien is minder over de taal en meer over de gewoonten die je opbouwt. Observability vanaf dag één, graceful shutdown als standaard, context overal, errors met context en eenvoud boven cleverness. Als je die fundamenten goed hebt, is Go een van de meest betrouwbare talen om services mee te bouwen.

De rest is discipline, en een team dat de lessen uit incidenten vastlegt in plaats van vergeet. Voor meer verdieping, zie onze complete serie over Golang Development of bekijk de officiële Effective Go guide van het Go-team.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.