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.

Pidhotovka do tekhnichnoji spivbesidy z Go shchodo goroutines channels ta paterniv konkurentnosti

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).

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")
}

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.

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 {}
}

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.

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
}

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.

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)
}

Krytychnyi moment: kanal maje zakrywaty lyshe widprawnik, nikoly otrymuvach. Zakryttia kanalu, w iakyi inshyi goroutine shche pyshe, sprychnjiuje panic.

Готовий до співбесід з Go?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

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.

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)
}

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.

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)
	}
}

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.

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)
	}
}

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.

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
}

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.

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)
	}
}

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 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.

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"
}

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.

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()
}

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

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

#go
#golang
#interview
#goroutines
#channels
#concurrency

Поділитися

Пов'язані статті