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.

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.
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).
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.
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.
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.
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.
Siap menguasai wawancara Go Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
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.
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.
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.
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.
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.
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.
Jangan pernah menyimpan context.Context di dalam field struct. Dokumentasi resmi Go 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.
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.
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.
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.
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
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
selectmelakukan multiplexing operasi channel dengan pemilihan acak ketika beberapa case siap; kombinasikan dengancontext.Contextuntuk timeout - Fan-out/fan-in dan worker pool (melalui
errgroup.SetLimit) adalah dua pola concurrency yang paling sering ditanyakan - Gunakan
sync.Mutexuntuk shared state kompleks,sync/atomicuntuk counter sederhana, dan channel untuk komunikasi goroutine - Selalu jalankan
go test -racedi CI untuk menangkap data race; deadlock parsial memerlukanpprofuntuk diagnosa - Jangan simpan
context.Contextdi struct — kirimkan sebagai parameter pertama fungsi - Rate limiting di Go mengombinasikan semaphore channel dengan ticker, dibungkus dalam select statement yang sadar context
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Tag
Bagikan
Artikel terkait

Konkurensi di Go: Goroutine dan Channel - Panduan Lengkap
Kuasai konkurensi di Go dengan goroutine dan channel. Pola lanjutan, sinkronisasi, pernyataan select, dan praktik terbaik dengan contoh kode terperinci.

25 pertanyaan wawancara Go teratas: panduan lengkap developer
Kuasai wawancara Go dengan 25 pertanyaan paling sering ditanyakan. Goroutine, channel, interface, dan pola konkurensi dengan contoh kode.

Go: Dasar-Dasar untuk Developer Java/Python di 2026
Pelajari Go dengan cepat menggunakan pengalaman Java atau Python. Goroutine, channel, interface, dan pola-pola penting untuk transisi yang lancar.