Banyak program perlu menyiapkan sumber daya yang mahal tepat satu kali: membuka koneksi database, memuat berkas konfigurasi, atau mengisi cache global. Masalah muncul ketika puluhan goroutine membutuhkan sumber daya yang sama pada waktu yang hampir bersamaan. Pemeriksaan flag sederhana tidak lagi cukup karena dua goroutine dapat melewati pemeriksaan tersebut secara bersamaan, sehingga inisialisasi berjalan dua kali. Tipe sync.Once dari package sync menyelesaikan masalah ini dengan satu jaminan tegas: fungsi yang diberikan hanya dieksekusi tepat satu kali, sekalipun Do dipanggil dari ribuan goroutine.

Masalah Inisialisasi Ganda #

Cara paling naif untuk memuat sesuatu sekali adalah memeriksa sebuah flag sebelum menjalankan inisialisasi:

go
if !sudahSiap {
        muatKonfigurasi() // dua goroutine bisa masuk blok ini bersamaan
        sudahSiap = true
}

Urutan langkahnya adalah: baca sudahSiap, jalankan muatKonfigurasi(), lalu tulis sudahSiap = true. Ketiga langkah tersebut bukan satu operasi tunggal. Dua goroutine dapat membaca sudahSiap bernilai false pada saat yang sama, keduanya lolos pemeriksaan, dan muatKonfigurasi() pun berjalan dua kali — dua pool koneksi dan dua kali biaya.

Cara Kerja sync.Once #

Tipe sync.Once adalah struct kecil dengan satu method utama, Do(f func()). Panggilan Do pertama menjalankan f, sementara pemanggil Do lain memblokir sampai f selesai. Setelah f selesai, semua panggilan Do berikutnya langsung kembali tanpa menjalankan apa pun. Dokumentasi package mencatat garansinya: kembalinya f synchronizes before kembalinya setiap panggilan once.Do(f) dalam artian Go memory model. Akibatnya, semua goroutine yang memanggil Do melihat hasil inisialisasi yang sudah selesai tanpa mutex tambahan.

Zero value dari sync.Once sudah siap dipakai; tidak ada konstruktor. Satu Once hanya berguna untuk satu fungsi dan seumur proses: setelah f selesai, tidak ada cara memakai Once yang sama untuk menjalankan fungsi lain.

Membuktikan Eksekusi Tunggal #

Berkas once-demo.go memanggil Do dari delapan goroutine sekaligus. Dua penghitung atomic.Int32 mencatat berapa kali Do dipanggil dan berapa kali fungsi inisialisasi benar-benar dieksekusi, sehingga jaminan sync.Once terbukti dengan angka, bukan sekadar klaim:

go
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var (
	bukaSesi      sync.Once
	eksekusiInit  atomic.Int32
	pemanggilanDo atomic.Int32
	koneksi       string
)

func buatKoneksi() {
	eksekusiInit.Add(1)
	koneksi = "koneksi-aktif"
}

func main() {
	var wg sync.WaitGroup

	for range 8 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			pemanggilanDo.Add(1)
			bukaSesi.Do(buatKoneksi)
		}()
	}
	wg.Wait()

	fmt.Println("pemanggilan Do:", pemanggilanDo.Load(), "kali")
	fmt.Println("eksekusi buatKoneksi:", eksekusiInit.Load(), "kali")
	fmt.Println("koneksi:", koneksi)
}

Jalankan program tersebut dari direktori chapter:

bash
go run once-demo.go

Output yang Dihasilkan:

text
pemanggilan Do: 8 kali
eksekusi buatKoneksi: 1 kali
koneksi: koneksi-aktif

Mari kita telaah baris demi baris program di atas:

  1. Baris 9–14 (blok var): bukaSesi bertipe sync.Once siap pakai dari zero value-nya, eksekusiInit dan pemanggilanDo adalah penghitung atomik, dan koneksi menyimpan hasil inisialisasi.
  2. Baris 16–19 (buatKoneksi): fungsi inisialisasi yang mensimulasikan pekerjaan mahal; fungsi ini menambah penghitung eksekusi lalu mengisi koneksi.
  3. Baris 24 (for range 8): membuat delapan goroutine yang semuanya membutuhkan koneksi pada saat yang hampir bersamaan.
  4. Baris 28 (pemanggilanDo.Add(1)): setiap goroutine menambah penghitung pemanggilan sebelum memanggil Do, sehingga total pemanggilan tercatat dengan akurat.
  5. Baris 29 (bukaSesi.Do(buatKoneksi)): hanya goroutine pertama yang sampai ke baris ini menjalankan buatKoneksi; goroutine lain menunggu sejenak lalu langsung kembali.
  6. Baris 32 (wg.Wait()): memastikan semua goroutine selesai sebelum main membaca penghitung, sehingga pembacaan tidak berlomba dengan penulisan.
  7. Baris 34–36: mencetak bukti angka: delapan pemanggilan Do, satu eksekusi buatKoneksi, dan koneksi yang terisi.

Perhatikan bahwa koneksi ditulis oleh satu goroutine di dalam Do lalu dibaca main setelah wg.Wait(). Garansi synchronizes before dari Once dan penghalang WaitGroup membuat urutan tersebut aman tanpa mutex tambahan.

Pembantu Modern: OnceFunc, OnceValue, OnceValues #

Sejak Go 1.21, package sync menyediakan tiga fungsi pembantu yang membungkus pola yang sama dengan hasil yang tersimpan otomatis:

go
var ambilToken = sync.OnceValue(func() string {
        return "token-sesi"
})

// pemanggilan pertama menjalankan fungsi dan menyimpan hasilnya;
// pemanggilan berikutnya mengembalikan hasil cache tanpa eksekusi ulang.
token := ambilToken()

sync.OnceFunc untuk fungsi tanpa nilai balik, sync.OnceValue untuk satu nilai, dan sync.OnceValues untuk pasangan nilai seperti (data, error). Ketiganya cocok saat kita hanya butuh nilai yang dihitung sekali, tanpa mengelola struct Once secara manual.

Kapan sync.Once Dipakai #

Pola ini paling tepat untuk inisialisasi yang boleh ditunda sampai pemakaian pertama: membuka koneksi di awal hidup proses, memuat konfigurasi besar saat pertama dibutuhkan, atau mendaftarkan sesuatu ke registry global. Jika kebutuhannya menghitung ulang secara berkala atau mereset status, gunakan mutex dengan flag sendiri.

Dengan sync.Once, pembagian kerja inisialisasi antar goroutine sudah beres. Untuk kebutuhan yang lebih kecil lagi — satu angka yang perlu ditambah tanpa mutex — chapter berikutnya memperkenalkan operasi atomik dari package sync/atomic.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

once-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

var (
	bukaSesi      sync.Once
	eksekusiInit  atomic.Int32
	pemanggilanDo atomic.Int32
	koneksi       string
)

func buatKoneksi() {
	eksekusiInit.Add(1)
	koneksi = "koneksi-aktif"
}

func main() {
	var wg sync.WaitGroup

	for range 8 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			pemanggilanDo.Add(1)
			bukaSesi.Do(buatKoneksi)
		}()
	}
	wg.Wait()

	fmt.Println("pemanggilan Do:", pemanggilanDo.Load(), "kali")
	fmt.Println("eksekusi buatKoneksi:", eksekusiInit.Load(), "kali")
	fmt.Println("koneksi:", koneksi)
}