Go Teknik Mulakat: Goroutine'lar, Channel'lar ve Concurrency

Goroutine'lar, channel'lar ve eszamanlilik kaliplari uzerine Go mulakat sorulari. Kod ornekleri, yaygin tuzaklar ve 2026 yilinda Go teknik mulakatina hazirlanmak icin uzman duzeyinde cevaplar.

Goroutine'lar channel'lar ve esanlilik kaliplarini kapsayan Go teknik mulakat hazirlik rehberi

Go mulakatlarinda goroutine'lar, channel'lar ve eszamanlilik konulari surekli olarak adaylarin karsilastigi en zorlu konular arasinda yer almaktadir. Bu kavramlarin derinlemesine anlasilmasi, kidemli Go muhendislerini dili hala ogrenenlelerden ayiran temel farktir. Bu rehber, 2026 yilinda mulakat yapanlarin sordugu sorulari, uretim kalitesinde kod ornekleri ve her cevabin arkasindaki mantikla birlikte ele almaktadir.

Mulakat yapanlar aslinda neyi test eder

Go eszamanlilik mulakatlari uc alana odaklanir: goroutine yasam dongusu yonetimi, channel semantigi (tamponlu ve tamponsuz, yonlu tipler) ve kalip kompozisyonu (fan-out/fan-in, worker havuzlari, context iptali). Sozdizimini ezberlemek yeterli degildir — mulakat yapanlar adaylarin veri yarislari ve kilitlenmeler hakkinda muhakeme yapmasini bekler.

Goroutine Temelleri: Her Mulakat Yapanin Sordugu Sorular

Ilk tur sorular genellikle adayin goroutine'larin gercekte ne oldugunu anlayip anlamadigini arastirir — sadece nasil baslatildigini degil.

S: Goroutine nedir ve bir isletim sistemi thread'inden nasil farklidir?

Goroutine, isletim sistemi tarafindan degil, Go runtime zamanlayicisi tarafindan yonetilen hafif bir eszamanli fonksiyondur. Go runtime, M:N zamanlama modeli kullanarak binlerce goroutine'i az sayida OS thread'i uzerine coklar (M goroutine, N OS thread'ine eslenir).

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

Mulakatta belirtilmesi gereken temel farklar: goroutine'lar dinamik olarak buyuyen 2-8KB yiginla baslar, OS thread'inin sabit 1-8MB yigininin aksine. Goroutine'lar arasi baglam degisimi, OS thread'lerinin pahali cekirdek duzeyindeki baglam degisimlerinden kacinarak, Go zamanlayicisi tarafindan kullanici alaninda gerceklestirilir. Bu, 100.000 goroutine baslatmayi pratik kilarken, 100.000 OS thread'i sistem kaynaklarini tukertirdi.

S: Bir goroutine panic yaptiginda ne olur?

Herhangi bir goroutine'daki yakalanmamis panic tum programi cokerttir. Java veya Python'daki istisnalarin aksine, panic goroutine'in kendi cagri yigini uzerinden yukari yayilir — onu baslatan goroutine'in yigini uzerinden degil. Yakamanin tek yolu, ayni goroutine icindeki ertelenmis bir fonksiyonda recover() kullanmaktir.

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

Mulakat yapanlar, uretim Go servislerinin goroutine baslatmalarini bir recovery kalibina sardigi bilincini arar. errgroup gibi kutuphaneler bunu daha zarif bir sekilde ele alir.

Channel Semantigi: Tamponlu, Tamponsuz ve Yonlu

Channel sorulari, adayin Go'nun eszamanlilik modelini gercekten anlayip anlamadigini veya sadece kaliplari ezberleyip ezberlemedigini ortaya cikarir.

S: Tamponlu ve tamponsuz channel arasindaki fark nedir?

Tamponsuz channel (make(chan T)), gonderici ve alicinin ayni anda hazir olmasini gerektirir — gonderme, baska bir goroutine alana kadar bloklanir. Tamponlu channel (make(chan T, n)), bloklanmadan n adede kadar deger gonderilmesine izin verir.

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
}

Mulakat yapanlarin sik sordugu takip sorusu: "Hangisini ne zaman tercih edersiniz?" Tamponsuz channel'lar senkronizasyonu zorunlu kilar — gondeicinin alicinin degeri isledigini bilmesi gerektiginde faydalidir. Tamponlu channel'lar gonderici ve alici zamanlamasini ayirir — bazi esnemelerin kabul edilebildigi is kuyruklerinde veya hiz sinirlamada faydalidir.

S: Bir channel kapatildiginda ne olur?

Bir channel'in kapatilmasi, baska deger gonderilmeyecegini sinyal eder. Kapatilmis channel'dan alma islemleri hemen sifir deger dondurur. Kapatilmis channel'a gonderme panic'e neden olur. Channel uzerindeki range dongusu, channel kapatildiginda cikar.

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

Kritik nokta: bir channel'i yalnizca gonderici kapatmalidir, asla alici degil. Baska bir goroutine'in hala yazdigi bir channel'i kapatmak panic'e neden olur.

Go mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Select Ifadesi: Channel'lari Coklamalama

S: select nasil calisir ve birden fazla durum ayni anda hazir oldugunda ne olur?

select ifadesi, channel islemlerinden biri gerceklesene kadar bloklanir. Birden fazla durum ayni anda hazir oldugunda, Go rastgele birini secer — bu, herhangi bir durumun acliktan olmasini onler.

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

Mulakat yapanlar select ile iki seyi test eder: rastgele secim kuralinin anlasilmasi ve channel'larin zaman asimi ve iptal kaliplari icin context.Context ile birlestirilme becerisi.

Mulakatlarda Sorulan Yaygin Eszamanlilik Kaliplari

Kidemli duzey Go mulakatlari neredeyse her zaman bu kaliplardan birinin sifirdan uygulanmasi sorusunu icerir.

Fan-Out/Fan-In Kalibi

S: Elemanlari eszamanli olarak isleyen bir fan-out/fan-in pipeline uygulama.

Fan-out, isi birden fazla goroutine'a dagitir. Fan-in, birden fazla goroutine'dan gelen sonuclari tek bir channel'da toplar.

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

Mulakat yapanlarin istedigi temel fikir: generator channel'i c1 ve c2 arasinda paylasiliyor, dolayisiyla her deger tam olarak bir worker tarafindan islenir (cogaltilmaz). fanIn fonksiyonu, birlestirilen channel'i kapatmadan once tum giris channel'larinin bosaltildigini bilmek icin WaitGroup kullanir.

errgroup ile Worker Havuzu

S: Hata isleme ile sinirli bir worker havuzunu nasil uygularsiniz?

golang.org/x/sync/errgroup paketi (Go genisletilmis standart kutuphanesinin parcasi) bunu temiz bir sekilde cozer. Goroutine yasam dongularini yonetir, ilk hatayi toplar ve iptal icin context ile entegre olur.

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

Go 1.24 itibariyle (2026 basinda guncel kararli surum) bu kalip onerilen yaklasim olmaya devam etmektedir. SetLimit metodu Go 1.20'de eklenmistir ve semafor tabanli eszamanlilik sinirlamasini elle uygulamaya olan ihtiyaci ortadan kaldirir.

Veri Yarislari ve sync Primitifleri

S: Go'da veri yarislarini nasil tespit eder ve onlersiniz?

Go, -race flagiyla etkinlestirilen yerlesik bir yaris detektoru saglar. Calisma zamaninda paylasilan bellige senkronize edilmemis esanli erisimi tespit eder.

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
}

Mulakat cevabi uc senkronizasyon stratejisini kapsamalidir: karmasik paylasilan durum icin sync.Mutex / sync.RWMutex, basit sayiclar ve flaglar icin sync/atomic ve goroutine'lar arasi iletisim icin channel'lar ("bellegi iletisim kurarak paylasin, paylasarak iletisim kurmayin"). go test -race ./... komutunun her CI pipeline'inda calistirilmasi gerekir.

Context ve Iptal Kaliplari

S: context.Context goroutine yasam dongusunu nasil kontrol eder?

context paketi, goroutine sinirlari arasinda iptal sinyallerini, son tarihleri ve istek kapsamli degerleri yaymak icin bir mekanizma saglar. Her uzun sureli goroutine, ilk parametresi olarak bir context.Context kabul etmelidir.

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

Worker 1 ve 2, 250ms son tarih icinde tamamlanir. Worker 3, 4 ve 5, iptal sinyalini ctx.Done() araciligiyla alir. Bu kalip, Go'da dayanikli HTTP sunuculari ve mikro servisler olusturmanin temelidir — her istek handler'i, istemci baglantiyi kestiginde iptali yayan bir context alir.

Yaygin mulakat tuzagi

Bir context.Context asla bir struct alaninda saklanmamalidir. Resmi Go dokumantasyonu acikca belirtir: "Kontekstleri struct tipleri icinde saklamayin; bunun yerine, ihtiyaci olan her fonksiyona acikca bir Kontekst iletin." Mulakat yapanlar bunu, adayin Go konvansiyonlarina uyup uymadigini olcmek icin test eder.

Kilitlenme Tespiti: Zor Mulakat Sorulari

S: Bu kod kilitlenecek mi? Neden?

Kilitlenme sorulari populerdir cunku adayin goroutine zamanlamasi ve channel islemleri hakkinda muhakeme yetenegini test eder.

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

Cozum basittir: ya channel'i tamponlu yapmak (make(chan int, 1)) ya da gondermeden once almak icin bir goroutine baslatmak. Go runtime, tum goroutine'lar bloklandiginda kilitlenmeleri tespit eder — ancak yalnizca tum goroutine'lar uykudayken. Tek bir goroutine bile calisiyorsa (ornegin arka plan HTTP sunucusu), runtime kismi kilitlenmeyi tespit edemez.

Kismi kilitlenmeler gorunmezdir

Go runtime, kilitlenmeleri yalnizca programdaki her goroutine bloklandiginda tespit eder. HTTP sunuculari veya arka plan worker'lari olan gercek uygulamalarda, kilitlenmis durumda olan sizdirilmis goroutine'lar runtime detektorunu tetiklemez. pprof ve goroutine dump'lari (runtime.Stack) gibi araclar, uretimde bu sorunlari teshis etmek icin gereklidir.

Ileri Duzey Kalip: Hiz Sinirli Esanli Isleme

S: Hiz sinirli esanli API cagrilarini nasil uygularsiniz?

Bu soru, birden fazla esanlilik primitivini tutarli bir cozumde birlestirme becerisini test eder.

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

Bu kalip, channel tabanli bir semaforu (esanlilik sinirlamasi icin) bir ticker ile (hiz sinirlamasi icin) birlestirir. Context kontrollu cift select, graceful shutdown saglar. Bu, kidemli adaylari ayiran uretim kalitesinde cevap turudur.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Sonuc

  • Goroutine'lar, M:N zamanlama ile Go runtime tarafindan yonetilen kullanici alani thread'leridir; baslatilan goroutine'larda her zaman panic'ler yakalanmalidir
  • Tamponsuz channel'lar gonderici ve aliciyi senkronize eder; tamponlu channel'lar zamanlamayi ayirir — gondeicinin onay ihtiyacina gore secim yapilir
  • select ifadesi, birden fazla hazir oldugunda rastgele secimle channel islemlerini coklar; zaman asimlari icin context.Context ile birlestirilir
  • Fan-out/fan-in ve worker havuzlari (errgroup.SetLimit ile) en sik sorulan iki esanlilik kalibidir
  • Karmasik paylasilan durum icin sync.Mutex, basit sayiclar icin sync/atomic, goroutine iletisimi icin channel'lar kullanilir
  • Veri yarislarini yakalamak icin CI'da her zaman go test -race calistirilmalidir; kismi kilitlenmeler teshis icin pprof gerektirir
  • context.Context asla struct'larda saklanmamalidir — ilk fonksiyon parametresi olarak iletilmelidir
  • Go'da hiz sinirlamasi, context-duyarli select ifadelerine sarili channel semaforlari ile ticker'lari birlestirir

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Etiketler

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

Paylaş

İlgili makaleler