Concurrency nâng cao
Chương dài nhất Phần 2 — đúng vì concurrency là thế mạnh cốt lõi của Go. Đọc từng mục, không cần 1 lượt.
E1. Mutex — Chia sẻ trạng thái an toàn
Bài toán: nhiều goroutine cùng đếm
var counter int
var mu sync.Mutex
func inc() {
mu.Lock()
defer mu.Unlock()
counter++
}
RWMutex — nhiều đọc, một ghi
var mu sync.RWMutex
func read() {
mu.RLock() // Nhiều goroutine cùng RLock được
defer mu.RUnlock()
// đọc
}
func write() {
mu.Lock() // Ghi độc quyền
defer mu.Unlock()
// ghi
}
🎯 Nguyên tắc vàng của Go
"Đừng giao tiếp bằng cách chia sẻ bộ nhớ; hãy chia sẻ bộ nhớ bằng cách giao tiếp."
→ Ưu tiên channel trước, mutex chỉ khi channel không phù hợp (thường là cache, counter).
🔍 -race — công cụ quan trọng nhất để tự bắt race condition
Race condition (2 goroutine cùng đọc/ghi 1 biến mà không đồng bộ) là loại lỗi khó thấy nhất — chương trình vẫn chạy, vẫn ra kết quả, chỉ thỉnh thoảng sai, cực khó tái hiện bằng mắt thường. Go có sẵn công cụ tự động phát hiện, không cần đoán:
go run -race main.go
go test -race ./...
Cờ -race bật race detector — Go tự theo dõi mọi lần đọc/ghi biến chia sẻ giữa các
goroutine lúc chạy, in ra cảnh báo WARNING: DATA RACE kèm đúng dòng code gây ra, ngay cả
khi race đó chưa từng gây sai kết quả quan sát được. Nên bật -race khi test bất kỳ code
nào có go/goroutine/channel/Mutex — chạy thường không phát hiện được gì, chỉ khi có
-race mới thấy.
E2. sync.Once, sync.Map, atomic
sync.Once — khởi tạo 1 lần duy nhất (thread-safe singleton)
var once sync.Once
var db *DB
func GetDB() *DB {
once.Do(func() {
db = connectDB()
})
return db
}
sync.Map — map an toàn sẵn cho concurrency
var m sync.Map
m.Store("key", 42)
v, ok := m.Load("key")
m.Delete("key")
m.Range(func(k, v any) bool { ... ; return true })
👉 Chỉ nhanh khi: nhiều goroutine đọc/ghi các key khác nhau. Nếu tranh chấp cùng
key → dùng map + Mutex tốt hơn.
atomic — thao tác nguyên tử, nhanh hơn Mutex
var counter int64
atomic.AddInt64(&counter, 1)
v := atomic.LoadInt64(&counter)
E3. Channel Patterns kinh điển
🔹 Worker Pool
Nhiều worker cùng xử lý job từ 1 hàng đợi:
jobs := make(chan int, 100)
results := make(chan int, 100)
// 3 worker
for w := 1; w <= 3; w++ {
go func(id int) {
for j := range jobs {
results <- j * 2
}
}(w)
}
// Gửi job
for j := 1; j <= 10; j++ { jobs <- j }
close(jobs)
// Đọc kết quả
for i := 0; i < 10; i++ { fmt.Println(<-results) }
🔹 Fan-out — 1 nguồn → nhiều consumer
Chia tải: 1 producer gửi vào channel, N goroutine cùng đọc.
🔹 Fan-in — Nhiều nguồn → 1 consumer
Gộp nhiều channel về 1:
func fanIn(chs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range chs {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c { out <- v }
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}
🔹 Pipeline — Chuỗi xử lý
Stage 1: sinh số → Stage 2: bình phương → Stage 3: cộng dồn. Mỗi stage là 1 goroutine + channel kết nối stage trước với stage sau.
E4. select với default — Non-blocking
select {
case v := <-ch:
fmt.Println("nhận:", v)
default:
fmt.Println("không có dữ liệu, không đợi")
}
Timeout ngắn gọn
select {
case v := <-ch:
// xử lý
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
E5. Context — Đầy đủ hơn
context.WithCancel — huỷ chủ động
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(3 * time.Second)
cancel() // Ra lệnh dừng
context.WithDeadline — hạn chót cụ thể
deadline := time.Now().Add(10 * time.Minute)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
context.WithValue — mang metadata (dùng thận trọng)
ctx := context.WithValue(context.Background(), "userID", 42)
// Trong hàm sâu bên trong:
if v := ctx.Value("userID"); v != nil {
userID := v.(int)
}
👉 Chỉ dùng cho request-scoped data (trace ID, user ID). Không dùng để truyền tham số bình thường.