Sync #

Goroutine membuat concurrency di Go terasa mudah — tapi begitu dua goroutine mengakses data yang sama secara bersamaan, tanpa koordinasi yang tepat, hasilnya tidak bisa diprediksi. Package sync menyediakan primitif sinkronisasi dasar untuk situasi ini: Mutex untuk memastikan hanya satu goroutine yang mengakses data kritis pada satu waktu, RWMutex untuk membedakan antara pembaca dan penulis, WaitGroup untuk menunggu sekelompok goroutine selesai, Once untuk inisialisasi yang dijamin hanya terjadi sekali, Pool untuk mendaur ulang objek dan mengurangi tekanan pada garbage collector, dan Map untuk concurrent map yang aman. Memahami kapan dan bagaimana menggunakan setiap primitif ini adalah perbedaan antara program concurrent yang benar dan yang penuh race condition.

Gambaran Besar Package sync #

flowchart TD
    Sync["package sync"] --> Mutex["Mutex\nExclusive lock\nsatu goroutine pada satu waktu"]
    Sync --> RWMutex["RWMutex\nRead-Write lock\nbanyak reader atau satu writer"]
    Sync --> WaitGroup["WaitGroup\nTunggu N goroutine selesai"]
    Sync --> Once["Once\nJalankan fungsi tepat sekali"]
    Sync --> Pool["Pool\nRecycle objek, kurangi GC pressure"]
    Sync --> Map["Map\nConcurrent-safe map"]
    Sync --> Cond["Cond\nCondition variable\nnotifikasi antar goroutine"]

    Mutex --> M1["Lock() / Unlock()\nselalu defer Unlock()"]
    RWMutex --> RW1["Lock() / Unlock()\nuntuk write"]
    RWMutex --> RW2["RLock() / RUnlock()\nuntuk read"]
    WaitGroup --> WG1["Add(n) / Done() / Wait()"]
    Once --> O1["Do(func())\nhanya pertama kali dijalankan"]
    Pool --> P1["Get() / Put()\nrecycle objek mahal"]
    Map --> SM1["Store / Load / Delete\nLoadOrStore / Range"]

    style Sync fill:#4f86c6,color:#fff
    style Mutex fill:#e8f5e9
    style RWMutex fill:#e3f2fd
    style WaitGroup fill:#fff3e0
    style Once fill:#f3e5f5
    style Pool fill:#fce4ec
    style Map fill:#e0f7fa

Mutex — Mutual Exclusion #

sync.Mutex memastikan hanya satu goroutine yang bisa mengeksekusi critical section — bagian kode yang mengakses data bersama — pada satu waktu. Goroutine lain yang mencoba Lock() akan blokir sampai Mutex dibebaskan.

package main

import (
    "fmt"
    "sync"
)

// ANTI-PATTERN: counter tanpa sinkronisasi — race condition!
type CounterTidakAman struct {
    nilai int
}

func (c *CounterTidakAman) Tambah() {
    c.nilai++ // baca-modifikasi-tulis tidak atomic!
}

// BENAR: counter dengan Mutex
type Counter struct {
    mu    sync.Mutex
    nilai int
}

func (c *Counter) Tambah() {
    c.mu.Lock()
    defer c.mu.Unlock() // SELALU defer Unlock — mencegah lupa unlock saat panic
    c.nilai++
}

func (c *Counter) Kurang() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.nilai--
}

func (c *Counter) Nilai() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.nilai
}

func main() {
    counter := &Counter{}
    var wg sync.WaitGroup

    // 1000 goroutine menambah counter secara bersamaan
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Tambah()
        }()
    }

    wg.Wait()
    fmt.Println("Nilai akhir:", counter.Nilai()) // selalu 1000
}

Pola: Struct dengan Mutex Tertanam #

Konvensi di Go adalah menempatkan Mutex tepat di atas field yang dilindunginya, dan tidak mengekspor Mutex:

type Cache struct {
    mu   sync.Mutex      // melindungi field di bawah ini
    data map[string]string
    hits int
    misses int
}

func NewCache() *Cache {
    return &Cache{
        data: make(map[string]string),
    }
}

func (c *Cache) Set(key, val string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = val
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.Lock()
    defer c.mu.Unlock()

    val, ada := c.data[key]
    if ada {
        c.hits++
    } else {
        c.misses++
    }
    return val, ada
}

func (c *Cache) Stats() (hits, misses int) {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.hits, c.misses
}

Deadlock — Jebakan yang Harus Dihindari #

flowchart LR
    subgraph Deadlock["Deadlock"]
        G1["Goroutine A\npegang Lock mu1\ntunggu mu2"] 
        G2["Goroutine B\npegang Lock mu2\ntunggu mu1"]
        G1 <-->|"saling tunggu\nselamanya"| G2
    end

    subgraph Cara["Cara Menghindari"]
        C1["Selalu lock dalam\nurutan yang sama"]
        C2["Gunakan satu Mutex\nuntuk data yang terkait"]
        C3["Hindari lock\nbersarang jika bisa"]
        C4["Gunakan defer Unlock\nagar tidak lupa"]
    end

    style Deadlock fill:#fce4ec
    style Cara fill:#e8f5e9
// ANTI-PATTERN: deadlock karena urutan lock berbeda
var mu1, mu2 sync.Mutex

// Goroutine A
go func() {
    mu1.Lock()
    defer mu1.Unlock()
    // ... lakukan sesuatu
    mu2.Lock() // tunggu mu2 — tapi goroutine B pegang mu2 dan tunggu mu1!
    defer mu2.Unlock()
}()

// Goroutine B
go func() {
    mu2.Lock()
    defer mu2.Unlock()
    // ... lakukan sesuatu
    mu1.Lock() // deadlock!
    defer mu1.Unlock()
}()

// BENAR: selalu lock dalam urutan yang sama
go func() {
    mu1.Lock()
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
    // ...
}()

go func() {
    mu1.Lock() // urutan sama: mu1 dulu, baru mu2
    defer mu1.Unlock()
    mu2.Lock()
    defer mu2.Unlock()
    // ...
}()

RWMutex — Read-Write Lock #

sync.RWMutex adalah optimasi dari Mutex untuk skenario di mana operasi baca jauh lebih sering dari tulis. Banyak goroutine bisa memegang RLock secara bersamaan, tapi hanya satu yang bisa memegang Lock (write lock) — dan tidak ada yang bisa baca saat write lock aktif.

flowchart TD
    subgraph States["State RWMutex"]
        Free["Bebas\n(tidak ada lock)"]
        Reading["Reading\n(N goroutine pakai RLock)\n✓ RLock bisa masuk\n✗ Lock harus tunggu"]
        Writing["Writing\n(1 goroutine pakai Lock)\n✗ RLock harus tunggu\n✗ Lock harus tunggu"]
    end

    Free -- "RLock()" --> Reading
    Free -- "Lock()" --> Writing
    Reading -- "semua RUnlock()" --> Free
    Writing -- "Unlock()" --> Free
    Reading -- "Lock() — tunggu" --> Writing

    style Free fill:#e8f5e9
    style Reading fill:#e3f2fd
    style Writing fill:#fce4ec
type ConfigStore struct {
    mu     sync.RWMutex
    config map[string]string
}

func NewConfigStore() *ConfigStore {
    return &ConfigStore{
        config: make(map[string]string),
    }
}

// Read — banyak goroutine bisa baca bersamaan
func (cs *ConfigStore) Get(key string) (string, bool) {
    cs.mu.RLock()         // read lock — tidak blokir reader lain
    defer cs.mu.RUnlock()
    val, ada := cs.config[key]
    return val, ada
}

func (cs *ConfigStore) GetAll() map[string]string {
    cs.mu.RLock()
    defer cs.mu.RUnlock()

    // Buat salinan untuk menghindari akses concurrent ke map yang dikembalikan
    salinan := make(map[string]string, len(cs.config))
    for k, v := range cs.config {
        salinan[k] = v
    }
    return salinan
}

// Write — hanya satu goroutine pada satu waktu, blokir semua reader
func (cs *ConfigStore) Set(key, val string) {
    cs.mu.Lock()         // write lock — eksklusif
    defer cs.mu.Unlock()
    cs.config[key] = val
}

func (cs *ConfigStore) SetBanyak(kv map[string]string) {
    cs.mu.Lock()
    defer cs.mu.Unlock()
    for k, v := range kv {
        cs.config[k] = v
    }
}

// Kapan RWMutex lebih baik dari Mutex?
// Gunakan RWMutex jika:
// - Operasi baca >> operasi tulis (misalnya 100:1)
// - Operasi baca memakan waktu cukup lama
// Untuk operasi yang sangat cepat (sekadar baca/tulis field),
// overhead RWMutex bisa lebih besar dari manfaatnya

WaitGroup — Menunggu Goroutine Selesai #

sync.WaitGroup digunakan untuk menunggu sekelompok goroutine selesai sebelum melanjutkan eksekusi. Ia bekerja seperti counter: Add menambah, Done mengurangi, Wait memblokir sampai counter mencapai nol.

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    tugas := []string{"kirim email", "update cache", "tulis log", "notifikasi"}

    for _, t := range tugas {
        wg.Add(1) // tambahkan SEBELUM goroutine dimulai
        go func(nama string) {
            defer wg.Done() // kurangi saat goroutine selesai
            fmt.Printf("Memulai: %s\n", nama)
            time.Sleep(100 * time.Millisecond) // simulasi kerja
            fmt.Printf("Selesai: %s\n", nama)
        }(t) // teruskan sebagai argumen, bukan capture langsung
    }

    wg.Wait() // blokir sampai semua goroutine Done
    fmt.Println("Semua tugas selesai")
}

Pola: Fan-Out dengan Pengumpulan Hasil #

func prosesParalel(items []string, workerCount int) []Hasil {
    jobs := make(chan string, len(items))
    results := make(chan Hasil, len(items))

    var wg sync.WaitGroup

    // Mulai worker pool
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for item := range jobs {
                hasil := prosesItem(item)
                results <- hasil
            }
        }()
    }

    // Kirim semua pekerjaan
    for _, item := range items {
        jobs <- item
    }
    close(jobs) // sinyal: tidak ada pekerjaan lagi

    // Tunggu semua worker selesai, lalu tutup results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Kumpulkan semua hasil
    var semua []Hasil
    for hasil := range results {
        semua = append(semua, hasil)
    }
    return semua
}

Anti-Pattern WaitGroup #

// ANTI-PATTERN 1: Add di dalam goroutine — race condition!
var wg sync.WaitGroup
for _, item := range items {
    go func(i string) {
        wg.Add(1) // terlambat! Wait() bisa selesai sebelum Add() dipanggil
        defer wg.Done()
        proses(i)
    }(item)
}
wg.Wait()

// BENAR: Add di luar goroutine, sebelum go keyword
for _, item := range items {
    wg.Add(1) // ini yang benar
    go func(i string) {
        defer wg.Done()
        proses(i)
    }(item)
}

// ANTI-PATTERN 2: Reuse WaitGroup sebelum Wait selesai
var wg2 sync.WaitGroup
wg2.Add(1)
go func() {
    defer wg2.Done()
    time.Sleep(time.Second)
}()
wg2.Add(1) // ini AMAN jika counter masih > 0
go func() {
    defer wg2.Done()
}()
wg2.Wait()
// wg2.Add(1) di sini TIDAK AMAN jika goroutine lain sudah Wait

Once — Inisialisasi Sekali Jalan #

sync.Once memastikan sebuah fungsi hanya dieksekusi tepat sekali, meskipun dipanggil dari banyak goroutine secara bersamaan. Ini adalah cara idiomatis untuk lazy initialization yang thread-safe.

import "sync"

// Pola: singleton dengan Once
type Database struct {
    conn *sql.DB
}

var (
    dbInstance *Database
    dbOnce     sync.Once
)

func GetDatabase() *Database {
    dbOnce.Do(func() {
        // Hanya dijalankan SEKALI, meski GetDatabase dipanggil dari
        // ribuan goroutine secara bersamaan
        conn, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
        if err != nil {
            panic(fmt.Sprintf("gagal buka koneksi DB: %v", err))
        }
        if err := conn.Ping(); err != nil {
            panic(fmt.Sprintf("gagal ping DB: %v", err))
        }
        dbInstance = &Database{conn: conn}
        fmt.Println("Koneksi database berhasil dibuat")
    })
    return dbInstance
}

Once dengan Error Handling #

sync.Once tidak mengembalikan error — jika fungsi di dalam Do gagal, kamu perlu menyimpan errornya secara terpisah:

type Service struct {
    once sync.Once
    err  error
    conn *Koneksi
}

func (s *Service) pastikanTerkoneksi() error {
    s.once.Do(func() {
        conn, err := bukaKoneksi()
        if err != nil {
            s.err = fmt.Errorf("pastikanTerkoneksi: %w", err)
            return
        }
        s.conn = conn
    })
    return s.err
}

func (s *Service) Proses(data string) error {
    if err := s.pastikanTerkoneksi(); err != nil {
        return err
    }
    return s.conn.Kirim(data)
}
sync.Once tidak bisa di-reset. Jika fungsi di dalam Do panik, Once tetap menganggap eksekusi sudah selesai — pemanggilan Do berikutnya tidak akan menjalankan fungsi lagi. Jika kamu perlu inisialisasi yang bisa diulangi setelah gagal, gunakan Mutex dengan flag boolean sebagai gantinya.

Pool — Object Pooling #

sync.Pool adalah pool objek yang bisa didaur ulang untuk mengurangi tekanan pada garbage collector. Berguna untuk objek yang mahal dibuat dan sering dibuat-dibuang, seperti buffer, koneksi sementara, atau decoder.

flowchart LR
    subgraph TanpaPool["Tanpa Pool"]
        A1["Request 1\nbuat buffer baru"] --> GC1["GC harus\nbersihkan buffer"]
        A2["Request 2\nbuat buffer baru"] --> GC2["GC harus\nbersihkan buffer"]
        A3["Request 3\nbuat buffer baru"] --> GC3["GC harus\nbersihkan buffer"]
    end

    subgraph DenganPool["Dengan Pool"]
        B1["Request 1\nPool.Get()"] --> Buf["Buffer\n(didaur ulang)"]
        Buf --> B1Done["Pool.Put()\nkembalikan ke pool"]
        B1Done --> B2["Request 2\nPool.Get()\n(pakai buffer yang sama)"]
        B2 --> B2Done["Pool.Put()"]
        B2Done --> B3["Request 3\nPool.Get()"]
    end

    style TanpaPool fill:#fce4ec
    style DenganPool fill:#e8f5e9
import (
    "bytes"
    "sync"
)

// Pool untuk bytes.Buffer — sangat umum di aplikasi HTTP
var bufferPool = sync.Pool{
    New: func() any {
        // Dipanggil saat pool kosong dan perlu objek baru
        return new(bytes.Buffer)
    },
}

func prosesRequest(data []byte) string {
    // Ambil buffer dari pool
    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset() // PENTING: selalu reset sebelum digunakan!
    defer bufferPool.Put(buf) // kembalikan ke pool setelah selesai

    // Gunakan buffer
    buf.Write(data)
    buf.WriteString(" [diproses]")
    return buf.String()
}

// Pool untuk JSON encoder/decoder
var jsonDecoderPool = sync.Pool{
    New: func() any {
        return json.NewDecoder(nil)
    },
}

// Pool untuk struct yang mahal diinisialisasi
type WorkerState struct {
    Buffer    []byte
    Hasil     []string
    hitungan  int
}

var workerPool = sync.Pool{
    New: func() any {
        return &WorkerState{
            Buffer: make([]byte, 0, 4096),
            Hasil:  make([]string, 0, 100),
        }
    },
}

func jalankanWorker(input string) []string {
    state := workerPool.Get().(*WorkerState)
    // Reset state sebelum digunakan
    state.Buffer = state.Buffer[:0]
    state.Hasil = state.Hasil[:0]
    state.hitungan = 0
    defer workerPool.Put(state)

    // Gunakan state...
    state.Hasil = append(state.Hasil, input+" hasil")
    return state.Hasil
}
sync.Pool bukan cache permanen — GC bisa membersihkan isi pool kapan saja. Jangan simpan state penting di pool. Pool paling cocok untuk objek yang mahal dibuat (alokasi memori besar, koneksi, encoder) tapi tidak perlu persistent. Selalu Reset() atau bersihkan objek sebelum digunakan kembali dari pool.

sync.Map — Concurrent-Safe Map #

Map bawaan Go (map[K]V) tidak aman untuk akses concurrent — membaca dan menulis dari goroutine berbeda tanpa sinkronisasi menyebabkan race condition. sync.Map menyediakan map yang aman untuk concurrent access tanpa perlu Mutex manual.

import "sync"

var m sync.Map

// Store — simpan nilai
m.Store("kunci1", "nilai1")
m.Store("kunci2", 42)
m.Store("kunci3", true)

// Load — baca nilai
val, ada := m.Load("kunci1")
if ada {
    fmt.Println(val.(string)) // "nilai1"
}

// LoadOrStore — baca atau simpan jika belum ada (atomic)
actual, loaded := m.LoadOrStore("kunci1", "nilai-baru")
fmt.Println(actual.(string)) // "nilai1" — tidak berubah
fmt.Println(loaded)          // true — kunci sudah ada

actual2, loaded2 := m.LoadOrStore("kunci-baru", "nilai-baru")
fmt.Println(actual2.(string)) // "nilai-baru" — disimpan
fmt.Println(loaded2)          // false — kunci baru

// LoadAndDelete — baca dan hapus (atomic)
val2, ada2 := m.LoadAndDelete("kunci2")
if ada2 {
    fmt.Println(val2.(int)) // 42
}

// Delete — hapus kunci
m.Delete("kunci3")

// Range — iterasi semua pasangan key-value
// PERHATIAN: tidak ada jaminan urutan, dan map bisa berubah saat iterasi
m.Range(func(key, value any) bool {
    fmt.Printf("%v: %v\n", key, value)
    return true // kembalikan false untuk menghentikan iterasi
})

// CompareAndSwap — update hanya jika nilai saat ini sesuai (Go 1.20+)
swapped := m.CompareAndSwap("kunci1", "nilai1", "nilai-diperbarui")
fmt.Println(swapped) // true jika berhasil diswap

// CompareAndDelete — hapus hanya jika nilai sesuai (Go 1.20+)
deleted := m.CompareAndDelete("kunci1", "nilai-diperbarui")
fmt.Println(deleted) // true jika berhasil dihapus

sync.Map vs map + Mutex — Kapan Mana #

flowchart TD
    Q{"Pola akses\nconcurrent map?"} --> P1["Banyak goroutine\nbaca/tulis kunci\nyang SAMA"]
    Q --> P2["Setiap goroutine\nbaca/tulis kunci\nyang BERBEDA"]
    Q --> P3["Kunci stabil setelah\ninisialisasi,\nbanyak reader"]

    P1 --> R1["map + Mutex\nlebih efisien\nuntuk high contention"]
    P2 --> R2["sync.Map\noptimal untuk\npola ini (sharding internal)"]
    P3 --> R3["sync.Map\natau map + RWMutex\nkeduanya baik"]

    style R1 fill:#e3f2fd
    style R2 fill:#e8f5e9
    style R3 fill:#fff3e0
// Kasus ideal sync.Map: cache yang dibaca banyak goroutine,
// jarang diupdate, kunci berbeda per goroutine
var cache sync.Map

func ambilDariCache(key string) (string, bool) {
    val, ada := cache.Load(key)
    if !ada {
        return "", false
    }
    return val.(string), true
}

func simpanKeCache(key, val string) {
    cache.Store(key, val)
}

// Kasus yang lebih baik pakai map + Mutex:
// counter per kunci yang diakses banyak goroutine
type HitCounter struct {
    mu   sync.Mutex
    hits map[string]int
}

func (hc *HitCounter) Catat(endpoint string) {
    hc.mu.Lock()
    defer hc.mu.Unlock()
    hc.hits[endpoint]++
}

Cond — Condition Variable #

sync.Cond adalah primitif untuk mengirim notifikasi antar goroutine ketika kondisi tertentu terpenuhi. Ia lebih jarang digunakan dibanding primitif lain, tapi berguna untuk producer-consumer yang membutuhkan koordinasi yang lebih halus dari channel.

type AntrianTerbatas struct {
    mu      sync.Mutex
    cond    *sync.Cond
    antrian []string
    maks    int
}

func NewAntrianTerbatas(maks int) *AntrianTerbatas {
    a := &AntrianTerbatas{maks: maks}
    a.cond = sync.NewCond(&a.mu)
    return a
}

// Masukkan item — blokir jika penuh
func (a *AntrianTerbatas) Masuk(item string) {
    a.mu.Lock()
    defer a.mu.Unlock()

    for len(a.antrian) >= a.maks {
        a.cond.Wait() // lepas lock, tunggu notifikasi, ambil lock lagi
    }

    a.antrian = append(a.antrian, item)
    a.cond.Signal() // beritahu satu goroutine yang menunggu
}

// Ambil item — blokir jika kosong
func (a *AntrianTerbatas) Ambil() string {
    a.mu.Lock()
    defer a.mu.Unlock()

    for len(a.antrian) == 0 {
        a.cond.Wait()
    }

    item := a.antrian[0]
    a.antrian = a.antrian[1:]
    a.cond.Signal()
    return item
}

Pola Penggunaan di Produksi #

Thread-Safe Struct dengan Metode CRUD #

type UserStore struct {
    mu    sync.RWMutex
    users map[int]*User
    nextID int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users: make(map[int]*User),
    }
}

func (s *UserStore) Tambah(nama, email string) *User {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.nextID++
    user := &User{ID: s.nextID, Nama: nama, Email: email}
    s.users[user.ID] = user
    return user
}

func (s *UserStore) Cari(id int) (*User, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    user, ada := s.users[id]
    return user, ada
}

func (s *UserStore) Perbarui(id int, nama, email string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()

    user, ada := s.users[id]
    if !ada {
        return false
    }
    user.Nama = nama
    user.Email = email
    return true
}

func (s *UserStore) Hapus(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()

    _, ada := s.users[id]
    if !ada {
        return false
    }
    delete(s.users, id)
    return true
}

func (s *UserStore) Semua() []*User {
    s.mu.RLock()
    defer s.mu.RUnlock()

    hasil := make([]*User, 0, len(s.users))
    for _, u := range s.users {
        // Buat salinan untuk keamanan
        salinan := *u
        hasil = append(hasil, &salinan)
    }
    return hasil
}

Worker Pool dengan Context #

type WorkerPool struct {
    jobs    chan func()
    wg      sync.WaitGroup
    once    sync.Once
    quit    chan struct{}
}

func NewWorkerPool(jumlahWorker int) *WorkerPool {
    wp := &WorkerPool{
        jobs: make(chan func(), jumlahWorker*10),
        quit: make(chan struct{}),
    }

    for i := 0; i < jumlahWorker; i++ {
        wp.wg.Add(1)
        go wp.worker()
    }

    return wp
}

func (wp *WorkerPool) worker() {
    defer wp.wg.Done()
    for {
        select {
        case job, ok := <-wp.jobs:
            if !ok {
                return
            }
            job()
        case <-wp.quit:
            return
        }
    }
}

func (wp *WorkerPool) Kirim(job func()) {
    select {
    case wp.jobs <- job:
    case <-wp.quit:
    }
}

func (wp *WorkerPool) Hentikan() {
    wp.once.Do(func() { // Once memastikan hanya dipanggil sekali
        close(wp.quit)
        close(wp.jobs)
        wp.wg.Wait()
    })
}

// Penggunaan
func main() {
    pool := NewWorkerPool(5)
    defer pool.Hentikan()

    var mu sync.Mutex
    hasil := make([]int, 0)

    var wg sync.WaitGroup
    for i := 0; i < 20; i++ {
        wg.Add(1)
        n := i
        pool.Kirim(func() {
            defer wg.Done()
            // Simulasi kerja
            time.Sleep(10 * time.Millisecond)
            mu.Lock()
            hasil = append(hasil, n*n)
            mu.Unlock()
        })
    }

    wg.Wait()
    fmt.Println("Hasil:", hasil)
}

Rate Limiter dengan Mutex #

type RateLimiter struct {
    mu         sync.Mutex
    tokens     float64
    maxTokens  float64
    refillRate float64 // token per detik
    lastRefill time.Time
}

func NewRateLimiter(maxTokens, refillRate float64) *RateLimiter {
    return &RateLimiter{
        tokens:     maxTokens,
        maxTokens:  maxTokens,
        refillRate: refillRate,
        lastRefill: time.Now(),
    }
}

func (rl *RateLimiter) Allow() bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    // Isi ulang token berdasarkan waktu yang berlalu
    sekarang := time.Now()
    elapsed := sekarang.Sub(rl.lastRefill).Seconds()
    rl.tokens = min(rl.maxTokens, rl.tokens+elapsed*rl.refillRate)
    rl.lastRefill = sekarang

    if rl.tokens < 1 {
        return false
    }

    rl.tokens--
    return true
}

func min(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

Mendeteksi Race Condition #

Go menyediakan race detector yang bisa diaktifkan saat build atau test:

# Jalankan dengan race detector
go run -race main.go

# Test dengan race detector
go test -race ./...

# Build dengan race detector (untuk staging/canary)
go build -race -o myapp main.go
// Race detector akan mendeteksi ini:
var counter int

go func() { counter++ }() // tulis
go func() { counter++ }() // tulis bersamaan — RACE!

// Dan ini:
m := map[string]int{}
go func() { m["key"] = 1 }() // tulis
go func() { _ = m["key"] }() // baca bersamaan — RACE!
Selalu jalankan go test -race ./... di CI/CD pipeline. Race detector menambah overhead memori ~5-10× dan waktu eksekusi ~2-20×, tapi sangat efektif mendeteksi race condition yang sulit ditemukan secara manual. Jangan deploy ke produksi dengan race detector aktif karena overhead-nya signifikan.

Kapan Beralih ke Alternatif #

Tetap gunakan sync jika:
  ✓ Mutual exclusion dengan Mutex untuk data yang diakses concurrent
  ✓ RWMutex untuk data yang lebih sering dibaca daripada ditulis
  ✓ WaitGroup untuk menunggu goroutine selesai
  ✓ Once untuk inisialisasi singleton yang thread-safe
  ✓ Pool untuk mendaur ulang objek mahal dan kurangi GC pressure

Pertimbangkan channel jika:
  ✗ Komunikasi antar goroutine (kirim data, bukan hanya sinkronisasi)
  ✗ Pipeline processing — channel lebih ekspresif untuk alur data
  ✗ Fan-out / fan-in pattern
  ✗ "Share memory by communicating" — filosofi Go yang dianjurkan

Pertimbangkan sync/atomic jika:
  ✗ Counter sederhana (int32, int64, uint64)
  ✗ Flag boolean (aktif/nonaktif)
  ✗ Pointer swap yang atomic
  ✗ Operasi yang lebih cepat dari Mutex untuk tipe primitif

Pertimbangkan golang.org/x/sync jika:
  ✗ errgroup — WaitGroup + error handling + context cancellation
  ✗ semaphore — batasi jumlah operasi concurrent
  ✗ singleflight — deduplikasi request concurrent yang identik

Ringkasan #

  • Selalu defer mu.Unlock() tepat setelah mu.Lock() berhasil — ini memastikan Mutex selalu dibebaskan meski terjadi panic atau early return.
  • RWMutex untuk data yang lebih sering dibaca — banyak goroutine bisa RLock() bersamaan, tapi hanya satu yang bisa Lock(). Efektif jika read » write.
  • WaitGroup.Add() harus dipanggil sebelum goroutine dimulai, bukan di dalam goroutine — race condition antara Add dan Wait adalah bug yang umum.
  • sync.Once untuk singleton dan lazy initialization — thread-safe tanpa perlu lock manual, tapi tidak bisa di-reset dan tidak menangani error dengan elegan.
  • sync.Pool untuk objek mahal yang sering dibuat-dibuang — selalu Reset() objek sebelum digunakan dari pool, dan jangan simpan state penting di pool karena GC bisa membersihkannya kapan saja.
  • sync.Map untuk cache concurrent dengan kunci yang jarang konflik — untuk high-contention (banyak goroutine mengakses kunci yang sama), map + Mutex seringkali lebih cepat.
  • Gunakan go test -race secara rutin — race detector adalah alat terbaik untuk mendeteksi race condition yang tidak terlihat dari review kode.
  • Deadlock terjadi saat goroutine saling tunggu — hindari dengan selalu mengunci Mutex dalam urutan yang sama di semua goroutine, dan hindari lock bersarang jika memungkinkan.
  • Pertimbangkan channel sebelum Mutex — jika kamu bisa memodelkan masalah sebagai komunikasi (kirim data antar goroutine), channel seringkali menghasilkan kode yang lebih bersih dan mudah dipahami.

← Sebelumnya: Context   Berikutnya: Log Slog →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact