Go Technisch Interview: Goroutines, Channels en Concurrency [2026]

Go interviewvragen over goroutines, channels en concurrency-patronen. Codevoorbeelden en antwoorden voor technische Go-interviews.

Go Technical Interview: Goroutines, Channels and Concurrency

Go interviewvragen over goroutines, channels en concurrency behoren tot de zwaarste onderwerpen waarmee kandidaten in technische gesprekken te maken krijgen. Een diepgaand begrip van deze concepten onderscheidt ervaren Go-engineers van ontwikkelaars die de taal nog aan het leren zijn. Deze gids behandelt de exacte vragen die interviewers in 2026 stellen, met productieklare codevoorbeelden en de redenering achter elk antwoord.

Waar interviewers daadwerkelijk op testen

Go concurrency-interviews richten zich op drie gebieden: goroutine-lifecycle management, channel-semantiek (buffered vs unbuffered, directionele types) en patrooncompositie (fan-out/fan-in, worker pools, context-cancellation). Syntaxis uit het hoofd kennen is onvoldoende -- interviewers verwachten dat de kandidaat kan redeneren over race conditions en deadlocks.

Goroutine-basisprincipes: standaardvragen in elk interview

De eerste ronde vragen peilt doorgaans of de kandidaat begrijpt wat goroutines werkelijk zijn, niet alleen hoe men ze start.

V: Wat is een goroutine en hoe verschilt deze van een OS-thread?

Een goroutine is een lichtgewicht concurrent functie die wordt beheerd door de Go runtime-scheduler, niet door het besturingssysteem. De Go runtime multiplext duizenden goroutines over een klein aantal OS-threads met behulp van een M:N-schedulingmodel (M goroutines toegewezen aan N OS-threads).

goroutine_basics.gogo
package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	// Print the number of OS threads available
	fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

	var wg sync.WaitGroup

	for i := 0; i < 10000; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			// Each goroutine starts with ~2-8KB stack
			// OS threads typically start with 1-8MB
			_ = id
		}(i)
	}

	wg.Wait()
	fmt.Println("All 10,000 goroutines completed")
}

Belangrijke verschillen om te benoemen in een interview: goroutines starten met een stack van 2-8KB die dynamisch groeit, tegenover de vaste stack van 1-8MB van een OS-thread. Context switching tussen goroutines wordt afgehandeld in user space door de Go-scheduler, waardoor de kostbare kernel-level context switches van OS-threads worden vermeden. Dit maakt het starten van 100.000 goroutines praktisch haalbaar, terwijl 100.000 OS-threads de systeembronnen zouden uitputten.

V: Wat gebeurt er als een goroutine in paniek raakt?

Een onafgevangen panic in een willekeurige goroutine crasht het volledige programma. Anders dan exceptions in Java of Python propageert een panic omhoog via de call stack van de goroutine zelf, niet via de stack van de goroutine die deze heeft gestart. De enige manier om de panic op te vangen is met recover() binnen een deferred functie in dezelfde goroutine.

panic_recovery.gogo
package main

import "fmt"

func safeGo(fn func()) {
	go func() {
		defer func() {
			if r := recover(); r != nil {
				fmt.Println("recovered from panic:", r)
			}
		}()
		fn() // execute the actual work
	}()
}

func main() {
	safeGo(func() {
		panic("something went wrong")
	})

	// Give goroutine time to complete
	select {}
}

Interviewers letten op het besef dat productie-Go-services goroutine-launches altijd inpakken in een recovery-patroon. Bibliotheken zoals errgroup behandelen dit op een elegantere manier.

Channel-semantiek: buffered, unbuffered en directioneel

Channelvragen onthullen of de kandidaat het concurrency-model van Go daadwerkelijk begrijpt of slechts patronen uit het hoofd heeft geleerd.

V: Wat is het verschil tussen een buffered en een unbuffered channel?

Een unbuffered channel (make(chan T)) vereist dat zowel verzender als ontvanger tegelijkertijd gereed zijn -- de verzending blokkeert totdat een andere goroutine ontvangt. Een buffered channel (make(chan T, n)) staat toe dat maximaal n waarden worden verzonden zonder te blokkeren.

channel_semantics.gogo
package main

import "fmt"

func main() {
	// Unbuffered: send blocks until receive is ready
	ch := make(chan string)
	go func() {
		ch <- "hello" // blocks here until main reads
	}()
	msg := <-ch
	fmt.Println(msg)

	// Buffered: send does not block until buffer is full
	buf := make(chan int, 3)
	buf <- 1 // does not block (buffer has space)
	buf <- 2 // does not block
	buf <- 3 // does not block
	// buf <- 4 would block — buffer is full

	fmt.Println(<-buf, <-buf, <-buf) // 1 2 3
}

De vervolgvraag die interviewers vaak stellen: "Wanneer kies je het ene boven het andere?" Unbuffered channels dwingen synchronisatie af -- nuttig wanneer de verzender moet weten dat de ontvanger de waarde heeft verwerkt. Buffered channels ontkoppelen de timing van verzender en ontvanger -- nuttig voor werkwachtrijen of rate limiting, waar enige speling acceptabel is.

V: Wat gebeurt er wanneer een channel wordt gesloten?

Het sluiten van een channel signaleert dat er geen waarden meer worden verzonden. Ontvangstoperaties op een gesloten channel retourneren onmiddellijk de zero value. Verzending op een gesloten channel veroorzaakt een panic. Een range-loop over een channel eindigt wanneer het channel wordt gesloten.

close_channel.gogo
package main

import "fmt"

func producer(ch chan<- int, count int) {
	for i := 0; i < count; i++ {
		ch <- i
	}
	close(ch) // signal: no more values
}

func main() {
	ch := make(chan int, 5)
	go producer(ch, 5)

	// range exits automatically when channel closes
	for val := range ch {
		fmt.Println("received:", val)
	}

	// Reading from closed channel returns zero value + false
	val, ok := <-ch
	fmt.Printf("after close: val=%d, ok=%v\n", val, ok)
}

Een kritiek punt: alleen de verzender dient een channel te sluiten, nooit de ontvanger. Het sluiten van een channel waar een andere goroutine nog naar schrijft, veroorzaakt een panic.

Klaar om je Go gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Het select-statement: channels multiplexen

V: Hoe werkt select en wat gebeurt er wanneer meerdere cases gereed zijn?

Het select-statement blokkeert totdat een van de channel-operaties kan doorgaan. Wanneer meerdere cases tegelijkertijd gereed zijn, kiest Go er willekeurig een -- dit voorkomt uithongering van een bepaalde case.

select_multiplex.gogo
package main

import (
	"context"
	"fmt"
	"time"
)

func fetchFromAPI(ctx context.Context, url string) (string, error) {
	resultCh := make(chan string, 1)
	errCh := make(chan error, 1)

	go func() {
		// Simulate API call
		time.Sleep(200 * time.Millisecond)
		resultCh <- fmt.Sprintf("data from %s", url)
	}()

	select {
	case result := <-resultCh:
		return result, nil
	case err := <-errCh:
		return "", err
	case <-ctx.Done():
		// Context cancelled or timed out
		return "", ctx.Err()
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()

	result, err := fetchFromAPI(ctx, "https://api.example.com/data")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(result)
}

Interviewers testen twee zaken met select: begrip van de willekeurige selectieregel en het vermogen om channels te combineren met context.Context voor timeout- en cancellation-patronen.

Veelvoorkomende concurrency-patronen in interviews

Bij Go-interviews op seniorniveau komt vrijwel altijd de opdracht voor om een van deze patronen vanaf nul te implementeren.

Het Fan-Out/Fan-In-patroon

V: Implementeer een fan-out/fan-in pipeline die items gelijktijdig verwerkt.

Fan-out verdeelt werk over meerdere goroutines. Fan-in verzamelt resultaten van meerdere goroutines in een enkel channel.

fanout_fanin.gogo
package main

import (
	"fmt"
	"sync"
)

// generator produces values on a channel
func generator(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out)
	}()
	return out
}

// square reads from input, squares each value
func square(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			out <- n * n
		}
		close(out)
	}()
	return out
}

// fanIn merges multiple channels into one
func fanIn(channels ...<-chan int) <-chan int {
	var wg sync.WaitGroup
	merged := make(chan int)

	for _, ch := range channels {
		wg.Add(1)
		go func(c <-chan int) {
			defer wg.Done()
			for val := range c {
				merged <- val
			}
		}(ch)
	}

	go func() {
		wg.Wait()
		close(merged) // close after all inputs are drained
	}()

	return merged
}

func main() {
	in := generator(2, 3, 4, 5, 6)

	// Fan out: two goroutines reading from same channel
	c1 := square(in)
	c2 := square(in)

	// Fan in: merge results
	for result := range fanIn(c1, c2) {
		fmt.Println(result)
	}
}

Het cruciale inzicht dat interviewers willen horen: het generator-channel wordt gedeeld tussen c1 en c2, waardoor elke waarde door precies een worker wordt verwerkt (niet gedupliceerd). De fanIn-functie gebruikt een WaitGroup om te weten wanneer alle invoerchannels zijn leeggelezen voordat het samengevoegde channel wordt gesloten.

Worker Pool met errgroup

V: Hoe zou men een begrensd worker pool met foutafhandeling implementeren?

Het pakket golang.org/x/sync/errgroup (onderdeel van de uitgebreide Go-standaardbibliotheek) lost dit op een nette manier op. Het beheert goroutine-lifecycles, verzamelt de eerste fout en integreert met context voor cancellation.

worker_pool.gogo
package main

import (
	"context"
	"fmt"
	"golang.org/x/sync/errgroup"
)

func processItem(ctx context.Context, id int) error {
	// Check for cancellation before heavy work
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	if id == 7 {
		return fmt.Errorf("failed to process item %d", id)
	}
	fmt.Printf("processed item %d\n", id)
	return nil
}

func main() {
	g, ctx := errgroup.WithContext(context.Background())
	g.SetLimit(3) // maximum 3 concurrent goroutines

	for i := 0; i < 10; i++ {
		id := i
		g.Go(func() error {
			return processItem(ctx, id)
		})
	}

	// Wait blocks until all goroutines finish
	// Returns the first non-nil error
	if err := g.Wait(); err != nil {
		fmt.Println("pipeline error:", err)
	}
}

Vanaf Go 1.24 (de huidige stabiele release begin 2026) blijft dit patroon de aanbevolen aanpak. De SetLimit-methode werd toegevoegd in Go 1.20 en vervangt de noodzaak om handmatig semafoor-gebaseerde concurrency-begrenzing te implementeren.

Race conditions en sync-primitieven

V: Hoe detecteert en voorkomt men race conditions in Go?

Go beschikt over een ingebouwde race detector die wordt geactiveerd met de -race-vlag. Deze detecteert ongesynchroniseerde gelijktijdige toegang tot gedeeld geheugen tijdens runtime.

race_condition.gogo
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

// BAD: race condition — do not use in production
func unsafeCounter() int {
	counter := 0
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter++ // DATA RACE: concurrent read/write
		}()
	}
	wg.Wait()
	return counter // result is non-deterministic
}

// GOOD: atomic operations for simple counters
func atomicCounter() int64 {
	var counter atomic.Int64
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter.Add(1) // thread-safe atomic increment
		}()
	}
	wg.Wait()
	return counter.Load() // always 1000
}

// GOOD: mutex for complex shared state
type SafeMap struct {
	mu sync.RWMutex
	data map[string]int
}

func (m *SafeMap) Set(key string, val int) {
	m.mu.Lock()         // exclusive lock for writes
	defer m.mu.Unlock()
	m.data[key] = val
}

func (m *SafeMap) Get(key string) (int, bool) {
	m.mu.RLock()         // shared lock for reads
	defer m.mu.RUnlock()
	v, ok := m.data[key]
	return v, ok
}

func main() {
	fmt.Println("unsafe:", unsafeCounter())  // unpredictable
	fmt.Println("atomic:", atomicCounter())  // always 1000
}

Het interviewantwoord dient drie synchronisatiestrategieën te behandelen: sync.Mutex / sync.RWMutex voor complexe gedeelde state, sync/atomic voor eenvoudige tellers en vlaggen, en channels voor communicatie tussen goroutines ("share memory by communicating, don't communicate by sharing memory"). Het uitvoeren van go test -race ./... dient onderdeel te zijn van elke CI-pipeline.

Context en cancellation-patronen

V: Leg uit hoe context.Context de levenscyclus van goroutines beheert.

Het context-pakket biedt een mechanisme om cancellation-signalen, deadlines en request-scoped waarden te propageren over goroutine-grenzen heen. Elke langlopende goroutine dient een context.Context als eerste parameter te accepteren.

context_cancellation.gogo
package main

import (
	"context"
	"fmt"
	"time"
)

// worker simulates a long-running task
func worker(ctx context.Context, id int, results chan<- string) {
	select {
	case <-time.After(time.Duration(id*100) * time.Millisecond):
		results <- fmt.Sprintf("worker %d: done", id)
	case <-ctx.Done():
		results <- fmt.Sprintf("worker %d: cancelled (%v)", id, ctx.Err())
	}
}

func main() {
	// Parent context with 250ms deadline
	ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
	defer cancel()

	results := make(chan string, 5)

	// Launch 5 workers with increasing durations
	for i := 1; i <= 5; i++ {
		go worker(ctx, i, results)
	}

	// Collect all results
	for i := 0; i < 5; i++ {
		fmt.Println(<-results)
	}
}

Workers 1 en 2 voltooien binnen de deadline van 250ms. Workers 3, 4 en 5 ontvangen het cancellation-signaal via ctx.Done(). Dit patroon is fundamenteel voor het bouwen van veerkrachtige HTTP-servers en microservices in Go -- elke request handler ontvangt een context die cancellation propageert wanneer de client de verbinding verbreekt.

Veelvoorkomende interviewvalkuil

Sla een context.Context nooit op in een struct-veld. De officiële Go-documentatie stelt expliciet: "Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it." Interviewers testen dit om te beoordelen of de kandidaat Go-conventies volgt.

Deadlock-detectie: lastige interviewvragen

V: Zal deze code een deadlock veroorzaken? Waarom?

Deadlockvragen zijn populair omdat ze testen of de kandidaat kan redeneren over goroutine-scheduling en channel-operaties.

deadlock_example.gogo
package main

func main() {
	ch := make(chan int)
	ch <- 42 // DEADLOCK: unbuffered send with no receiver
	// The main goroutine blocks here forever
	// Go runtime detects this: "fatal error: all goroutines are asleep"
}

De oplossing is eenvoudig: maak het channel buffered (make(chan int, 1)) of start een goroutine om te ontvangen voordat er wordt verzonden. De Go runtime detecteert deadlocks wanneer alle goroutines geblokkeerd zijn -- maar alleen wanneer alle goroutines in slaap zijn. Als zelfs maar één goroutine actief is (bijvoorbeeld een HTTP-server op de achtergrond), zal de runtime een partiële deadlock niet detecteren.

Partiële deadlocks zijn onzichtbaar

De Go runtime detecteert deadlocks alleen wanneer elke goroutine in het programma geblokkeerd is. In echte applicaties met HTTP-servers of achtergrondwerkers worden gelekte goroutines die in een deadlock verkeren niet opgepikt door de runtime-detector. Tools zoals pprof en goroutine dumps (runtime.Stack) zijn noodzakelijk om deze problemen in productie te diagnosticeren.

Rate-limited gelijktijdige verwerking

V: Hoe zou men rate-limited gelijktijdige API-aanroepen implementeren?

Deze vraag test het vermogen om meerdere concurrency-primitieven te combineren tot een samenhangende oplossing.

rate_limited.gogo
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

// RateLimiter controls concurrent and temporal access
type RateLimiter struct {
	semaphore chan struct{}   // limits concurrency
	ticker    *time.Ticker    // limits rate
}

func NewRateLimiter(maxConcurrent int, interval time.Duration) *RateLimiter {
	return &RateLimiter{
		semaphore: make(chan struct{}, maxConcurrent),
		ticker:    time.NewTicker(interval),
	}
}

func (rl *RateLimiter) Execute(ctx context.Context, fn func() error) error {
	// Wait for rate limit tick
	select {
	case <-rl.ticker.C:
	case <-ctx.Done():
		return ctx.Err()
	}

	// Acquire concurrency slot
	select {
	case rl.semaphore <- struct{}{}:
	case <-ctx.Done():
		return ctx.Err()
	}

	defer func() { <-rl.semaphore }() // release slot
	return fn()
}

func main() {
	rl := NewRateLimiter(3, 100*time.Millisecond)
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			err := rl.Execute(ctx, func() error {
				fmt.Printf("[%v] processing %d\n", time.Now().Format("04:05.000"), id)
				time.Sleep(150 * time.Millisecond) // simulate work
				return nil
			})
			if err != nil {
				fmt.Printf("item %d: %v\n", id, err)
			}
		}(i)
	}
	wg.Wait()
}

Dit patroon combineert een channel-gebaseerde semafoor (voor concurrency-begrenzing) met een ticker (voor rate limiting). De dubbele select met context-controle zorgt voor een graceful shutdown. Dit is het type productieklaar antwoord dat senior kandidaten onderscheidt van minder ervaren ontwikkelaars.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Conclusie

  • Goroutines zijn user-space threads beheerd door de Go runtime met M:N-scheduling; vang panics altijd op in gestarte goroutines
  • Unbuffered channels synchroniseren verzender en ontvanger; buffered channels ontkoppelen de timing -- kies op basis van of de verzender bevestiging nodig heeft
  • Het select-statement multiplext channel-operaties met willekeurige selectie wanneer meerdere cases gereed zijn; combineer met context.Context voor timeouts
  • Fan-out/fan-in en worker pools (via errgroup.SetLimit) zijn de twee meest gevraagde concurrency-patronen
  • Gebruik sync.Mutex voor complexe gedeelde state, sync/atomic voor eenvoudige tellers en channels voor goroutine-communicatie
  • Voer altijd go test -race uit in CI om data races op te sporen; partiële deadlocks vereisen pprof voor diagnose
  • Sla context.Context nooit op in structs -- geef het door als eerste functieparameter
  • Rate limiting in Go combineert channel-semaforen met tickers, gewrapped in context-aware select-statements

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#go
#interview
#concurrency
#goroutines
#channels

Delen

Gerelateerde artikelen