15 thg 3, 2025·7 phút đọc

Timeout của context trong Go cho API: từ HTTP handler đến SQL

Timeout của context trong Go giúp truyền deadline từ HTTP handler tới các gọi SQL, ngăn request bị kẹt và giữ dịch vụ ổn định khi có tải.

Timeout của context trong Go cho API: từ HTTP handler đến SQL

Tại sao request bị kẹt (và tại sao nó gây hại khi có tải)\n\nMột request bị “kẹt” khi nó chờ một thứ gì đó không trả về: một truy vấn database chậm, một kết nối trong pool bị chặn, trục trặc DNS, hoặc một dịch vụ upstream chấp nhận cuộc gọi nhưng không trả lời.\n\nTriệu chứng trông đơn giản: một vài request mất mãi không xong, rồi nhiều request khác cứ xếp sau chúng. Bạn thường thấy bộ nhớ tăng, số goroutine tăng, và hàng đợi kết nối mở không bao giờ cạn.\n\nDưới tải, request kẹt gây hại gấp hai. Chúng giữ worker bận, và chiếm các tài nguyên khan hiếm như kết nối database và lock. Điều đó khiến các request vốn nhanh trở nên chậm, tạo thêm chồng chéo, rồi lại chờ nhiều hơn.\n\nRetry và đột biến traffic làm vòng xoáy này tệ hơn. Client timeout rồi retry trong khi request gốc vẫn đang chạy, vậy là bạn phải trả cho hai request. Nhân điều đó lên nhiều client trong một đoạn nghẽn ngắn, bạn có thể quá tải DB hoặc chạm giới hạn kết nối ngay cả khi lưu lượng trung bình vẫn ổn.\n\nMột timeout đơn giản là một lời hứa: “chúng ta sẽ không chờ lâu hơn X”. Nó giúp fail-fast và giải phóng tài nguyên, nhưng không làm công việc hoàn thành nhanh hơn.\n\nNó cũng không đảm bảo công việc dừng ngay lập tức. Ví dụ: database có thể vẫn thực thi truy vấn, một service upstream có thể phớt lờ hủy, hoặc code của bạn có thể không an toàn khi xảy ra cancellation.\n\nNhững gì timeout đảm bảo là handler của bạn có thể dừng chờ, trả về lỗi rõ ràng, và giải phóng những gì nó nắm giữ. Khoảng chờ có giới hạn đó là thứ ngăn vài cuộc gọi chậm biến thành toàn bộ sự cố.\n\nMục tiêu với timeout của context trong Go là một deadline chung từ biên tới bước gọi sâu nhất. Đặt một lần ở ranh giới HTTP, truyền cùng context qua code dịch vụ, và dùng nó trong các gọi database/sql để database cũng biết khi nào phải dừng chờ.\n\n## Context trong Go theo cách dễ hiểu\n\ncontext.Context là một đối tượng nhỏ bạn truyền qua code để mô tả điều gì đang diễn ra ngay lúc này. Nó trả lời các câu hỏi như: “Request này còn hợp lệ không?”, “Khi nào ta nên bỏ cuộc?”, và “Những giá trị theo request nào nên đi cùng công việc này?”.\n\nLợi ích lớn là một quyết định ở mép hệ thống (HTTP handler) có thể bảo vệ mọi bước phía sau, miễn là bạn tiếp tục truyền cùng một context.\n\n### Context mang theo gì\n\nContext không phải nơi đặt dữ liệu nghiệp vụ. Nó dùng cho tín hiệu điều khiển và một lượng nhỏ metadata theo request: cancellation, deadline/timeout, và metadata nhỏ như request ID để log.\n\nSự khác biệt giữa timeout và cancellation đơn giản: timeout là một lý do để cancel. Nếu bạn đặt timeout 2 giây, context sẽ bị cancel sau 2 giây. Nhưng context cũng có thể bị cancel sớm hơn nếu người dùng đóng tab, load balancer cắt kết nối, hoặc code của bạn quyết định dừng request.\n\nContext chảy qua các lời gọi hàm bằng cách là một tham số tường minh, thường là tham số đầu tiên: func DoThing(ctx context.Context, ...). Đó là ý nghĩa. Khó mà “quên” nó khi nó xuất hiện ở mọi chỗ gọi.\n\nKhi deadline hết hạn, bất cứ thứ gì đang theo dõi context đó nên dừng nhanh. Ví dụ, một truy vấn DB dùng QueryContext nên trả về sớm với lỗi như context deadline exceeded, và handler của bạn có thể phản hồi với timeout thay vì treo cho tới khi server cạn worker.\n\nMột mô hình tư duy tốt: một request, một context, truyền khắp nơi. Nếu request chết, công việc cũng nên chết theo.\n\n## Đặt deadline rõ ràng ở ranh giới HTTP\n\nNếu bạn muốn timeout end-to-end hoạt động, hãy quyết định nơi bắt đầu tính giờ. An toàn nhất là ngay ở ranh giới HTTP, để mọi cuộc gọi phía sau (business logic, SQL, dịch vụ khác) kế thừa cùng deadline.\n\nBạn có thể đặt deadline ở vài nơi. Timeout ở cấp server là nền tảng tốt và bảo vệ bạn khỏi client chậm. Middleware hữu ích để nhất quán across nhóm route. Đặt trong handler cũng được khi bạn muốn rõ ràng và cục bộ.\n\nVới hầu hết API, timeout theo request trong middleware hoặc handler dễ hiểu nhất. Giữ chúng thực tế: người dùng thích thất bại nhanh và rõ ràng hơn là request treo. Nhiều đội dùng ngân sách ngắn hơn cho read (khoảng 1–2s) và lâu hơn một chút cho write (3–10s), tùy endpoint.\n\nDưới đây là pattern handler đơn giản:\n\ngo\nfunc (s *Server) getReport(w http.ResponseWriter, r *http.Request) {\n ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)\n defer cancel()\n\n report, err := s.reports.Generate(ctx, r.URL.Query().Get("id"))\n if err != nil {\n http.Error(w, err.Error(), http.StatusGatewayTimeout)\n return\n }\n\n json.NewEncoder(w).Encode(report)\n}\n\n\nHai quy tắc giữ cho điều này hiệu quả:\n\n- Luôn gọi cancel() để timer và tài nguyên được giải phóng nhanh.\n- Không bao giờ thay thế request context bằng context.Background() hoặc context.TODO() trong handler. Điều đó phá vỡ chuỗi, và gọi DB hoặc outbound có thể chạy mãi ngay cả khi client đã rời.\n\n## Truyền context qua codebase của bạn\n\nMột khi bạn đặt deadline ở ranh giới HTTP, công việc thực sự là đảm bảo cùng deadline đó tới mọi lớp có thể chặn. Ý tưởng là một chiếc đồng hồ chung, chia sẻ giữa handler, service, và mọi thứ chạm mạng hoặc đĩa.\n\nMột quy tắc đơn giản giữ mọi thứ nhất quán: mọi hàm có thể chờ nên chấp nhận context.Context, và nó nên là tham số đầu tiên. Điều đó làm rõ ở chỗ gọi, và thành thói quen.\n\n### Mẫu chữ ký thực tế\n\nƯu tiên chữ ký như DoThing(ctx context.Context, ...) cho services và repositories. Tránh giấu context trong struct hoặc tạo lại bằng context.Background() ở tầng thấp, vì điều đó âm thầm loại bỏ deadline của caller.\n\ngo\nfunc (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {\n ctx := r.Context()\n\n if err := h.svc.CreateOrder(ctx, r.Body); err != nil {\n // map context errors to a clear client response elsewhere\n http.Error(w, err.Error(), http.StatusRequestTimeout)\n return\n }\n}\n\nfunc (s *Service) CreateOrder(ctx context.Context, body io.Reader) error {\n // parsing or validation can still respect cancellation\n select {\n case <-ctx.Done():\n return ctx.Err()\n default:\n }\n\n return s.repo.InsertOrder(ctx, /* data */)\n}\n\n\n### Xử lý thoát sớm một cách gọn gàng\n\nXem ctx.Done() như một đường điều khiển bình thường. Hai thói quen hữu ích:\n\n- Kiểm tra ctx.Err() trước khi bắt đầu công việc tốn kém, và sau các vòng lặp dài.\n- Trả ctx.Err() lên trên không thay đổi, để handler có thể phản hồi nhanh và ngừng lãng phí tài nguyên.\n\nKhi mọi lớp truyền cùng ctx, một timeout duy nhất có thể cắt parsing, logic nghiệp vụ và chờ DB cùng một lúc.\n\n## Áp dụng deadline cho database/sql queries\n\nKhi handler HTTP của bạn có deadline, hãy chắc rằng công việc database cũng lắng nghe nó. Với database/sql, điều đó có nghĩa dùng các phương thức có context mọi lúc. Nếu bạn gọi Query() hoặc Exec() không có context, API của bạn có thể tiếp tục chờ một truy vấn chậm ngay cả khi client đã bỏ cuộc.\n\nDùng nhất quán: db.QueryContext, db.QueryRowContext, db.ExecContext, và db.PrepareContext (rồi QueryContext/ExecContext trên statement trả về).\n\ngo\nfunc (s *Store) GetUser(ctx context.Context, id int64) (*User, error) {\n row := s.db.QueryRowContext(ctx,\n `SELECT id, email FROM users WHERE id = $1`, id,\n )\n var u User\n if err := row.Scan(&u.ID, &u.Email); err != nil {\n return nil, err\n }\n return &u, nil\n}\n\nfunc (s *Store) UpdateEmail(ctx context.Context, id int64, email string) error {\n _, err := s.db.ExecContext(ctx,\n `UPDATE users SET email = $1 WHERE id = $2`, email, id,\n )\n return err\n}\n\n\nHai điều dễ bỏ sót.\n\nĐầu tiên, driver SQL của bạn phải tôn trọng hủy context. Nhiều driver làm vậy, nhưng hãy xác nhận trong stack của bạn bằng cách test một truy vấn chậm cố ý và kiểm tra xem nó có bị hủy nhanh khi deadline vượt không.\n\nThứ hai, cân nhắc timeout phía database như lớp phòng vệ. Ví dụ, Postgres có thể áp giới hạn per-statement (statement timeout). Điều đó bảo vệ DB ngay cả khi một bug trong app quên truyền context.\n\nKhi một thao tác dừng do timeout, xử lý nó khác với lỗi SQL bình thường. Kiểm tra errors.Is(err, context.DeadlineExceeded)errors.Is(err, context.Canceled) và trả về phản hồi rõ ràng (như 504) thay vì xử lý như “database hỏng”. Nếu bạn sinh backend Go (ví dụ với AppMaster), giữ các đường lỗi này tách biệt cũng giúp logs và retry dễ hiểu hơn.\n\n## Các cuộc gọi downstream: HTTP client, cache và dịch vụ khác\n\nNgay cả khi handler và các truy vấn SQL tôn trọng context, request vẫn có thể treo nếu một cuộc gọi downstream chờ mãi. Dưới tải, vài goroutine kẹt có thể xếp chồng, ăn cắp pool kết nối, và biến một chậm nhỏ thành sự cố toàn bộ. Cách khắc phục là truyền nhất quán cộng với một backstop cứng.\n\n### HTTP outbound\n\nKhi gọi API khác, tạo request với cùng context để deadline và cancellation được truyền tự động.\n\ngo\nreq, err := http.NewRequestWithContext(ctx, "GET", url, nil)\nif err != nil { /* handle */ }\nresp, err := httpClient.Do(req)\n\n\nĐừng chỉ dựa vào context. Cũng cấu hình HTTP client và transport để bạn được bảo vệ nếu code vô tình dùng background context, hoặc DNS/TLS/idle connections bị treo. Đặt http.Client.Timeout như giới hạn trên cho toàn bộ cuộc gọi, đặt timeout cho transport (dial, TLS handshake, response header), và tái sử dụng một client thay vì tạo client mới cho mỗi request.\n\n### Cache và queue\n\nCache, message broker và RPC client thường có điểm chờ riêng: lấy kết nối, chờ phản hồi, block trên queue đầy, hoặc chờ lock. Hãy đảm bảo các thao tác đó chấp nhận ctx, và dùng timeout ở mức thư viện khi có.\n\nMột quy tắc thực tế: nếu request của user còn 800ms, đừng bắt đầu một cuộc gọi downstream có thể mất 2s. Bỏ qua, suy giảm, hoặc trả dữ liệu một phần.\n\nQuyết định trước timeout nghĩa là gì cho API của bạn. Đôi khi câu trả lời đúng là trả lỗi nhanh. Đôi khi là dữ liệu một phần cho trường tùy chọn. Đôi khi là dữ liệu cũ từ cache, được đánh dấu rõ.\n\nNếu bạn xây dựng backend Go (kể cả backend sinh mã, như AppMaster), điều này là khác biệt giữa “có timeout” và “timeout bảo vệ hệ thống nhất quán” khi traffic tăng vọt.\n\n## Bước theo bước: refactor API để dùng timeout end-to-end\n\nRefactor cho timeout chủ yếu là một thói quen: truyền cùng context.Context từ mép HTTP xuống mọi cuộc gọi có thể chặn.\n\nCách thực tế làm theo kiểu top-down:\n\n- Thay đổi handler và method core service để chấp nhận ctx context.Context.\n- Cập nhật mọi gọi DB để dùng QueryContext hoặc ExecContext.\n- Làm tương tự cho các cuộc gọi ra ngoài (HTTP client, cache, queue). Nếu thư viện không chấp nhận ctx, bọc nó hoặc thay thế.\n- Quyết định ai sở hữu timeout. Quy tắc phổ biến: handler đặt deadline tổng; các lớp thấp hơn chỉ đặt deadline ngắn hơn cho thao tác cụ thể khi cần.\n- Làm cho lỗi ở biên dễ đoán: ánh xạ context.DeadlineExceededcontext.Canceled thành HTTP response rõ ràng.\n\nDưới đây là hình dạng bạn muốn giữa các lớp:\n\ngo\nfunc (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {\n ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)\n defer cancel()\n\n order, err := h.svc.GetOrder(ctx, r.PathValue("id"))\n if errors.Is(err, context.DeadlineExceeded) {\n http.Error(w, "request timed out", http.StatusGatewayTimeout)\n return\n }\n if err != nil {\n http.Error(w, "internal error", http.StatusInternalServerError)\n return\n }\n _ = json.NewEncoder(w).Encode(order)\n}\n\nfunc (r *Repo) GetOrder(ctx context.Context, id string) (Order, error) {\n row := r.db.QueryRowContext(ctx, `SELECT id,total FROM orders WHERE id=$1`, id)\n // scan...\n}\n\n\nGiá trị timeout nên nhàm chán và nhất quán. Nếu handler có 2s tổng, giữ truy vấn DB dưới 1s để còn chỗ cho encoding JSON và các công việc khác.\n\nĐể chứng minh nó hoạt động, thêm test ép timeout. Một cách đơn giản là repository giả chặn tới ctx.Done() rồi trả ctx.Err(). Test của bạn nên khẳng định handler trả 504 nhanh, không phải sau delay giả đó.\n\nNếu bạn sinh backend Go bằng generator (ví dụ AppMaster), quy tắc vẫn vậy: một context request, luồn khắp nơi, với ownership deadline rõ ràng.\n\n## Observability: chứng minh timeout đang hoạt động\n\nTimeout chỉ có ích nếu bạn thấy chúng xảy ra. Mục tiêu đơn giản: mỗi request có deadline, và khi nó thất bại bạn biết thời gian tiêu tốn ở đâu.\n\nBắt đầu với log an toàn và hữu ích. Thay vì dump nguyên body request, log đủ để liên kết sự kiện và phát hiện đường chậm: request ID (hoặc trace ID), liệu deadline có được đặt và còn bao nhiêu thời gian tại các điểm chính, tên thao tác (handler, tên query SQL, gọi outbound), và hạng kết quả (ok, timeout, canceled, lỗi khác).\n\nThêm một vài metric tập trung để hành vi dưới tải rõ ràng:\n\n- Số timeout theo endpoint và dependency\n- Độ trễ request (p50/p95/p99)\n- Số request đang chạy\n- Độ trễ truy vấn DB (p95/p99)\n- Tỉ lệ lỗi phân theo loại\n\nKhi bạn xử lý lỗi, gán tag chính xác. context.DeadlineExceeded thường có nghĩa bạn vượt ngân sách. context.Canceled thường là client đã rời hoặc timeout upstream xảy ra trước. Giữ hai loại này riêng vì cách sửa khác nhau.\n\n### Tracing: tìm nơi tiêu thời gian\n\nSpan tracing nên theo cùng context từ handler HTTP vào các gọi database/sql như QueryContext. Ví dụ, một request timeout sau 2s và trace cho thấy 1.8s chờ connection DB. Điều đó chỉ ra vấn đề kích thước pool hoặc transaction chậm, không phải text truy vấn.\n\nNếu bạn làm dashboard nội bộ cho điều này (timeouts theo route, truy vấn chậm hàng đầu), một công cụ no-code như AppMaster có thể giúp triển khai nhanh mà không biến observability thành dự án riêng.\n\n## Sai lầm phổ biến làm mất tác dụng của timeout\n\nHầu hết bug “vẫn treo đôi khi” đến từ vài sai lầm nhỏ.\n\n- Đặt lại đồng hồ giữa chừng. Handler đặt deadline 2s, nhưng repository tạo context mới với timeout riêng (hoặc không có). Bây giờ DB có thể tiếp tục chạy sau khi client đã rời. Truyền ctx vào và chỉ thu hẹp timeout khi có lý do rõ ràng.\n- Khởi tạo goroutine mà không dừng. Spawn công việc với context.Background() (hoặc bỏ qua ctx) nghĩa là nó sẽ chạy tiếp ngay cả khi request bị cancel. Truyền request ctx vào goroutine và select trên ctx.Done().\n- Deadline quá ngắn so với lưu lượng thực. Timeout 50ms có thể ổn trên laptop nhưng fail production khi có spike nhỏ, gây retry, thêm tải, và một mini-outage do chính bạn. Chọn timeout dựa trên latency bình thường cộng dư địa.\n- Che giấu lỗi thật. Xử lý context.DeadlineExceeded như 500 làm debug và hành vi client tệ hơn. Ánh xạ nó thành phản hồi timeout rõ ràng và log khác biệt giữa “bị client hủy” và “hết thời gian”.\n- Để tài nguyên mở khi thoát sớm. Nếu bạn return sớm, đảm bảo vẫn defer rows.Close() và gọi cancel từ context.WithTimeout. Rows rò rỉ hoặc công việc còn lại có thể làm cạn kết nối dưới tải.\n\nMột ví dụ nhanh: một endpoint kích hoạt truy vấn báo cáo. Nếu user đóng tab, handler ctx bị cancel. Nếu gọi SQL dùng background context mới, truy vấn vẫn chạy, chiếm kết nối và làm chậm người khác. Khi bạn truyền cùng ctx vào QueryContext, cuộc gọi DB bị gián đoạn và hệ thống phục hồi nhanh hơn.\n\n## Checklist nhanh để timeout tin cậy\n\nTimeout chỉ hữu ích nếu nhất quán. Một cuộc gọi bị bỏ sót có thể giữ goroutine bận, giữ connection DB, và làm chậm request kế tiếp.\n\n- Đặt một deadline rõ ràng ở mép (thường là handler HTTP). Mọi thứ trong request nên kế thừa.\n- Truyền cùng ctx qua service và repository. Tránh context.Background() trong code request.\n- Dùng phương thức DB có context ở mọi nơi: QueryContext, QueryRowContext, ExecContext.\n- Gắn cùng ctx cho các cuộc gọi outbound (HTTP client, cache, queue). Nếu tạo context con, giữ nó ngắn hơn, không dài hơn.\n- Xử lý cancellation và timeout nhất quán: trả lỗi rõ ràng, dừng công việc, và tránh loop retry trong một request đã bị cancel.\n\nSau đó, kiểm chứng hành vi dưới tải. Một timeout kích hoạt nhưng không giải phóng tài nguyên đủ nhanh vẫn gây hại cho độ tin cậy.\n\nDashboard nên làm timeout hiển nhiên, không ẩn trong trung bình. Theo dõi vài tín hiệu trả lời câu “deadlines có thực sự được thi hành không?”: timeout request và timeout DB (riêng biệt), độ trễ phần trăm (p95, p99), stats pool DB (kết nối in-use, wait count, wait duration), và phân tích nguyên nhân lỗi (context deadline exceeded vs lỗi khác).\n\nNếu bạn xây dựng công cụ nội bộ trên nền tảng như AppMaster, cùng checklist áp dụng cho mọi service Go bạn kết nối: định nghĩa deadline ở biên, truyền nó, và xác nhận bằng metric rằng request kẹt trở thành fail-fast thay vì pileup chậm.\n\n## Kịch bản ví dụ và bước tiếp theo\n\nMột trường hợp phổ biến nơi điều này có ích là endpoint tìm kiếm. Tưởng tượng GET /search?q=printer trở nên chậm khi DB bận với truy vấn báo cáo lớn. Không có deadline, mỗi request mới có thể ngồi chờ một truy vấn dài. Dưới tải, các request kẹt xếp chồng, chiếm goroutine và connection, và toàn bộ API như đóng băng.\n\nVới deadline rõ ràng ở handler HTTP và cùng ctx truyền xuống repository, hệ thống dừng chờ khi ngân sách hết. Khi deadline tới, driver DB hủy truy vấn (nếu hỗ trợ), handler trả về, và server tiếp tục phục vụ request mới thay vì chờ mãi.\n\nHành vi nhìn thấy được cho user cũng tốt hơn khi có sự cố. Thay vì quay vòng 30–120 giây rồi fail một cách lộn xộn, client nhận lỗi nhanh, dự đoán được (thường 504 hoặc 503 với thông báo ngắn như "request timed out"). Quan trọng hơn, hệ thống phục hồi nhanh vì request mới không bị chặn phía sau các request cũ.\n\nCác bước tiếp theo để duy trì rải rác giữa các endpoint và team:\n\n- Chọn timeout chuẩn cho từng loại endpoint (search vs write vs export).\n- Yêu cầu QueryContextExecContext trong code review.\n- Làm lỗi timeout rõ ràng ở biên (mã trạng thái rõ ràng, thông điệp đơn giản).\n- Thêm metric cho timeout và cancellation để phát hiện suy giảm sớm.\n- Viết một helper tạo context và logging để mọi handler hành xử giống nhau.\n\nNếu bạn xây dựng service và công cụ nội bộ với AppMaster, bạn có thể áp dụng các quy tắc timeout này nhất quán cho backend Go được sinh ra, tích hợp API, và dashboard ở cùng một nơi. AppMaster (no-code, với khả năng sinh mã nguồn Go thực tế) là một lựa chọn thực tế khi bạn muốn nhất quán trong xử lý request và observability mà không cần tự tay xây mọi công cụ admin.

Câu hỏi thường gặp

Điều gì có nghĩa khi một request bị “kẹt” trong API Go?

Một request bị “kẹt” khi nó chờ thứ gì đó không trả về, ví dụ một truy vấn SQL chậm, một kết nối trong pool bị block, sự cố DNS, hoặc một dịch vụ upstream chấp nhận gọi nhưng không trả lời. Dưới tải, các request kẹt sẽ xếp chồng, chiếm worker và kết nối, và có thể biến một sự chậm nhỏ thành sự cố rộng hơn.

Tôi nên đặt timeout ở middleware, handler hay sâu trong code?

Thiết lập deadline tổng thể ở ranh giới HTTP và truyền cùng ctx đó tới mọi lớp có thể chặn. Deadline chia sẻ đó sẽ ngăn một vài thao tác chậm chiếm giữ tài nguyên đủ lâu để gây hiệu ứng domino.

Tại sao tôi cần gọi `cancel()` nếu timeout rồi sẽ tự xảy ra?

Dùng ctx, cancel := context.WithTimeout(r.Context(), d) và luôn defer cancel() trong handler (hoặc middleware). Gọi cancel sẽ giải phóng timer và giúp dừng chờ kịp thời khi request kết thúc sớm.

Sai lầm lớn nhất khiến timeout trở nên vô ích là gì?

Đừng thay thế bằng context.Background() hay context.TODO() trong code xử lý request, vì điều đó phá vỡ việc hủy và deadline. Nếu bạn bỏ context của request, công việc downstream như SQL hay HTTP outbound có thể tiếp tục chạy ngay cả khi client đã rời đi.

Tôi nên xử lý `context deadline exceeded` khác với `context canceled` thế nào?

Xử lý context.DeadlineExceededcontext.Canceled như những kết quả điều khiển bình thường và truyền chúng lên trên nguyên trạng. Ở biên, ánh xạ chúng thành phản hồi rõ ràng (thường 504 cho timeout) để client không retry mù quáng trên lỗi trông giống 500.

Những gọi `database/sql` nào nên dùng context?

Dùng các phương thức có context ở mọi nơi: QueryContext, QueryRowContext, ExecContext, và PrepareContext. Nếu gọi Query() hoặc Exec() không có context, handler của bạn có thể timeout nhưng cuộc gọi DB vẫn tiếp tục chặn goroutine và giữ kết nối.

Hủy context có thực sự dừng một truy vấn PostgreSQL đang chạy không?

Nhiều driver hỗ trợ hủy truy vấn, nhưng bạn nên kiểm chứng trên hệ thống của mình bằng cách chạy một truy vấn chậm cố ý và xác nhận nó trả về nhanh khi deadline vượt. Ngoài ra nên đặt timeout phía DB (ví dụ statement timeout ở Postgres) như một lớp phòng thủ nếu có đoạn code quên truyền ctx.

Làm sao để áp dụng cùng deadline cho các cuộc gọi HTTP đi ra?

Tạo request outbound bằng http.NewRequestWithContext(ctx, ...) để cùng deadline và hủy truyền xuống. Đồng thời cấu hình client và transport (timeout tổng http.Client.Timeout, timeout dial, TLS handshake, response header) để có giới hạn cứng khi có chỗ bị treo hoặc code vô tình dùng background context.

Các lớp thấp hơn (repo/services) có nên tạo timeout riêng không?

Tránh tạo context mới ở lớp thấp làm kéo dài ngân sách thời gian của request; context con nếu cần thì nên ngắn hơn, không dài hơn. Nếu request gần hết thời gian, hãy bỏ qua gọi downstream tùy chọn, trả dữ liệu một phần nếu phù hợp, hoặc fail-fast với lỗi rõ ràng.

Tôi nên giám sát gì để chứng minh timeout end-to-end hoạt động?

Theo dõi timeout và cancellation riêng biệt theo endpoint và dependency, cùng các phần trăm độ trễ và số request đang chạy. Trong tracing, truyền cùng context từ handler vào các gọi outbound và QueryContext để thấy rõ thời gian tiêu tốn cho chờ connection DB, chạy truy vấn hay chờ service khác.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu
Timeout của context trong Go cho API: từ HTTP handler đến SQL | AppMaster