Portal/Notes πŸ“
Interview prep

Go-Specific Interview Questions

Kumpulan soal Go yang sering keluar di interview backend: goroutine leak, race condition, context cancellation, channel patterns, error wrapping β€” lengkap dengan kode dan penjelasan.

1. Goroutine Leak Detection

Soal

"Temukan goroutine leak di kode ini dan perbaiki."

func fetchURLs(urls []string) []string {
    results := []string{}
    for _, url := range urls {
        go func(u string) {
            resp, _ := http.Get(u)
            body, _ := io.ReadAll(resp.Body)
            results = append(results, string(body)) // race condition juga
        }(url)
    }
    return results
}

Masalah

  1. Goroutine leak: main return sebelum goroutine selesai
  2. Race condition: results di-append dari multiple goroutine tanpa mutex
  3. No error handling: resp.Body gak di-close β†’ connection leak
  4. Unbounded concurrency: kalau urls ada 10K, spawn 10K goroutine β†’ DOS diri sendiri

Solusi

func fetchURLs(ctx context.Context, urls []string, maxConcurrency int) ([]string, error) {
    urlsCh := make(chan string, len(urls))
    resultsCh := make(chan fetchResult, len(urls))

    // Fan-out: spawn worker pool
    var wg sync.WaitGroup
    for i := 0; i < maxConcurrency; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for url := range urlsCh {
                resp, err := fetchOne(ctx, url)
                resultsCh <- fetchResult{url: url, body: resp, err: err}
            }
        }()
    }

    // Send work
    for _, url := range urls {
        urlsCh <- url
    }
    close(urlsCh)

    // Wait for workers
    go func() { wg.Wait(); close(resultsCh) }()

    // Collect results
    results := make([]string, 0, len(urls))
    for r := range resultsCh {
        if r.err != nil {
            return nil, r.err
        }
        results = append(results, r.body)
    }

    return results, nil
}

type fetchResult struct {
    url  string
    body string
    err  error
}

func fetchOne(ctx context.Context, url string) (string, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", fmt.Errorf("fetch %s: %w", url, err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", fmt.Errorf("read body %s: %w", url, err)
    }
    return string(body), nil
}

2. Channel Patterns

Fan-Out / Fan-In

// Fan-out: satu input channel β†’ banyak worker
func fanOut(ctx context.Context, input <-chan Task, workers int) []<-chan Result {
    channels := make([]<-chan Result, workers)
    for i := 0; i < workers; i++ {
        channels[i] = worker(ctx, input)
    }
    return channels
}

// Fan-in: banyak channel β†’ satu output channel
func fanIn(ctx context.Context, channels []<-chan Result) <-chan Result {
    output := make(chan Result)
    var wg sync.WaitGroup
    wg.Add(len(channels))

    for _, ch := range channels {
        go func(c <-chan Result) {
            defer wg.Done()
            for r := range c {
                select {
                case output <- r:
                case <-ctx.Done():
                    return
                }
            }
        }(ch)
    }

    go func() { wg.Wait(); close(output) }()
    return output
}

Pipeline Pattern

// Generate numbers β†’ Square β†’ Print
func generator(ctx context.Context, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

3. Context Cancellation & Timeout

Interview Question

"Sebutkan best practices untuk context di Go."

Jawaban

  1. Context selalu jadi parameter PERTAMA: func Process(ctx context.Context, data Data) error
  2. Jangan simpan context di struct β€” pass sebagai parameter
  3. Turunkan context, jangan buat dari context.Background() di dalam function
  4. Selalu defer cancel() setelah context.WithTimeout/WithCancel
  5. Check ctx.Err() di loop untuk early return
func processWithTimeout(ctx context.Context, items []Item) error {
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    for _, item := range items {
        select {
        case <-ctx.Done():
            return fmt.Errorf("process interrupted: %w", ctx.Err())
        default:
            // Process item
            if err := processOne(ctx, item); err != nil {
                return err
            }
        }
    }
    return nil
}

4. Error Wrapping

// βœ… Good: wrap dengan context
func GetUser(ctx context.Context, id string) (*User, error) {
    user, err := db.QueryUser(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("GetUser(%s): %w", id, err)
    }
    return user, nil
}

// βœ… Good: custom error type untuk sentinel checking
var ErrInsufficientBalance = errors.New("insufficient balance")

func Transfer(ctx context.Context, from, to string, amount int64) error {
    balance, err := GetBalance(ctx, from)
    if err != nil {
        return fmt.Errorf("Transfer: get balance: %w", err)
    }
    if balance < amount {
        return fmt.Errorf("Transfer: %w (balance=%d, need=%d)",
            ErrInsufficientBalance, balance, amount)
    }
    // ...
}

// βœ… Good: errors.Is + errors.As
if errors.Is(err, ErrInsufficientBalance) {
    // Handle specific error
}

5. Race Condition Detection

# Run tests with race detector
go test -race ./...

# Run binary with race detector
go build -race && ./binary

Common Race Condition Patterns

// ❌ BAD: closure captures loop variable
for _, item := range items {
    go func() {
        process(item) // item is the SAME variable β€” race!
    }()
}

// βœ… GOOD: pass as parameter
for _, item := range items {
    go func(i Item) {
        process(i)
    }(item)
}

// ❌ BAD: write to map without mutex
var cache = map[string]Result{}
func Set(k string, v Result) {
    cache[k] = v // fatal error: concurrent map writes
}

// βœ… GOOD: sync.Map atau mutex
var cache sync.Map
func Set(k string, v Result) {
    cache.Store(k, v)
}
Edit on GitHub

Last updated on

Go-Specific Interview Questions | Faisal Affan