# Rozmowa techniczna z Go: Goroutines, Channels i Concurrency > Pytania z rozmowy kwalifikacyjnej Go dotyczace goroutines, channels i wzorcow wspolbieznosci. Przyklady kodu, typowe pulapki i odpowiedzi na poziomie eksperckim na rozmowy techniczne Go w 2026 roku. - Published: 2026-04-01 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: go, golang, interview, goroutines, channels, concurrency - Reading time: 9 min --- Pytania dotyczace goroutines, channels i wspolbieznosci w Go konsekwentnie naleza do najtrudniejszych tematow, z jakimi spotykaja sie kandydaci na rozmowach kwalifikacyjnych. Gleboka znajomosc tych koncepcji odroznia seniorowych inzynierow Go od osob wciaz uczacych sie jezyka. Ten przewodnik obejmuje dokladnie te pytania, ktore rekruterzy zadaja w 2026 roku, wraz z produkcyjnymi przykladami kodu i uzasadnieniem kazdej odpowiedzi. > **Co faktycznie testuja rekruterzy** > > Rozmowy dotyczace wspolbieznosci w Go koncentruja sie na trzech obszarach: zarzadzanie cyklem zycia goroutines, semantyka kanalow (buforowane vs niebuforowane, typy kierunkowe) oraz kompozycja wzorcow (fan-out/fan-in, pule workerow, anulowanie kontekstu). Samo zapamietanie skladni nie wystarczy — rekruterzy oczekuja, ze kandydaci potrafia rozumowac o wyscigach danych i zakleszczeniach. ## Podstawy Goroutines: Co pyta kazdy rekruter Pierwsza runda pytan zazwyczaj sprawdza, czy kandydat rozumie, czym goroutines naprawde sa — nie tylko jak je uruchamiac. **P: Czym jest goroutine i jak rozni sie od watku systemu operacyjnego?** Goroutine to lekka wspolbiezna funkcja zarzadzana przez scheduler runtime Go, a nie przez system operacyjny. Runtime Go multipleksuje tysiace goroutines na niewielka liczbe watkow OS przy uzyciu modelu planowania M:N (M goroutines mapowanych na N watkow OS). ```go // goroutine_basics.go 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") } ``` Kluczowe roznice, ktore nalezy wymienic na rozmowie: goroutines startuja ze stosem 2-8KB, ktory rosnie dynamicznie, w przeciwienstwie do stalego stosu 1-8MB watku OS. Przelaczanie kontekstu miedzy goroutines odbywa sie w przestrzeni uzytkownika przez scheduler Go, co eliminuje kosztowne przelaczanie na poziomie jadra systemu. Dzieki temu uruchomienie 100 000 goroutines jest praktyczne, podczas gdy 100 000 watkow OS wyczerpaloby zasoby systemowe. **P: Co sie dzieje, gdy goroutine wywola panic?** Nieobsluzony panic w dowolnym goroutine powoduje crash calego programu. W przeciwienstwie do wyjatkow w Javie czy Pythonie, panic propaguje sie w gore stosu wywolan wlasnego goroutine — nie stosu goroutine, ktory go uruchomil. Jedyny sposob na przechwycenie to `recover()` wewnatrz odroczonej funkcji w tym samym goroutine. ```go // panic_recovery.go 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 {} } ``` Rekruterzy szukaja swiadomosci, ze produkcyjne serwisy Go opakowuja uruchomienia goroutines we wzorzec recovery. Biblioteki takie jak `errgroup` obsluguja to bardziej elegancko. ## Semantyka Kanalow: Buforowane, Niebuforowane i Kierunkowe Pytania o kanaly ujawniaja, czy kandydat naprawde rozumie model wspolbieznosci Go, czy tylko zapamietuje wzorce. **P: Jaka jest roznica miedzy kanalem buforowanym a niebuforowanym?** Kanal niebuforowany (`make(chan T)`) wymaga, aby nadawca i odbiorca byli gotowi jednoczesnie — wysylanie blokuje do momentu, gdy inny goroutine odbierze wartosc. Kanal buforowany (`make(chan T, n)`) pozwala wyslac do `n` wartosci bez blokowania. ```go // channel_semantics.go 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 } ``` Czeste pytanie uzupelniajace: "Kiedy wybrac jedno lub drugie?" Kanaly niebuforowane wymuszaja synchronizacje — przydatne, gdy nadawca musi wiedziec, ze odbiorca przetworzyl wartosc. Kanaly buforowane rozdzielaja czas nadawcy i odbiorcy — przydatne w kolejkach zadan lub ograniczaniu szybkosci, gdzie pewien luz jest akceptowalny. **P: Co sie dzieje po zamknieciu kanalu?** Zamkniecie kanalu sygnalizuje, ze zadne wiecej wartosci nie beda wyslane. Odbiory z zamknietego kanalu natychmiast zwracaja wartosc zerowa. Wyslanie na zamkniety kanal powoduje panic. Petla `range` po kanale konczy sie, gdy kanal zostanie zamkniety. ```go // close_channel.go 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) } ``` Krytyczny punkt: tylko nadawca powinien zamykac kanal, nigdy odbiorca. Zamkniecie kanalu, na ktory inny goroutine wciaz pisze, powoduje panic. ## Instrukcja Select: Multipleksowanie Kanalow **P: Jak dziala `select` i co sie dzieje, gdy kilka przypadkow jest gotowych jednoczesnie?** Instrukcja `select` blokuje do momentu, az jedna z operacji na kanale moze zostac wykonana. Gdy kilka przypadkow jest gotowych jednoczesnie, Go wybiera jeden losowo — zapobiega to zaglodzeniu ktorejkolwiek galezi. ```go // select_multiplex.go 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) } ``` Rekruterzy testuja dwie rzeczy za pomoca `select`: rozumienie reguly losowego wyboru oraz umiejetnosc laczenia kanalow z `context.Context` do obslugi timeoutow i wzorcow anulowania. ## Popularne wzorce wspolbieznosci na rozmowach kwalifikacyjnych Rozmowy kwalifikacyjne na poziomie seniora prawie zawsze obejmuja pytanie o implementacje jednego z tych wzorcow od podstaw. ### Wzorzec Fan-Out/Fan-In **P: Zaimplementuj potok fan-out/fan-in, ktory przetwarza elementy wspolbieznie.** Fan-out rozdziela prace na wiele goroutines. Fan-in zbiera wyniki z wielu goroutines do jednego kanalu. ```go // fanout_fanin.go 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) } } ``` Kluczowy wniosek, ktorego oczekuja rekruterzy: kanal `generator` jest wspoldzielony miedzy `c1` i `c2`, wiec kazda wartosc jest przetwarzana przez dokladnie jednego workera (nie duplikowana). Funkcja `fanIn` uzywa `WaitGroup`, aby wiedziec, kiedy wszystkie kanaly wejsciowe zostana oprozniowane przed zamknieciem kanalu scalonego. ### Pula Workerow z errgroup **P: Jak zaimplementowac ograniczona pule workerow z obsluga bledow?** Pakiet `golang.org/x/sync/errgroup` (czesc rozszerzonej biblioteki standardowej Go) rozwiazuje to czysto. Zarzadza cyklem zycia goroutines, zbiera pierwszy blad i integruje sie z `context` do anulowania. ```go // worker_pool.go 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) } } ``` Od Go 1.24 (aktualna stabilna wersja na poczatek 2026 roku) ten wzorzec pozostaje zalecanym podejsciem. Metoda `SetLimit` zostala dodana w Go 1.20 i eliminuje potrzebe recznej implementacji ograniczania wspolbieznosci opartego na semaforach. ## Wyscigi danych i prymitywy sync **P: Jak wykrywac i zapobiegac wyscigow danych w Go?** Go zapewnia wbudowany detektor wyscigow aktywowany flaga `-race`. Wykrywa on niesynchronizowany rownoczesty dostep do wspoldzielonej pamieci w czasie wykonywania. ```go // race_condition.go 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 } ``` Odpowiedz na rozmowie powinna obejmowac trzy strategie synchronizacji: `sync.Mutex` / `sync.RWMutex` dla zlozonego wspoldzielonego stanu, `sync/atomic` dla prostych licznikow i flag, oraz kanaly do komunikacji miedzy goroutines ("dziel pamiec poprzez komunikowanie sie, nie komunikuj sie poprzez dzielenie pamieci"). Uruchamianie `go test -race ./...` powinno byc czescia kazdego pipeline CI. ## Kontekst i wzorce anulowania **P: Jak context.Context kontroluje cykl zycia goroutines?** Pakiet `context` dostarcza mechanizm propagacji sygnalow anulowania, terminow i wartosci zakresowych miedzy goroutines. Kazdy dlugo dzialajacy goroutine powinien przyjmowac `context.Context` jako swoj pierwszy parametr. ```go // context_cancellation.go 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) } } ``` Workery 1 i 2 koncza sie w ramach limitu 250ms. Workery 3, 4 i 5 otrzymuja sygnal anulowania przez `ctx.Done()`. Ten wzorzec jest fundamentalny przy budowie odpornych serwerow HTTP i mikroserwisow w Go — kazdy handler zapytan otrzymuje kontekst, ktory propaguje anulowanie, gdy klient sie rozlaczy. > **Czesta pulapka na rozmowie** > > Nigdy nie nalezy przechowywac `context.Context` w polu struktury. [Oficjalna dokumentacja Go](https://pkg.go.dev/context) wyraznie stwierdza: "Nie przechowuj kontekstow wewnatrz typow struktur; zamiast tego przekazuj kontekst jawnie do kazdej funkcji, ktora go potrzebuje." Rekruterzy testuja to, aby ocenic, czy kandydat stosuje sie do konwencji Go. ## Wykrywanie zakleszczeN: Podchwytliwe pytania rekrutacyjne **P: Czy ten kod spowoduje zakleszczenie? Dlaczego?** Pytania o zakleszczenia sa popularne, poniewaz testuja zdolnosc kandydata do rozumowania o planowaniu goroutines i operacjach na kanalach. ```go // deadlock_example.go 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" } ``` Naprawa jest prosta: nalezy albo uczynic kanal buforowanym (`make(chan int, 1)`), albo uruchomic goroutine do odbioru przed wyslaniem. Runtime Go wykrywa zakleszczenia, gdy wszystkie goroutines sa zablokowane — ale tylko wtedy, gdy *wszystkie* goroutines sa uspione. Jesli chociaz jeden goroutine dziala (np. serwer HTTP w tle), runtime nie wykryje czesciowego zakleszczenia. > **Czesciowe zakleszczenia sa niewidoczne** > > Runtime Go wykrywa zakleszczenia tylko wtedy, gdy kazdy goroutine w programie jest zablokowany. W rzeczywistych aplikacjach z serwerami HTTP lub workerami w tle, wyciekle goroutines w stanie zakleszczenia nie uruchomia detektora runtime. Narzedzia takie jak `pprof` i zrzuty goroutines (`runtime.Stack`) sa niezbedne do diagnozowania tych problemow na produkcji. ## Zaawansowany wzorzec: Przetwarzanie wspolbiezne z ograniczeniem szybkosci **P: Jak zaimplementowac wspolbiezne wywolania API z ograniczeniem szybkosci?** To pytanie testuje umiejetnosc laczenia wielu prymitywow wspolbieznosci w spojne rozwiazanie. ```go // rate_limited.go 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() } ``` Ten wzorzec laczy semafor oparty na kanale (do ograniczania wspolbieznosci) z tickerem (do ograniczania szybkosci). Podwojny `select` ze sprawdzaniem kontekstu zapewnia graceful shutdown. To wlasnie taki produkcyjny poziom odpowiedzi wyroznia seniorowych kandydatow. ## Podsumowanie - Goroutines to watki w przestrzeni uzytkownika zarzadzane przez runtime Go z planowaniem M:N; zawsze nalezy obsługiwac panic w uruchomionych goroutines - Kanaly niebuforowane synchronizuja nadawce i odbiorce; kanaly buforowane rozdzielaja czas — wybor zalezy od tego, czy nadawca potrzebuje potwierdzenia - Instrukcja `select` multipleksuje operacje na kanalach z losowym wyborem, gdy kilka jest gotowych; laczenie z `context.Context` umozliwia obsluge timeoutow - Fan-out/fan-in i pule workerow (przez `errgroup.SetLimit`) to dwa najczesciej pytane wzorce wspolbieznosci - `sync.Mutex` sluzy do zlozonego wspoldzielonego stanu, `sync/atomic` do prostych licznikow, a kanaly do komunikacji miedzy goroutines - Zawsze nalezy uruchamiac `go test -race` w CI do wychwytywania wyscigow danych; czesciowe zakleszczenia wymagaja `pprof` do diagnozy - Nigdy nie nalezy przechowywac `context.Context` w strukturach — nalezy przekazywac go jako pierwszy parametr funkcji - Ograniczanie szybkosci w Go laczy semafory kanalowe z tickerami, opakowane w instrukcje select swiadome kontekstu --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/go/go-technical-interview-goroutines-channels-concurrency