# Wawancara Teknis Go: Goroutine, Channel, dan Concurrency > Pertanyaan wawancara teknis Go tentang goroutine, channel, dan pola concurrency. Contoh kode, jebakan umum, dan jawaban tingkat ahli untuk persiapan wawancara teknis Go pada tahun 2026. - Published: 2026-04-01 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: go, golang, interview, goroutines, channels, concurrency - Reading time: 9 min --- Pertanyaan wawancara Go tentang goroutine, channel, dan concurrency secara konsisten menjadi topik paling menantang yang dihadapi kandidat. Pemahaman mendalam terhadap konsep-konsep ini membedakan engineer Go senior dari mereka yang masih mempelajari bahasa tersebut. Panduan ini membahas pertanyaan yang sering ditanyakan pewawancara pada tahun 2026, dilengkapi contoh kode siap produksi dan penjelasan di balik setiap jawaban. > **Yang sebenarnya diuji pewawancara** > > Wawancara concurrency Go berfokus pada tiga area: manajemen siklus hidup goroutine, semantik channel (buffered vs unbuffered, tipe directional), dan komposisi pola (fan-out/fan-in, worker pool, context cancellation). Menghafal sintaks saja tidak cukup — pewawancara mengharapkan kandidat mampu menganalisis race condition dan deadlock. ## Dasar-Dasar Goroutine: Pertanyaan yang Selalu Ditanyakan Putaran pertama pertanyaan biasanya menguji apakah kandidat memahami apa itu goroutine sebenarnya — bukan sekadar cara membuatnya. **T: Apa itu goroutine, dan apa bedanya dengan OS thread?** Goroutine adalah fungsi concurrent ringan yang dikelola oleh Go runtime scheduler, bukan oleh sistem operasi. Go runtime melakukan multiplexing ribuan goroutine ke sejumlah kecil OS thread menggunakan model penjadwalan M:N (M goroutine dipetakan ke N OS thread). ```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") } ``` Perbedaan utama yang perlu disebutkan dalam wawancara: goroutine dimulai dengan stack 2-8KB yang tumbuh secara dinamis, dibandingkan stack tetap 1-8MB pada OS thread. Context switching antar goroutine ditangani di user space oleh Go scheduler, menghindari context switch tingkat kernel yang mahal pada OS thread. Hal ini membuat pembuatan 100.000 goroutine sangat praktis, sementara 100.000 OS thread akan menghabiskan sumber daya sistem. **T: Apa yang terjadi jika sebuah goroutine mengalami panic?** Panic yang tidak ter-recover pada goroutine mana pun akan menghentikan seluruh program. Berbeda dengan exception di Java atau Python, panic merambat ke atas call stack goroutine itu sendiri — bukan stack goroutine yang membuatnya. Satu-satunya cara menangkapnya adalah dengan `recover()` di dalam deferred function pada goroutine yang sama. ```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 {} } ``` Pewawancara mencari kesadaran bahwa layanan Go produksi membungkus peluncuran goroutine dalam pola recovery. Library seperti `errgroup` menangani hal ini dengan lebih elegan. ## Semantik Channel: Buffered, Unbuffered, dan Directional Pertanyaan tentang channel mengungkapkan apakah kandidat benar-benar memahami model concurrency Go atau hanya menghafal pola. **T: Apa perbedaan antara buffered dan unbuffered channel?** Unbuffered channel (`make(chan T)`) mengharuskan pengirim dan penerima siap secara bersamaan — pengiriman memblokir sampai goroutine lain menerima. Buffered channel (`make(chan T, n)`) mengizinkan hingga `n` nilai dikirim tanpa memblokir. ```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 } ``` Pertanyaan lanjutan yang sering diajukan pewawancara: "Kapan memilih salah satu di antara keduanya?" Unbuffered channel memaksakan sinkronisasi — berguna ketika pengirim harus mengetahui bahwa penerima telah memproses nilai. Buffered channel memisahkan waktu pengirim dan penerima — berguna untuk work queue atau rate limiting di mana toleransi tertentu dapat diterima. **T: Apa yang terjadi ketika channel ditutup?** Menutup channel menandakan bahwa tidak ada lagi nilai yang akan dikirim. Operasi receive pada channel yang sudah ditutup langsung mengembalikan zero value. Mengirim ke channel yang sudah ditutup menyebabkan panic. Loop `range` pada channel berhenti ketika channel ditutup. ```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) } ``` Poin kritis: hanya pengirim yang boleh menutup channel, tidak pernah penerima. Menutup channel yang masih ditulis oleh goroutine lain menyebabkan panic. ## Pernyataan Select: Multiplexing Channel **T: Bagaimana `select` bekerja, dan apa yang terjadi ketika beberapa case siap bersamaan?** Pernyataan `select` memblokir sampai salah satu operasi channel dapat dilanjutkan. Ketika beberapa case siap secara bersamaan, Go memilih secara acak — ini mencegah starvation pada case tertentu. ```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) } ``` Pewawancara menguji dua hal dengan `select`: pemahaman tentang aturan pemilihan acak, dan kemampuan mengombinasikan channel dengan `context.Context` untuk pola timeout dan pembatalan. ## Pola Concurrency Umum dalam Wawancara Wawancara Go tingkat senior hampir selalu menyertakan pertanyaan tentang implementasi salah satu pola berikut dari awal. ### Pola Fan-Out/Fan-In **T: Implementasikan pipeline fan-out/fan-in yang memproses item secara concurrent.** Fan-out mendistribusikan pekerjaan ke beberapa goroutine. Fan-in mengumpulkan hasil dari beberapa goroutine ke satu channel. ```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) } } ``` Insight utama yang dicari pewawancara: channel `generator` dibagi antara `c1` dan `c2`, sehingga setiap nilai diproses oleh tepat satu worker (tidak diduplikasi). Fungsi `fanIn` menggunakan `WaitGroup` untuk mengetahui kapan semua input channel telah habis sebelum menutup channel gabungan. ### Worker Pool dengan errgroup **T: Bagaimana cara mengimplementasikan bounded worker pool dengan penanganan error?** Package `golang.org/x/sync/errgroup` (bagian dari Go extended standard library) menyelesaikan masalah ini secara elegan. Package ini mengelola siklus hidup goroutine, mengumpulkan error pertama, dan terintegrasi dengan `context` untuk pembatalan. ```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) } } ``` Sejak Go 1.24 (rilis stabil terbaru pada awal 2026), pola ini tetap menjadi pendekatan yang direkomendasikan. Metode `SetLimit` ditambahkan pada Go 1.20 dan menghilangkan kebutuhan untuk mengimplementasikan pembatasan concurrency berbasis semaphore secara manual. ## Race Condition dan Primitif sync **T: Bagaimana cara mendeteksi dan mencegah race condition di Go?** Go menyediakan race detector bawaan yang diaktifkan dengan flag `-race`. Alat ini mendeteksi akses concurrent tidak tersinkronisasi ke shared memory pada saat runtime. ```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 } ``` Jawaban wawancara harus mencakup tiga strategi sinkronisasi: `sync.Mutex` / `sync.RWMutex` untuk shared state yang kompleks, `sync/atomic` untuk counter dan flag sederhana, dan channel untuk komunikasi antar goroutine ("share memory by communicating, don't communicate by sharing memory"). Menjalankan `go test -race ./...` harus menjadi bagian dari setiap CI pipeline. ## Context dan Pola Pembatalan **T: Jelaskan bagaimana context.Context mengontrol siklus hidup goroutine.** Package `context` menyediakan mekanisme untuk menyebarkan sinyal pembatalan, deadline, dan nilai berskala request ke seluruh batas goroutine. Setiap goroutine yang berjalan lama harus menerima `context.Context` sebagai parameter pertamanya. ```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) } } ``` Worker 1 dan 2 selesai dalam deadline 250ms. Worker 3, 4, dan 5 menerima sinyal pembatalan melalui `ctx.Done()`. Pola ini fundamental untuk membangun HTTP server dan microservice yang tangguh di Go — setiap request handler menerima context yang menyebarkan pembatalan ketika klien terputus. > **Jebakan umum dalam wawancara** > > Jangan pernah menyimpan `context.Context` di dalam field struct. [Dokumentasi resmi Go](https://pkg.go.dev/context) secara eksplisit menyatakan: "Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it." Pewawancara menguji hal ini untuk mengukur apakah kandidat mengikuti konvensi Go. ## Deteksi Deadlock: Pertanyaan Wawancara yang Menjebak **T: Apakah kode ini akan deadlock? Mengapa?** Pertanyaan deadlock populer karena menguji kemampuan kandidat menganalisis penjadwalan goroutine dan operasi channel. ```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" } ``` Solusinya sederhana: buat channel menjadi buffered (`make(chan int, 1)`) atau buat goroutine untuk menerima sebelum mengirim. Go runtime mendeteksi deadlock ketika semua goroutine terblokir — tetapi hanya ketika *semua* goroutine tertidur. Jika bahkan satu goroutine masih berjalan (misalnya, HTTP server background), runtime tidak akan mendeteksi deadlock parsial. > **Deadlock parsial tidak terdeteksi** > > Go runtime hanya mendeteksi deadlock ketika setiap goroutine dalam program terblokir. Pada aplikasi nyata dengan HTTP server atau background worker, goroutine yang bocor dan mengalami deadlock tidak akan memicu detektor runtime. Tool seperti `pprof` dan goroutine dump (`runtime.Stack`) diperlukan untuk mendiagnosis masalah ini di produksi. ## Pola Lanjutan: Pemrosesan Concurrent dengan Rate Limiting **T: Bagaimana cara mengimplementasikan rate-limited concurrent API call?** Pertanyaan ini menguji kemampuan mengombinasikan beberapa primitif concurrency menjadi solusi yang kohesif. ```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() } ``` Pola ini mengombinasikan semaphore berbasis channel (untuk pembatasan concurrency) dengan ticker (untuk rate limiting). Double `select` dengan pengecekan context memastikan shutdown yang graceful. Jawaban siap produksi seperti inilah yang membedakan kandidat senior. ## Kesimpulan - Goroutine adalah thread user-space yang dikelola Go runtime dengan penjadwalan M:N; selalu recover panic pada goroutine yang di-spawn - Unbuffered channel mensinkronisasi pengirim dan penerima; buffered channel memisahkan waktu — pilih berdasarkan apakah pengirim memerlukan konfirmasi - Pernyataan `select` melakukan multiplexing operasi channel dengan pemilihan acak ketika beberapa case siap; kombinasikan dengan `context.Context` untuk timeout - Fan-out/fan-in dan worker pool (melalui `errgroup.SetLimit`) adalah dua pola concurrency yang paling sering ditanyakan - Gunakan `sync.Mutex` untuk shared state kompleks, `sync/atomic` untuk counter sederhana, dan channel untuk komunikasi goroutine - Selalu jalankan `go test -race` di CI untuk menangkap data race; deadlock parsial memerlukan `pprof` untuk diagnosa - Jangan simpan `context.Context` di struct — kirimkan sebagai parameter pertama fungsi - Rate limiting di Go mengombinasikan semaphore channel dengan ticker, dibungkus dalam select statement yang sadar context --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/go/go-technical-interview-goroutines-channels-concurrency