# Tekhnichna spivbesida z Go: Goroutines, Channels ta Concurrency > Pytannia na spivbesidi z Go shchodo goroutines, channels ta paterniw konkurentnosti. Pryklady kodu, typowi pastky ta ekspertni widpovidi dlia pidhotovky do tekhnichnykh spivbesid z Go u 2026 rotsi. - Published: 2026-04-01 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: go, golang, interview, goroutines, channels, concurrency - Reading time: 9 min --- Pytannia pro goroutines, channels ta konkurentnist u Go stabilno zalyshaiutsia sered naiskladnishykh tem, z iakymy stykaiutsia kandydaty na spivbesidakh. Hlyboke rozuminnia tsykh kontseptsii widrizniaje seniornykh inzheneriv Go vid tykh, khto shche vyvchaje movu. Tsei posibnyk okhopliuje same ti pytannia, iaki rekrutery stavliat u 2026 rotsi, z vyrobnychymy prykladamy kodu ta obgruntuvanniam kozhnoji widpovidi. > **Shcho nasrawdi testujut rekrutery** > > Spivbesidy z konkurentnosti Go zoseredzheni na triokh oblastiakh: keruvannia zhyttievym tsyklom goroutines, semantyka kanaliw (buferowani vs nebuferowani, napriamkowi typy) ta kompozytsiia paterniv (fan-out/fan-in, puly workeriv, skasuvannia kontekstu). Zapamiatovuvannia syntaksysu nedostatnio — rekrutery ochikujut, shcho kandydaty zmozhut mirkuvaty pro perehony danykh ta vzajemni blokuvannia. ## Osnovy Goroutines: Shcho pytaje kozhen rekruter Pershyi raund pytan zazwychaj perevirjaje, chy kandydat rozumije, chym goroutines nasrawdi je — a ne lyshe jak jikh zapuskaty. **P: Shcho take goroutine i chym win widrizniajetsia wid potoku operatsijnoji systemy?** Goroutine — tse lehka konkurentna funktsiia, iakoiu keruje planuwalnyk runtime Go, a ne operatsijna systema. Runtime Go multypleksuje tysiachi goroutines na newelyku kilkist potokiw OS za dopomohoiu modeli planuvannia M:N (M goroutines widobrazhajutsia na N potokiw 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") } ``` Kliuchowi widminnosti, iaki slid zhadaty na spivbesidi: goroutines startujut zi stekom 2-8KB, iakyi dynamichno zrostaje, na widminu wid fiksowoho steku 1-8MB potoku OS. Peremykannia kontekstu mizh goroutines widbuwajetsja u prostorowij korystuwacha planuwalnikom Go, shcho unykaje koshtovnoho peremykannia na riwni jadra systemy. Tse robyt zapusk 100 000 goroutines praktychnym, todi jak 100 000 potokiw OS vycherpaly b systemni resursy. **P: Shcho widbuwajetsja, koly goroutine vyklyche panic?** Neobroblenyi panic u bud-iakomu goroutine sprychnjiaje crash usijeji prohramy. Na widminu wid vykliuchenʼ u Java chy Python, panic poshyrjujetsia whoru po steku wyklykiw swogo goroutine — a ne steku goroutine, iakyi joho zapustyv. Jedynym sposobom perkhopyty tse je `recover()` u seredyni widkladenoji funktsiji w tomu zh 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 {} } ``` Rekrutery shukajut uswidomlennia toho, shcho vyrobnychi servisy Go obhortajut zapusky goroutines u patern widnowlennia. Biblioteky na kshtalt `errgroup` obrobliajut tse bilsh elehantno. ## Semantyka Kanaliw: Buferowani, Nebuferowani ta Napriamkowi Pytannia pro kanaly wyjavliajut, chy kandydat dijsno rozumije model konkurentnosti Go, chy prosto zapamiatovuje paterny. **P: Iaka riznytsia mizh buferowanym i nebuferowanym kanalom?** Nebuferowanyi kanal (`make(chan T)`) vymaghaje, shchob widprawnik i otrymuvach buly hotowi odnochasno — widsylannia blokuje, doky inshyi goroutine ne otrymaje znachennia. Buferowanyi kanal (`make(chan T, n)`) dozwoliaje widisylaty do `n` znachen bez blokuvannia. ```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 } ``` Chastu dodatkowe pytannia: "Koly wybraty odne abo inshe?" Nebuferowani kanaly zabezpechujut synkhronizatsiiu — korysni, koly widprawnik maje znaty, shcho otrymuvach obrobyv znachennia. Buferowani kanaly rozdialiajut chas widprawnika ta otrymuwcha — korysni dlia cherh zaavdanh abo obmezhennia shwydkosti, de pewnyi zapas pryjniatnyi. **P: Shcho widbuwajetsja, koly kanal zakrywajetsja?** Zakryttia kanalu syghnalizuje, shcho bilshe znachen ne bude widsilano. Otrymuwannia z zakrytoho kanalu negajno povertaje nulowe znachennia. Widsylannia na zakrytyi kanal sprychniuje panic. Tsykl `range` po kanalu zawershajetsja, koly kanal zakrywajetsja. ```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) } ``` Krytychnyi moment: kanal maje zakrywaty lyshe widprawnik, nikoly otrymuvach. Zakryttia kanalu, w iakyi inshyi goroutine shche pyshe, sprychnjiuje panic. ## Instruktsija Select: Multypleksuvannia Kanaliw **P: Iak pratsiuje `select` i shcho widbuwajetsja, koly kilka wypadkiw hotowi odnochasno?** Instruktsija `select` blokuje, doky odna z operatsij na kanali ne zmozhe buty wykonana. Koly kilka wypadkiw hotowi odnochasno, Go obyraje odyn wypadkovo — tse zapobihaje holoduwanniu bud-iakoho okremogo wypadku. ```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) } ``` Rekrutery testujut dvi rechi za dopomohoiu `select`: rozuminnia prawyla wypadkowoho wyboru ta zdatnist pojednuwaty kanaly z `context.Context` dlia paterniv tajm-autiw ta skasuvannia. ## Populiarni paterny konkurentnosti na spivbesidakh Spivbesidy seniornoho riwnia z Go maizhe zawzhdy wkliuchajut pytannia pro implementatsiiu odnoho z tsykh paterniv z nulia. ### Patern Fan-Out/Fan-In **P: Implementujte pipeline fan-out/fan-in, iakyi obrobliaje elementy konkurentno.** Fan-out rozpodijiaje robotu mizh kilkoma goroutines. Fan-in zbyraje rezultaty z kilkokh goroutines u odyn kanal. ```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) } } ``` Kliuchowyi wysnowok, iakoho ochikujut rekrutery: kanal `generator` spilnyi dlia `c1` i `c2`, tozh kozhne znachennia obrobliajetsja rivno odnym workerom (ne dybljujetsia). Funktsiia `fanIn` wykorystowuje `WaitGroup`, shchob znaty, koly wsi wkhidni kanaly sporizhneni, pered zakryttiam objedanoho kanalu. ### Pul Workeriv z errgroup **P: Iak implementuwaty obmezhenyj pul workeriv z obrobkoiu pomylok?** Paket `golang.org/x/sync/errgroup` (chastyna rozshyrenoji standartnoji biblioteky Go) rozviazuje tse chysto. Win keruje zhyttievym tsyklom goroutines, zbyraje pershu pomylku ta integrujetsja z `context` dlia skasuvannia. ```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) } } ``` Stanom na Go 1.24 (potochna stabilna wersiia na pochatok 2026 roku) tsei patern zalyshajetsja rekomendowanym pidkhodom. Metod `SetLimit` buv dodanyi u Go 1.20 i usuwaje potrebu w ruchnij implementatsiji obmezhennia konkurentnosti na osnowi semaforiv. ## Perehony danykh ta prymitywy sync **P: Iak wyjavliaty ta zapobihaty perehony danykh u Go?** Go nadaje wbudowanyi detektor perehoniw, iakyi aktywujetsja flazhkom `-race`. Win wyjavliaje nesynkhronizowanyi odnochasnyi dostup do spilnoji pamiati pid chas wykonannia. ```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 } ``` Widpowid na spivbesidi maje okhopliuvaty try stratehiji synkhronizatsiji: `sync.Mutex` / `sync.RWMutex` dlia skladnoho spilnoho stanu, `sync/atomic` dlia prostykh lichylnykiw ta flazhkiw, ta kanaly dlia komunikatsiji mizh goroutines ("dilitsia pamiattiiu cherez komunikatsiiu, a ne komunikuwaty cherez spilnu pamiat"). Zapusk `go test -race ./...` maje buty chastynoju kozhnoho CI pipeline. ## Kontekst ta paterny skasuvannia **P: Iak context.Context keruje zhyttievym tsyklom goroutines?** Paket `context` nadaje mekhanizm poshyrennia syhnaliw skasuvannia, dedlajniw ta znachen oblasti zapytu cherez mezhi goroutines. Kozhna dowhotrywala goroutine powynna pryjmaty `context.Context` iak swij pershyi 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 zawershujutsia w mezhakh dedlajnu 250ms. Workery 3, 4 i 5 otrymujut syhnal skasuvannia cherez `ctx.Done()`. Tsei patern je fundamentalnym dlia pobudowy stijkykh HTTP-serweriv ta mikroservisiv u Go — kozhen obrobnik zapytiv otrymuje kontekst, iakyi poshyrjuje skasuvannia, koly klijent widkliuchajetsja. > **Typova pastka na spivbesidi** > > Nikoly ne slid zberihaty `context.Context` u poli struktury. [Ofitsijna dokumentatsiia Go](https://pkg.go.dev/context) chisko stverdzuje: "Ne zberihaite konteksty u seredyni typiw struktur; natomist peredavaite kontekst jawno kozhnij funktsiji, iaka joho potrebuje." Rekrutery testujut tse, shchob otsinty, chy dotrymujetsia kandydat konwentsij Go. ## Vyjavlennia vzajemnykh blokuwan: Pidstupni pytannia na spivbesidi **P: Chy tsei kod sprychnyt vzajemne blokuvannia? Chomu?** Pytannia pro vzajemni blokuvannia populiarni, bo testujut zdatnist kandydata mirkuwaty pro planuvannia goroutines ta operatsiji z kanalamy. ```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" } ``` Vyrishhennia proste: abo zrobyty kanal buferowanym (`make(chan int, 1)`), abo zapustyty goroutine dlia otrymuwannia pered widsylanniam. Runtime Go wyjavliaje vzajemni blokuvannia, koly wsi goroutines zablokwani — ale lyshe koly *wsi* goroutines spliat. Yakshcho khoch odna goroutine pratsiuje (napryklad, fonovyi HTTP-serwer), runtime ne vyjavyt chastkowoho blokuvannia. > **Chastkowi vzajemni blokuvannia newidymi** > > Runtime Go wyjavliaje vzajemni blokuvannia lyshe koly kozhna goroutine u prohrami zablokowana. U realnykh zastosunkakh z HTTP-serveramy abo fonovymy workeramy, vytikli goroutines u stani blokuvannia ne zapustiut detektor runtime. Instrumenty na kshtalt `pprof` ta dampy goroutines (`runtime.Stack`) neobkhidni dlia diahnostyky tsykh problem na prodaksheni. ## Prosunutyj patern: Konkurentna obrobka z obmezhenniam shwydkosti **P: Iak implementuwaty konkurentni API-wyklyky z obmezhenniam shwydkosti?** Tse pytannia testuje zdatnist pojednuwaty kilka prymitywiw konkurentnosti u tsilisnye rishennia. ```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() } ``` Tsei patern pojednuje kanalnyj semafor (dlia obmezhennia konkurentnosti) z tikerom (dlia obmezhennia shwydkosti). Podwijnyj `select` z perevirkoiu kontekstu zabezpechuje graceful shutdown. Same takyi wyrobnychyj riwen widpovidi wyriznijaje seniornykh kandydatiw. ## Wysnovok - Goroutines — tse potoki u prostori korystuwacha, iakymy keruje runtime Go z planuwanniam M:N; zawzhdy slid obrobliaty panic u zapushchenykh goroutines - Nebuferowani kanaly synkhronizujut widprawnika i otrymuwcha; buferowani kanaly rozdialiajut chas — wibir zalezhyt wid toho, chy widprawnik potrebuje pidtwerdzhhennia - Instruktsija `select` multypleksuje operatsiji z kanalamy z wypadkowym wyborom, koly kilka hotowi; pojednujetsia z `context.Context` dlia tajm-autiw - Fan-out/fan-in ta puly workeriv (cherez `errgroup.SetLimit`) — dwa paterniv konkurentnosti, pro iaki pytajut najchastishe - `sync.Mutex` dlia skladnoho spilnoho stanu, `sync/atomic` dlia prostykh lichylnykiw, kanaly dlia komunikatsiji mizh goroutines - Zawzhdy slid zapuskaty `go test -race` u CI dlia wyjavlennia perehoniw danykh; chastkowi vzajemni blokuvannia potrebujut `pprof` dlia diahnostyky - Nikoly ne slid zberihaty `context.Context` u strukturakh — slid peredawaty joho iak pershyi parametr funktsiji - Obmezhennia shwydkosti u Go pojednuje kanalni semafory z tikeramy, obhornutymy u instruktsiji select, shcho wrakhovujut kontekst --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/uk/blog/go/go-technical-interview-goroutines-channels-concurrency