Timeout ของ context ใน Go ช่วยป้องกันไม่ให้การเรียก DB ช้าและการร้องขอภายนอกกองทับบนเซิร์ฟเวอร์ เรียนรู้การส่งต่อเดดไลน์ การยกเลิก และค่าดีฟอลต์ที่ปลอดภัย

คำขอช้าเพียงรายการเดียวมักไม่ใช่แค่ "ช้า" ขณะที่มันรอ มันจะทำให้ goroutine ยังคงทำงาน คงหน่วยความจำสำหรับบัฟเฟอร์และวัตถุการตอบกลับ และมักจะยึดการเชื่อมต่อฐานข้อมูลหรือช่องว่างในพูลไว้ด้วย เมื่อมีคำขอช้าสะสมมากพอ ทรัพยากรจำกัดของคุณจะติดค้าง และ API ก็หยุดทำงานที่มีประโยชน์
คุณจะรู้สึกได้ในสามที่โดยทั่วไป Goroutine สะสมและค่าใช้จ่ายในการจัดตารางเพิ่มขึ้น ทำให้ความหน่วงแย่ลงสำหรับทุกคน พูลของฐานข้อมูลหมดการเชื่อมต่อฟรี ทำให้คำถามที่เร็วต้องรอหลังคำถามช้า หน่วยความจำเพิ่มขึ้นจากข้อมูลระหว่างทางและการสร้างคำตอบบางส่วน ซึ่งเพิ่มงาน GC
การเพิ่มเซิร์ฟเวอร์มักไม่แก้ปัญหา ถ้าแต่ละอินสแตนซ์เจอคอขวดเดียวกัน (พูล DB เล็ก, อัพสตรีมช้า, หรือขีดจำกัดอัตราร่วม) คุณแค่ย้ายคิวไปมาและจ่ายเพิ่ม ในขณะที่ข้อผิดพลาดยังพุ่ง
ลองนึกถึง handler ที่กระจายงาน: โหลดผู้ใช้จาก PostgreSQL, เรียกบริการชำระเงิน แล้วเรียกบริการแนะนำ หากการเรียกบริการแนะนำค้างและไม่มีสิ่งใดยกเลิก มันจะทำให้คำขอไม่จบ การเชื่อมต่อ DB อาจถูกคืน แต่ goroutine และทรัพยากร HTTP client ยังคงถูกผูกอยู่ คูณสิ่งนั้นด้วยคำขอนับร้อยและคุณจะได้การละลายแบบช้า ๆ
เป้าหมายชัดเจน: กำหนดขีดเวลาชัด ๆ หยุดงานเมื่อหมดเวลา ปล่อยทรัพยากร และคืนข้อผิดพลาดที่คาดเดาได้ context ของ Go ให้เดดไลน์กับทุกขั้นตอนเพื่อให้งานหยุดเมื่อผู้ใช้ไม่ได้รออีกต่อไป
context.Context เป็นวัตถุขนาดเล็กที่คุณส่งลงไปในลำดับการเรียก เพื่อให้ทุกชั้นเห็นพ้องเรื่องเดียว: เมื่อใดที่คำขอนี้ต้องหยุด Timeouts เป็นวิธีที่พบได้บ่อยที่สุดเพื่อป้องกันไม่ให้ dependency ช้า ๆ ผูกเซิร์ฟเวอร์ของคุณไว้
Context สามารถถือข้อมูลสามแบบ: เดดไลน์ (เมื่อใดงานต้องหยุด), สัญญาณการยกเลิก (มีคนตัดสินใจหยุดก่อน), และค่าที่มีขอบเขตของคำขอ (ใช้แต่น้อย และอย่าใส่ข้อมูลขนาดใหญ่)
การยกเลิกไม่ใช่เวทมนตร์ context จะมีช่อง Done() เมื่อมันปิด หมายความว่าคำขอถูกยกเลิกหรือหมดเวลา โค้ดที่เคารพ context จะตรวจสอบ Done() (มักใช้ select) และคืนค่าเร็ว คุณยังสามารถตรวจ ctx.Err() เพื่อรู้เหตุผลที่มันจบ ซึ่งมักเป็น context.Canceled หรือ context.DeadlineExceeded
ใช้ context.WithTimeout สำหรับ "หยุดหลัง X วินาที" ใช้ context.WithDeadline เมื่อคุณรู้เวลาตัดที่แน่นอนแล้ว ใช้ context.WithCancel เมื่อเงื่อนไขในพาเรนต์ควรหยุดงาน (ไคลเอนต์ตัดการเชื่อมต่อ, ผู้ใช้เปลี่ยนหน้า, คุณได้คำตอบแล้ว)
เมื่อ context ถูกยกเลิก พฤติกรรมที่ถูกต้องคือเรียบง่ายแต่สำคัญ: หยุดทำงาน หยุดรอ I/O ช้า ๆ และคืนข้อผิดพลาดที่ชัดเจน หาก handler กำลังรอคิวรีฐานข้อมูลและ context จบ ให้คืนเร็วและปล่อยให้การเรียกฐานข้อมูลยกเลิกถ้ารองรับ context
จุดที่ปลอดภัยที่สุดในการหยุดคำขอช้าคือขอบที่ทราฟิกเข้าเซอร์วิสของคุณ หากคำขอจะหมดเวลา คุณอยากให้มันเกิดขึ้นอย่างทำนองเดียวกันและเร็ว ไม่ใช่หลังจากที่ผูก goroutine, การเชื่อมต่อ DB, และหน่วยความจำ
เริ่มจากขอบ (load balancer, API gateway, reverse proxy) แล้วตั้งเพดานเวลาสำหรับระยะเวลาที่อนุญาตให้คำขอมีชีวิตอยู่ นั่นปกป้องเซอร์วิส Go ของคุณแม้ handler จะลืมตั้ง timeout
ภายในเซิร์ฟเวอร์ Go ของคุณ ตั้งค่า timeout ของ HTTP เพื่อเซิร์ฟเวอร์จะไม่รอไคลเอนต์ช้าหรือการตอบกลับที่ค้างอย่างไม่สิ้นสุด อย่างน้อย ควรรันค่าการอ่าน header, การอ่าน body เต็ม, การเขียน response, และการเก็บการเชื่อมต่อ idle
เลือกงบประมาณคำขอที่เหมาะกับผลิตภัณฑ์ สำหรับ API หลายตัว 1 ถึง 3 วินาทีเป็นจุดเริ่มต้นที่สมเหตุสมผลสำหรับคำขอทั่วไป และยอมให้มีขีดจำกัดสูงกว่าสำหรับงานที่ช้า เช่น การส่งออกข้อมูล ตัวเลขที่แน่นอนไม่สำคัญเท่ากับความสม่ำเสมอ การวัด และกฎชัดเจนสำหรับข้อยกเว้น
การสตรีมตอบกลับต้องระวังเป็นพิเศษ ง่ายที่จะทำให้เกิดสตรีมที่ไม่สิ้นสุดโดยบังเอิญ ซึ่งเซิร์ฟเวอร์เปิดการเชื่อมต่อและเขียนชิ้นเล็ก ๆ ไปเรื่อย ๆ หรือรอชิ้นแรกตลอดไป ตัดสินใจก่อนว่า endpoint ไหนเป็นสตรีมจริง ๆ หากไม่ใช่ ให้บังคับเวลาเต็มสูงสุดและเวลา-to-first-byte สูงสุด
เมื่อขอบระบบมีเดดไลน์ชัดเจน การส่งต่อเดดไลน์ผ่านคำขอทั้งหมดก็ง่ายขึ้นมาก
จุดเริ่มต้นที่ง่ายที่สุดคือ HTTP handler นั่นคือที่ที่คำขอหนึ่งรายการเข้าสู่ระบบของคุณ จึงเป็นจุดธรรมชาติในการตั้งขีดจำกัดแน่นอน
สร้าง context ใหม่ที่มีเดดไลน์ และมั่นใจว่าคุณเรียก cancel แล้วส่ง context นั้นไปยังทุกอย่างที่อาจบล็อก: งานฐานข้อมูล, การเรียก HTTP, หรือการคำนวณช้า ๆ
func (s *Server) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
userID := r.URL.Query().Get("id")
if userID == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
user, err := s.loadUser(ctx, userID)
if err != nil {
writeError(w, ctx, err)
return
}
writeJSON(w, http.StatusOK, user)
}
ctx ไปยังทุกการเรียกที่อาจบล็อกกฎที่ดี: ถ้าฟังก์ชันสามารถรอ I/O ให้มันรับ context.Context ไว้ ช่วยให้ handler อ่านง่ายโดยย้ายรายละเอียดไปไว้ใน helper ย่อย ๆ เช่น loadUser
func (s *Server) loadUser(ctx context.Context, id string) (User, error) {
return s.repo.GetUser(ctx, id) // repo ควรใช้ QueryRowContext/ExecContext
}
หากเดดไลน์ถูกทริกเกอร์ (หรือไคลเอนต์ตัดการเชื่อมต่อ) ให้หยุดงานและคืนการตอบกลับที่เป็นมิตรต่อผู้ใช้ การแมปที่พบบ่อยคือ context.DeadlineExceeded เป็น 504 Gateway Timeout และ context.Canceled เป็น "client is gone" (มักไม่มีบอดี้)
func writeError(w http.ResponseWriter, ctx context.Context, err error) {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
if errors.Is(err, context.Canceled) {
// Client went away. Avoid doing more work.
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
}
รูปแบบนี้ป้องกันการสะสม เมื่อเวลาในตัวจับหมด ทุกฟังก์ชันที่เคารพ context ในโซ่จะได้รับสัญญาณหยุดเดียวกันและออกได้เร็ว
เมื่อ handler ของคุณมี context ที่มีเดดไลน์ กฎสำคัญที่สุดคือ: ใช้ ctx เดียวกันนั้นจนถึงการเรียกฐานข้อมูล นั่นคือวิธีที่ timeout หยุดงาน แทนที่จะหยุดแค่ handler ที่รอ
กับ database/sql ให้ใช้เมธอดที่รองรับ context:
func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
row := s.db.QueryRowContext(ctx,
"SELECT id, email FROM users WHERE id = $1",
r.URL.Query().Get("id"),
)
var id int64
var email string
if err := row.Scan(&id, &email); err != nil {
// handle below
}
}
ถ้างบ handler คือ 2 วินาที ฐานข้อมูลควรได้เพียงส่วนหนึ่งของนั้น เผื่อเวลาไว้สำหรับการเข้ารหัส JSON, dependency อื่น และการจัดการข้อผิดพลาด จุดเริ่มต้นง่าย ๆ คือให้ Postgres ประมาณ 30% ถึง 60% ของงบรวม ด้วยเดดไลน์ handler 2 วินาที นั่นอาจเป็น 800ms ถึง 1.2s
เมื่อ context ถูกยกเลิก ไดรเวอร์จะขอให้ Postgres หยุดคิวรี โดยปกติการเชื่อมต่อจะถูกคืนสู่พูลและนำกลับมาใช้ใหม่ หากการยกเลิกเกิดขึ้นในช่วงเครือข่ายไม่ดี ไดรเวอร์อาจทิ้งการเชื่อมต่อและเปิดการเชื่อมต่อใหม่ภายหลัง ไม่ว่าอย่างไร คุณก็หลีกเลี่ยงการมี goroutine รอชั่วนิรันดร์
เมื่อเช็คข้อผิดพลาด ให้ปฏิบัติต่อ timeout แตกต่างจากความล้มเหลวจริงของ DB หาก errors.Is(err, context.DeadlineExceeded) แปลว่าหมดเวลาควรคืน timeout หาก errors.Is(err, context.Canceled) ไคลเอนต์จากไปและควรหยุดอย่างเงียบ ๆ ข้อผิดพลาดอื่น ๆ เป็นปัญหาปกติของคิวรี (SQL ผิด, ไม่มีแถว, สิทธิ์)
ถ้า handler ของคุณมีเดดไลน์ การเรียก HTTP ขาออกควรเคารพมันด้วย มิฉะนั้นไคลเอนต์จะเลิกแต่เซิร์ฟเวอร์ของคุณยังคงรอ upstream ช้า ๆ ผูก goroutine, ซ็อกเก็ต และหน่วยความจำไว้
สร้าง request ขาออกด้วย context ของพาเรนต์เพื่อให้การยกเลิกเดินทางอัตโนมัติ:
func fetchUser(ctx context.Context, url string) ([]byte, error) {
// Add a small per-call cap, but never exceed the parent deadline.
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close() // always close, even on non-200
return io.ReadAll(resp.Body)
}
การตั้ง timeout ต่อการเรียกเป็นตาข่ายความปลอดภัย เดดไลน์ของพาเรนต์ยังเป็น "หัวหน้า" หนึ่งนาฬิกาสำหรับคำขอทั้งหมด พร้อมกับการจำกัดย่อย ๆ สำหรับขั้นตอนที่เสี่ยง
นอกจากนี้ให้ตั้งค่า timeout ที่ระดับ transport ด้วย http.Transport เพราะ context ยกเลิก request ได้ แต่ transport timeout ป้องกันคุณจากการ handshake ช้าและเซิร์ฟเวอร์ที่ไม่ส่ง header
รายละเอียดหนึ่งที่ทำให้ทีมสะดุด: ต้องปิด response body ในทุกเส้นทาง หากคุณคืนค่าก่อน (เช็คสถานะ, decode JSON ผิด, timeout) ให้ปิด body เสมอ การรั่วไหลของ body อาจทำให้การเชื่อมต่อในพูลหมดและกลายเป็นความหน่วงสุ่ม
ตัวอย่างที่ชัดเจน: API ของคุณเรียกผู้ให้บริการจ่ายเงิน ไคลเอนต์หมดเวลาใน 2 วินาที แต่ upstream ค้าง 30 วินาที หากไม่มีการยกเลิก request และ transport timeout คุณจะต้องรอ 30 วินาทีสำหรับทุกคำขอที่ถูกละทิ้ง
คำขอหนึ่งรายการมักสัมผัสกับหลายสิ่งช้า: งาน handler, คิวรีฐานข้อมูล, และการเรียกภายนอกหนึ่งหรือมากกว่า หากคุณให้ timeout ใจกว้างสำหรับแต่ละขั้นตอน เวลารวมจะคืบขึ้นจนผู้ใช้รู้สึกและเซิร์ฟเวอร์ของคุณก็สะสมคำขอช้า
การจัดงบเป็นการแก้ที่ง่ายที่สุด ตั้งเดดไลน์พาเรนต์หนึ่งอันสำหรับคำขอทั้งหมด แล้วให้ dependency แต่ละอันส่วนเล็ก ๆ เด็กแต่ละอันควรมีเดดไลน์ก่อนพาเรนต์เพื่อให้ล้มเหลวเร็วและคุณยังมีเวลาในการคืนค่าข้อผิดพลาดอย่างเรียบร้อย
กฎคร่าว ๆ ที่ใช้ได้จริง:
หลีกเลี่ยงการซ้อนทับของ timeout ที่ขัดแย้งกัน หาก context handler มีเดดไลน์ 2 วินาทีแต่ HTTP client มี timeout 10 วินาที คุณปลอดภัยแต่สับสน หากสถานการณ์กลับกัน ไคลเอนต์อาจตัดการเชื่อมต่อก่อนด้วยเหตุผลอื่น
สำหรับงานแบ็กกราวด์ (เช่น audit logs, metrics, อีเมล) อย่าใช้ context ของคำขอซ้ำ ใช้ context แยกที่มี timeout สั้นของตัวเองเพื่อให้การยกเลิกจากไคลเอนต์ไม่ฆ่าการทำความสะอาดที่สำคัญ
บั๊กเรื่อง timeout ส่วนใหญ่ไม่อยู่ใน handler แต่เกิดในชั้นล่าง ๆ ที่เดดไลน์หายไปอย่างเงียบ ๆ หากคุณตั้ง timeout ที่ขอบแล้วแต่ละหว่างลูปลืมมัน คุณยังอาจมี goroutine, คิวรี DB, หรือการเรียก HTTP ที่ยังรันหลังจากไคลเอนต์จากไป
รูปแบบที่พบบ่อยที่สุดคือ:
context.Background() (หรือ TODO) ซึ่งตัดการเชื่อมต่อการยกเลิกจากเดดไลน์ของคำขอsleep, retry, หรือวนลูปโดยไม่ตรวจสอบ ctx.Done() คำขอยังคงรอแม้จะถูกยกเลิกcontext.WithTimeout ของตัวเองมากเกินไป ทำให้มีตัวจับเวลาเยอะและเดดไลน์สับสนctx กับการเรียกที่อาจบล็อก (DB, HTTP ขาออก, การเผยแพร่ข้อความ) เดดไลน์ handler ไม่มีผลหากการเรียก dependency ไม่สนใจมันความล้มเหลวยอดนิยม: คุณเพิ่ม timeout 2 วินาทีใน handler แต่ repository ใช้ context.Background() สำหรับคิวรี DB ภายใต้โหลด คิวรีช้าจะยังคงรันต่อแม้ไคลเอนต์จะเลิก และการสะสมก็เกิดขึ้น
แก้พื้นฐาน: ส่ง ctx เป็นอาร์กิวเมนต์แรกผ่านสแต็กการเรียกของคุณ ภายในงานยาว ๆ ให้เพิ่มการเช็คสั้น ๆ เช่น:
select {
case <-ctx.Done():
return ctx.Err()
default:
}
แมป context.DeadlineExceeded เป็นการตอบ timeout (มัก 504) และ context.Canceled เป็นการยกเลิกโดยไคลเอนต์ (มัก 408 หรือ 499 ขึ้นกับข้อตกลงของคุณ)
Timeouts มีประโยชน์ก็ต่อเมื่อคุณเห็นมันเกิดขึ้นและยืนยันว่าระบบฟื้นตัวอย่างสะอาด เมื่อบางอย่างช้า คำขอควรหยุด ทรัพยากรถูกปล่อย และ API ควรยังตอบสนอง
สำหรับแต่ละคำขอ ให้บันทึกชุดฟิลด์เล็ก ๆ เดียวกันเพื่อให้เปรียบเทียบคำขอปกติกับ timeout ได้ รวมเดดไลน์ของ context (ถ้ามี) และสิ่งที่ทำให้การทำงานจบ
ฟิลด์ที่มีประโยชน์ได้แก่เดดไลน์ (หรือ "none"), เวลาที่ผ่านไปทั้งหมด, เหตุผลการยกเลิก (timeout vs client canceled), ป้ายชื่อการดำเนินงานสั้น ๆ ("db.query users", "http.call billing"), และ request ID
รูปแบบมินิมัลตัวอย่าง:
start := time.Now()
deadline, hasDeadline := ctx.Deadline()
err := doWork(ctx)
log.Printf("op=%s hasDeadline=%t deadline=%s elapsed=%s err=%v",
"getUser", hasDeadline, deadline.Format(time.RFC3339Nano), time.Since(start), err)
โลกรช่วยแก้เหตุการณ์ทีละคำขอ เมตริกแสดงแนวโน้ม
ติดตามสัญญาณไม่กี่อย่างที่มักพุ่งก่อนเมื่อ timeout ผิดพลาด: จำนวน timeout แยกตาม route และ dependency, จำนวนคำขอที่กำลังทำงาน (in-flight) ซึ่งควรคงที่ภายใต้โหลด, เวลาในการรอของพูล DB, และเปอร์เซ็นไทล์ความหน่วง (p95/p99) แยกระหว่างสำเร็จกับ timeout
ทำให้ความช้าทำนายได้ เพิ่มดีเลย์แบบ debug ใน handler หนึ่ง ๆ ชะลอคิวรี DB ด้วยเวลารอที่ตั้งใจ หรือห่อการเรียกภายนอกด้วยเซิร์ฟเวอร์ทดสอบที่นอนหลับ แล้วยืนยันสองสิ่ง: คุณเห็นข้อผิดพลาด timeout และงานหยุดเร็วหลังจากการยกเลิก
การทดสอบโหลดขนาดเล็กช่วยได้เช่นกัน รันคำขอพร้อมกัน 20–50 รายการเป็นเวลา 30–60 วินาที โดยมี dependency ถูกบังคับให้ช้า จำนวน goroutine และ in-flight request ควรเพิ่มแล้วคงตัว หากยังคงเพิ่มต่อไป แปลว่ามีบางอย่างไม่สนใจการยกเลิก context
Timeouts ช่วยได้ก็ต่อเมื่อถูกใช้อย่างทั่วถึงในทุกที่ที่คำขออาจรอ ก่อน deploy ตรวจสอบโค้ดเบสของคุณตามกฎเดียวกันในทุก handler
context.DeadlineExceeded และ context.Canceledhttp.NewRequestWithContext (หรือ req = req.WithContext(ctx)) และ client มี transport timeouts (dial, TLS, response header). หลีกเลี่ยงการพึ่งพา http.DefaultClient ในทาง productionการซ้อม "dependency ช้า" ก่อน release คุ้มค่า เพิ่มดีเลย์เทียม 2 วินาทีในคิวรี SQL หนึ่งรายการและยืนยันสามสิ่ง: handler คืนค่าตรงเวลา, คิวรี DB หยุดจริง (ไม่ใช่แค่ handler), และโลกระบุชัดเจนว่าเป็น DB timeout
นึกภาพ endpoint เช่น GET /v1/account/summary การกระทำหนึ่งของผู้ใช้เรียกสามสิ่ง: คิวรี PostgreSQL (บัญชีและกิจกรรมล่าสุด) และการเรียก HTTP ภายนอกสองครั้ง (เช่น เช็คสถานะบิลและดึงข้อมูล enrichment ของโปรไฟล์)
ให้งบรวมคำขอเป็น 2 วินาที หากไม่มีงบ หนึ่ง dependency ช้าสามารถยึด goroutine, การเชื่อมต่อ DB, และหน่วยความจำไว้จนกว่า API ของคุณจะเริ่ม timeout ทั่วทั้งระบบ
การแบ่งเวลาเรียบง่ายอาจเป็น 800ms สำหรับ DB, 600ms สำหรับ external A, และ 600ms สำหรับ external B
เมื่อคุณรู้เดดไลน์ทั้งหมด ให้ส่งต่อนั้นลงไป แต่ละ dependency ได้ timeout ย่อยของตัวเอง แต่ยังสืบทอดการยกเลิกจากพาเรนต์
func AccountSummary(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
dbCtx, dbCancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer dbCancel()
aCtx, aCancel := context.WithTimeout(ctx, 600*time.Millisecond)
defer aCancel()
bCtx, bCancel := context.WithTimeout(ctx, 600*time.Millisecond)
defer bCancel()
// Use dbCtx for QueryContext, aCtx/bCtx for outbound HTTP requests.
}
หาก external B ช้าและใช้เวลา 2.5 วินาที handler ของคุณควรหยุดรอที่ 600ms ยกเลิกงานที่กำลังทำ และคืน response timeout ที่ชัดเจนให้ไคลเอนต์ ไคลเอนต์จะเห็นความล้มเหลวอย่างรวดเร็วแทนการรอไม่รู้จบ
โลกระบุชัดเจนว่ารายการใดใช้งบ เช่น: DB เสร็จเร็ว, external A สำเร็จ, external B ถึงขีดจำกัดและคืน context deadline exceeded
เมื่อ endpoint จริง ๆ หนึ่งตัวทำงานดีกับ timeout และการยกเลิกแล้ว ให้เปลี่ยนเป็นรูปแบบซ้ำได้ ใช้มันแบบ end-to-end: เดดไลน์ handler, การเรียก DB, และ HTTP ขาออก แล้วคัดลอกโครงสร้างเดียวกันไปยัง endpoint ถัดไป
คุณจะทำงานได้เร็วขึ้นหากรวมส่วนที่น่าเบื่อนี้ไว้ศูนย์กลาง: helper ตั้งเดดไลน์ที่ขอบ, wrapper ที่รับประกันว่า ctx ถูกส่งไปยัง DB และ HTTP, และการแมปข้อผิดพลาดพร้อมรูปแบบโลกเดียว
ถ้าต้องการต้นแบบอย่างรวดเร็ว Koder.ai (koder.ai) สามารถสร้าง Go handlers และ service calls จาก prompt ในแชท และคุณสามารถส่งออกซอร์สโค้ดเพื่อนำ helper เวลาและงบประมาณของคุณไปใส่ได้ เป้าหมายคือต้องสม่ำเสมอ: งานช้าต้องหยุดเร็ว, ข้อผิดพลาดต้องเหมือนกัน, และการดีบักไม่ควรขึ้นกับว่าใครเขียน endpoint
A ช้าหนึ่งคำขอจะถือทรัพยากรจำกัดไว้เมื่อรอ: goroutine หนึ่งตัว หน่วยความจำสำหรับบัฟเฟอร์และวัตถุตอบกลับ และบ่อยครั้งการเชื่อมต่อฐานข้อมูลหรือการเชื่อมต่อ HTTP ของไคลเอนต์ เมื่อมีคำขอช้าที่รอมาพร้อมกันมากพอ คิวจะก่อตัว ความหน่วงจะเพิ่มขึ้นสำหรับทุกทราฟิก และเซอร์วิสอาจล้มเหลวแม้แต่ละคำขอแต่ละอันจะสำเร็จได้ในที่สุด
กำหนดเดดไลน์ชัดเจนที่ขอบระบบ (proxy/gateway และในเซิร์ฟเวอร์ Go) สร้าง context ที่มีเวลาจำกัดใน handler แล้วส่ง ctx นั้นไปยังทุกการเรียกที่อาจบล็อก (ฐานข้อมูลและ HTTP ขาออก) เมื่อเดดไลน์มาถึง ให้คืนค่าเร็ว ๆ ด้วยการตอบ timeout ที่สอดคล้องและหยุดงานที่กำลังดำเนินการซึ่งรองรับการยกเลิก
ใช้ context.WithTimeout(parent, d) เมื่อคุณต้องการ "หยุดหลังจากระยะเวลานี้" ซึ่งเป็นกรณีที่พบบ่อยที่สุดใน handler ใช้ context.WithDeadline(parent, t) เมื่อคุณมีเวลาตัดที่แน่นอนอยู่แล้ว ใช้ context.WithCancel(parent) เมื่อเงื่อนไขภายในควรหยุดงานก่อนเวลา เช่น "เราได้คำตอบแล้ว" หรือ "ไคลเอนต์ตัดการเชื่อมต่อ"
เรียก cancel() เสมอ โดยปกติจะใช้ defer cancel() ทันทีหลังสร้าง context ที่ได้จาก WithTimeout หรือ WithCancel การเรียก cancel จะปล่อย timer และส่งสัญญาณหยุดให้กับงานลูก โดยเฉพาะในเส้นทางที่คืนค่าก่อนที่เดดไลน์จะถูกทริกเกอร์
สร้าง context ของคำขอครั้งเดียวใน handler แล้วส่งต่อเป็นอาร์กิวเมนต์แรกไปยังฟังก์ชันที่อาจบล็อก วิธีตรวจสอบอย่างรวดเร็วคือค้นหา context.Background() หรือ context.TODO() ในเส้นทางโค้ดของคำขอ เพราะบ่อยครั้งสิ่งเหล่านั้นตัดการเชื่อมต่อการยกเลิกจากเดดไลน์ของคำขอ
ใช้เมธอดของฐานข้อมูลที่รองรับ context เช่น QueryContext, QueryRowContext, และ ExecContext (หรือเทียบเท่าตาม driver) เมื่อ context สิ้นสุด ไดรเวอร์สามารถขอให้ Postgres ยกเลิกการคิวรีได้ ดังนั้นคุณจะไม่เผาเวลาและการเชื่อมต่อหลังจากคำขอจบแล้ว
แนบ context ของคำขอหลักไปกับการเรียก HTTP ขาออกโดยใช้ http.NewRequestWithContext(ctx, ...) และตั้งค่า timeout ใน client/transport เพื่อป้องกันในขั้นตอนเชื่อมต่อ, TLS และการรอ header ของการตอบกลับ แม้ในกรณีเกิดข้อผิดพลาดหรือสถานะไม่ใช่ 200 ให้ปิด resp.Body เสมอเพื่อให้การเชื่อมต่อกลับสู่พูล
กำหนดงบประมาณรวมก่อน จากนั้นแบ่งเวลาที่เหลือให้แต่ละ dependency โดยกันเวลาเล็ก ๆ สำหรับ handler overhead และการเข้ารหัสผลตอบกลับ หากมีการเรียกภายนอกหลายรายการ ให้จำกัดแต่ละรายการแทนการปล่อยให้รายการหนึ่งกินงบทั้งหมด และถ้า context หลักเหลือเวลาเพียงเล็กน้อย อย่าเริ่มงานที่ต้องการเวลามากกว่าที่เหลือ
โดยทั่วไปแมป context.DeadlineExceeded เป็น 504 Gateway Timeout พร้อมข้อความสั้น ๆ เช่น “request timed out” สำหรับ context.Canceled มักหมายความว่าไคลเอนต์ตัดการเชื่อมต่อ การกระทำที่ดีคือหยุดงานและไม่เขียนบอดี้ เพื่อไม่ให้เสียทรัพยากรเพิ่มเติม
ข้อผิดพลาดที่พบบ่อยที่สุดคือการทิ้ง request context โดยใช้ context.Background(), เริ่ม retries หรือ sleep โดยไม่ตรวจสอบ ctx.Done(), และลืมแนบ ctx ในการเรียกที่อาจบล็อก อีกปัญหาลึก ๆ คือการตั้ง timeout หลายจุดแบบไม่สอดคล้อง ซึ่งทำให้เกิดการตัดการเชื่อมต่อก่อนเวลาและยากต่อการเข้าใจ