Context trong Go — thứ mà dev Go nào cũng cần nắm

Phong

Context trong Go — thứ mà dev Go nào cũng cần nắm

Hồi mới học Go, mình cứ thấy cái context.Context lằng nhằng quá chừng. Hàm nào cũng nhận context làm tham số đầu tiên, nhìn code y chang mớ bòng bong. Rồi một ngày mình gặp bug — HTTP request bị treo vô thời hạn vì quên truyền context xuống dưới. Lúc đó mình mới chịu ngồi xuống tìm hiểu kỹ.

Server network Ảnh: Kevin Ku — Pexels

Nói đơn giản thì context trong Go giống như cái "hợp đồng" giữa function gọi và function được gọi. Nó cho phép bạn ba việc: một, set timeout cho tác vụ; hai, truyền dữ liệu xuyên suốt chuỗi gọi; ba, và quan trọng nhất — tự động cancel toàn bộ khi có lỗi.

Deadline và timeout — gốc của mọi chuyện

Cái hay nhất của context là bạn hổng cần tự quản lý timeout bằng tay nữa. Thay vì viết time.After rồi phải nhớ đóng channel, bạn chỉ cần:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

Xong. 5 giây sau, tự động ctx.Done() sẽ báo tín hiệu cho mọi function đang lắng nghe. Gọi API, query database, hay gọi service khác — tất cả đều biết "tới giờ rồi, nghỉ đi". Nhờ vậy server của mình khỏi bị treo mớ goroutine không hồi kết.

Mấy cái bẫy dễ mắc

Nhưng hổng phải ai cũng xài đúng. Mình thấy dân code mới qua Go thường mắc mấy lỗi sau:

Giữ context.Background() ở function xử lý business logic. Sai nha — context.Background() chỉ dùng ở entrypoint như main(), HTTP handler, test thôi. Hễ function nào cần gọi service khác thì phải nhận context từ trên truyền xuống.

Không defer cancel(). Quên cái này là context sống mãi, goroutine leaking tùm lum. Luôn nhớ: ctx, cancel := ... thì dòng kế tiếp phải là defer cancel().

Dùng context.WithValue() cho mọi thứ. Context.Value có thể truyền dữ liệu, nhưng đó là đường chót — chỉ nên xài cho request-scoped data như request ID, user token. Đừng biến nó thành global state thay thế, khổ lắm.

Diagram planning Ảnh: ThisIsEngineering — Pexels

Pattern nên theo

Kinh nghiệm xương máu của mình là: hàm nào cần context thì để ở tham số đầu tiên, kiểu func DoSomething(ctx context.Context, ...). Đây là convention chính thức trong Go, đừng cãi làm gì.

Khi gọi function con, luôn truyền context xuống — cái này gọi là "context propagation":

func HandleRequest(ctx context.Context, req Request) error {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    return db.Query(ctx, req.ID)
}

Một request chết, mọi goroutine liên quan cũng tắt theo. Cancel ở trên là cancel hết cả chuỗi bên dưới. Nghe hơi ác nhưng cần thiết, chứ để treo tài nguyên hoài thì server không chịu nổi.

Lời kết

Context package trong Go nhìn thì nhỏ mà ảnh hưởng lớn. Làm backend Go mà không hiểu context khác gì lái xe không thắng — thấy ngon mà hổng biết chừng nào... tới. Nếu bạn mới học Go, dành hẳn một buổi đọc documentation của context package đi. Mình hứa là có lợi đó.