Ba công cụ phối hợp trong gói sync mà bạn sẽ gõ hằng ngày.

WaitGroup: chờ nhóm goroutine

var wg sync.WaitGroup
for i := 0; i < n; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		lam()
	}()
}
wg.Wait()

Ba method: Add(n) tăng bộ đếm, Done() giảm một, Wait() chặn tới khi về 0.

Bốn quy tắc, và cả bốn đều là nguồn lỗi thật:

Add phải gọi TRƯỚC go, không phải bên trong goroutine. Gọi bên trong thì Wait() có thể chạy trước khi bộ đếm kịp tăng, và nó trả về ngay lập tức.

Done luôn trong defer. Panic giữa chừng mà không DoneWait() chặn vĩnh viễn.

Truyền *WaitGroup, không truyền giá trị. Sao chép WaitGroup là bản sao có bộ đếm riêng, và go vet bắt được lỗi này.

Bộ đếm âm là panic: sync: negative WaitGroup counter — nghĩa là bạn gọi Done nhiều hơn Add.

Từ Go 1.20 có context kết hợp sẵn qua errgroup — bài 41 sẽ nói, và nó thường tốt hơn WaitGroup trần vì xử lý được lỗi.

Once: chạy đúng một lần

  100 goroutine cùng gọi Once.Do -> n=1
var once sync.Once
once.Do(func() { khoiTao() })

An toàn với mọi số goroutine. Do chặn cho tới khi hàm bên trong chạy xong, nên goroutine thứ hai không đi tiếp khi khởi tạo còn dở — đây là điểm khác biệt quan trọng so với một cờ boolean tự viết.

Dùng cho khởi tạo lười: kết nối, biên dịch regex tốn kém, nạp cấu hình.

Ba cái bẫy:

Do chạy đúng một lần kể cả khi hàm bên trong hỏng. Không có cơ chế thử lại. Cần thử lại thì phải tự viết bằng Mutex.

Không lấy lại được lỗi. Mẫu chuẩn là cất vào biến ngoài:

var (
	once sync.Once
	db   *sql.DB
	err  error
)
func Lay() (*sql.DB, error) {
	once.Do(func() { db, err = sql.Open(...) })
	return db, err
}

Không sao chép Once sau khi dùng — cùng lý do với Mutex.

Từ Go 1.21 có sync.OnceValuesync.OnceValues gói sẵn mẫu trên:

var layDB = sync.OnceValues(func() (*sql.DB, error) { return sql.Open(...) })

Pool: tái dùng bộ nhớ

  Pool tái dùng cùng đối tượng: true
var p = sync.Pool{
	New: func() any { return new(bytes.Buffer) },
}

b := p.Get().(*bytes.Buffer)
b.Reset()                    // BẮT BUỘC: đối tượng cũ còn dữ liệu
defer p.Put(b)

Pool giảm áp lực lên bộ thu gom rác bằng cách tái dùng đối tượng thay vì cấp mới.

Ba điều phải biết trước khi dùng:

Bộ thu gom rác có thể dọn sạch Pool bất cứ lúc nào. Nó không phải cache — không bao giờ dựa vào việc đối tượng còn đó.

Phải Reset sau khi Get. Đối tượng lấy ra mang theo dữ liệu của lần dùng trước. Quên Reset là rò rỉ dữ liệu giữa các request — đúng loại lỗi bảo mật ở bài 78 sê-ri Java.

Chỉ dùng khi đã đo. Pool chỉ đáng khi bạn cấp phát cùng loại đối tượng rất nhiều lần trong đường chạy nóng. Với mã thường, nó thêm phức tạp mà không đổi lấy gì.

Ví dụ điển hình trong thư viện chuẩn: fmt dùng Pool cho bộ đệm định dạng, và net/http dùng cho bộ đệm đọc ghi.

Vài công cụ khác trong sync

sync.RWMutex — bài 34 đã đo: chỉ thắng khi phần đọc đủ dài.

sync.Cond — biến điều kiện, tương đương wait/notify của Java. Rất hiếm khi cần trong Go vì channel đã giải cùng bài toán gọn hơn. Nếu thấy mình cần Cond, hãy thử thiết kế lại bằng channel trước.

sync/atomic — bài 40 sẽ nói kỹ.

Thử ba mươi giây

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
	go func() {
		wg.Add(1)          // SAI: Add bên trong goroutine
		defer wg.Done()
		time.Sleep(10 * time.Millisecond)
	}()
}
wg.Wait()
fmt.Println("xong")

In ra xong ngay lập tức, không chờ gì cả — vì Wait() chạy khi bộ đếm còn 0.

Chuyển wg.Add(1) ra trước go và chạy lại. Ba mươi giây đó là lỗi WaitGroup phổ biến nhất.

Ngày mai: race detector — công cụ bắt lỗi đua mà bạn nên bật trong CI.