Bài 29 giới thiệu context. Bài này về cách dùng nó cùng goroutine — nơi nó thật sự cần thiết.

Ba chỗ context phải xuất hiện

Một: mọi lệnh gửi vào channel trong goroutine sống lâu.

select {
case out <- v:
case <-ctx.Done():
	return
}

Không có nhánh thứ hai, goroutine kẹt vĩnh viễn khi hạ nguồn ngừng đọc — bài 38 đã đo.

Hai: mọi vòng lặp chờ việc.

for {
	select {
	case <-ctx.Done():
		return ctx.Err()
	case v, ok := <-viec:
		if !ok { return nil }
		xuLy(v)
	}
}

Ba: truyền xuống mọi lời gọi có thể chặn — truy vấn CSDL, HTTP, đọc tệp lớn. Thư viện chuẩn đều nhận ctx ở tham số đầu.

Mẫu kết quả có đệm

Đây là mẹo tôi thấy đáng nhớ nhất trong bài này:

func Lay(ctx context.Context) (int, error) {
	ch := make(chan int)              // KHÔNG đệm -> rò rỉ
	go func() { ch <- tinhLau() }()
	select {
	case v := <-ch:      return v, nil
	case <-ctx.Done():   return 0, ctx.Err()
	}
}

Khi hết giờ, hàm trả về ngay. Goroutine bên trong xong việc, cố gửi vào ch, và không ai nhận nữa — nó kẹt vĩnh viễn.

Sửa bằng đúng một ký tự:

ch := make(chan int, 1)

Giờ goroutine gửi được rồi thoát, kể cả khi không ai lấy. Giá trị nằm trong đệm và được thu hồi cùng channel.

Quy tắc tổng quát: channel trả kết quả cho một lời gọi có thể bị bỏ dở luôn nên có đệm bằng số lượng người gửi.

Huỷ không dừng công việc đang chạy

Điểm này hay bị hiểu nhầm:

select {
case <-ctx.Done():
	return ctx.Err()
}

ctx.Done() chỉ báo rằng nên dừng. Nó không giết goroutine, không ngắt vòng lặp tính toán, không huỷ lời gọi hệ thống đang chạy.

Nghĩa là một vòng lặp tính toán thuần không dừng được nếu bạn không tự kiểm:

for i := 0; i < n; i++ {
	if i%1000 == 0 {
		select {
		case <-ctx.Done(): return ctx.Err()
		default:
		}
	}
	tinh(i)
}

Kiểm mỗi vòng thì tốn; kiểm mỗi nghìn vòng là cân bằng hợp lý.

Với I/O thì thư viện chuẩn lo hộ: http.Client, database/sql, net đều huỷ thao tác khi context hết hạn.

Đừng cất context vào struct

type Dich struct {
	ctx context.Context     // ĐỪNG
}

context gắn với một lời gọi, không gắn với một đối tượng sống lâu. Cất vào struct nghĩa là mọi lời gọi dùng chung một hạn chờ, và bạn mất khả năng huỷ từng request.

Ngoại lệ hiếm: struct đại diện cho chính một tác vụ đang chạy — một worker, một phiên. Ngay cả khi đó, đặt tên trường là ctx và ghi rõ trong comment.

contextWaitGroup

Hai thứ giải hai bài toán khác nhau và thường dùng cùng nhau:

ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup

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

cancel()      // bảo mọi người dừng
wg.Wait()     // chờ họ dừng xong

Thứ tự quan trọng: cancel() trước wg.Wait(). Ngược lại là chờ những goroutine chưa được bảo dừng.

Bài mai sẽ cho thấy errgroup gói cả hai vào một.

Thử ba mươi giây

func f(ctx context.Context) int {
	ch := make(chan int)
	go func() { time.Sleep(time.Second); ch <- 1 }()
	select {
	case v := <-ch: return v
	case <-ctx.Done(): return -1
	}
}

ctx, c := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer c()
f(ctx)
time.Sleep(2 * time.Second)
fmt.Println(runtime.NumGoroutine())

In ra 2. Thêm đệm make(chan int, 1) rồi chạy lại — in ra 1.

Ba mươi giây, một ký tự, và một lớp rò rỉ biến mất.

Ngày mai: sync/atomic và mô hình bộ nhớ Go.