รูปแบบการจัดการข้อผิดพลาดสำหรับ Go API ที่ทำให้รหัสข้อผิดพลาด สถานะ HTTP request ID และข้อความที่ปลอดภัยมีความสอดคล้องโดยไม่รั่วไหลข้อมูลภายใน

{ error: not found } อีกเส้นทางคืน { message: missing } และอีกเส้นทางหนึ่งส่งข้อความธรรมดา แม้ว่าความหมายจะใกล้เคียงกัน แต่โค้ดฝั่งไคลเอนต์ต้องเดาว่าเกิดอะไรขึ้น\n\nต้นทุนจะปรากฏอย่างรวดเร็ว ทีมสร้างตรรกะการแยกวิเคราะห์ที่เปราะบางและเพิ่มกรณีพิเศษต่อ endpoint การ retry เสี่ยงเพราะไคลเอนต์ไม่สามารถบอกได้ว่า "ลองใหม่ภายหลัง" ต่างจาก "ข้อมูลของคุณผิด" ตั๋วซัพพอร์ตเพิ่มขึ้นเพราะข้อความที่เห็นคลุมเครือ และทีมของคุณไม่สามารถจับคู่กับบรรทัด log ฝั่งเซิร์ฟเวอร์ได้ง่าย\n\nสถานการณ์ที่พบบ่อย: แอปมือถือเรียกสาม endpoint ระหว่างการสมัครใช้งาน ตัวแรกคืน HTTP 400 พร้อมแผนที่ข้อผิดพลาดระดับฟิลด์ ตัวที่สองคืน HTTP 500 พร้อมสตริง stack trace และตัวที่สามคืน HTTP 200 พร้อม { ok: false } ทีมแอปส่งสามตัวจัดการข้อผิดพลาดต่างกัน และทีมแบ็กเอนด์ของคุณยังคงได้รับรายงานอย่าง "การสมัครใช้งานล้มเหลวเป็นครั้งคราว" โดยไม่มีเบาะแสชัดเจนว่าจะเริ่มจากตรงไหน\n\nเป้าหมายคือสัญญาที่คาดเดาได้เดียว ไคลเอนต์ควรอ่านได้อย่างเชื่อถือว่าเกิดอะไรขึ้น ทำให้รู้ว่าเป็นความผิดของใคร ควร retry หรือไม่ และมี request ID ที่พวกเขาสามารถวางในตั๋วซัพพอร์ตได้\n\nหมายเหตุขอบเขต: โฟกัสที่ JSON HTTP APIs (ไม่ใช่ gRPC) แต่แนวคิดเดียวกันใช้ได้กับทุกที่ที่คุณส่งข้อผิดพลาดกลับไปยังระบบอื่น\n\n## เป้าหมายง่าย ๆ: สัญญาเดียวที่ทุก endpoint ปฏิบัติตาม\n\nเลือกสัญญาเดียวที่ชัดเจนสำหรับข้อผิดพลาดและบังคับให้ทุก endpoint ปฏิบัติตาม "สอดคล้อง" หมายถึงรูปแบบ JSON เดียวกัน ความหมายของฟิลด์เดียวกัน และพฤติกรรมเดียวกันไม่ว่า handler ใดจะล้มเหลว เมื่อทำเช่นนั้น ไคลเอนต์จะหยุดเดาและเริ่มจัดการข้อผิดพลาด\n\nสัญญาที่เป็นประโยชน์ช่วยให้ไคลเอนต์ตัดสินใจว่าควรทำอะไรต่อ สำหรับแอปส่วนใหญ่ การตอบข้อผิดพลาดควรตอบสามคำถาม:\n\n- ฉันจะแก้ไขข้อมูลนำเข้าได้ไหม?\n- ควรลองอีกครั้งภายหลังไหม?\n- ฉันจำเป็นต้องติดต่อซัพพอร์ตหรือไม่?\n\nชุดกฎปฏิบัติได้:\n\n- รูปแบบการตอบเดียวสำหรับข้อผิดพลาดทั้งหมด\n- นโยบายสถานะโค้ดเดียวกัน (ประเภทข้อผิดพลาดเดียวกันแมปเป็นสถานะ HTTP เดียวกัน)\n- นโยบายข้อความที่ปลอดภัยเดียวกัน (อะไรที่ผู้ใช้เห็นได้กับอะไรที่เก็บเป็นภายใน)\n- จุดเชื่อมโยงสำหรับการสัมพันธ์ (request ID คืนกลับเพื่อให้ซัพพอร์ตค้นหาความล้มเหลวได้)\n\nตัดสินใจก่อนว่าต้องห้ามไม่ให้ปรากฏอะไรในคำตอบ รายการ "ห้าม" ทั่วไปรวมถึงชิ้นส่วน SQL, stack traces, ชื่อโฮสต์ภายใน, ความลับ และสตริงข้อผิดพลาดดิบจาก dependency\n\nรักษาการแยกให้ชัดเจน: ข้อความสั้นสำหรับผู้ใช้ (ปลอดภัย สุภาพ และปฏิบัติได้) และรายละเอียดภายใน (ข้อผิดพลาดเต็ม, stack, และบริบท) เก็บไว้ในบันทึก ตัวอย่างเช่น “ไม่สามารถบันทึกการเปลี่ยนแปลงของคุณ โปรดลองอีกครั้ง” ปลอดภัย ในขณะที่ “pq: duplicate key value violates unique constraint users_email_key” ไม่ปลอดภัย\n\nเมื่อทุก endpoint ปฏิบัติตามสัญญาเดียวกัน ไคลเอนต์สามารถสร้างตัวจัดการข้อผิดพลาดเดียวและนำกลับมาใช้ซ้ำได้ทุกที่\n\n## กำหนดสกีมาการตอบข้อผิดพลาดที่ไคลเอนต์พึ่งพาได้\n\nไคลเอนต์จะจัดการข้อผิดพลาดได้สะอาดก็ต่อเมื่อทุก endpoint ตอบในรูปแบบเดียวกัน เลือกซอง JSON เดียวและรักษาให้คงที่\n\nค่าเริ่มต้นปฏิบัติได้คือตัว error บวก request_id บนระดับบนสุด:\n\n```json{
"error": {
"code": "VALIDATION_FAILED",
"message": "Some fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address"
}
}
},
"request_id": "req_01HV..."
}
\n\nสถานะ HTTP ให้หมวดกว้าง (400, 401, 409, 500) ส่วน `error.code` ที่อ่านได้โดยเครื่องจะให้กรณีเฉพาะที่ไคลเอนต์สามารถแยกทางได้ การแยกนี้สำคัญเพราะปัญหาหลายอย่างอาจแชร์สถานะเดียวกัน แอปมือถืออาจแสดง UI ต่างกันสำหรับ `EMAIL_TAKEN` กับ `WEAK_PASSWORD` แม้ทั้งสองจะเป็น `400`\n\nเก็บ `error.message` ให้ปลอดภัยและเป็นมิตรกับคน มันควรช่วยผู้ใช้แก้ปัญหา แต่ไม่ควรรั่วรายละเอียดภายใน (SQL, stack traces, ชื่อผู้ให้บริการ, เส้นทางไฟล์)\n\nฟิลด์เสริมมีประโยชน์เมื่อยังคงคาดเดาได้:\n\n- ข้อผิดพลาดการตรวจสอบ: `details.fields` เป็นแผนที่จากฟิลด์ไปยังข้อความ\n- ขีดจำกัดอัตราหรือปัญหาชั่วคราว: `details.retry_after_seconds`\n- คำแนะนำเพิ่มเติม: `details.docs_hint` เป็นข้อความธรรมดา (ไม่ใช่ URL)\n\nเพื่อความเข้ากันได้ย้อนหลัง ให้ถือว่า `error.code` เป็นส่วนหนึ่งของสัญญา API ของคุณ เพิ่มรหัสใหม่ได้โดยไม่เปลี่ยนความหมายเก่า และเพิ่มเฉพาะฟิลด์ optionals สมมติว่าไคลเอนต์จะเมินฟิลด์ที่ไม่รู้จัก\n\n## ข้อผิดพลาดแบบ typed ใน Go: โมเดลที่ชัดเจนสำหรับ handlers ของคุณ\n\nการจัดการข้อผิดพลาดจะยุ่งเมื่อแต่ละ handler คิดวิธีสื่อความล้มเหลวเอง ชุดข้อผิดพลาด typed เล็ก ๆ แก้ปัญหานั้น: handler คืนค่า error types ที่รู้จัก และชั้นตอบสนองเดียวจะแปลงพวกมันเป็นคำตอบที่สอดคล้องกัน\n\nชุดเริ่มต้นที่ปฏิบัติได้คลุมเกือบทุก endpoint ได้แก่:\n\n- ValidationError (ข้อมูลนำเข้าผิด)\n- NotFoundError (ทรัพยากรไม่พบ)\n- ConflictError (ข้อจำกัด unique, state mismatch)\n- UnauthorizedError (ยังไม่ล็อกอินหรือไม่อนุญาต)\n- InternalError (อย่างอื่นทั้งหมด)\n\nกุญแจคือต้องรักษาเสถียรภาพในระดับบนสุด แม้สาเหตุรากจะเปลี่ยน คุณสามารถห่อสาเหตุระดับต่ำกว่า (SQL, เครือข่าย, การแยกวิเคราะห์ JSON) โดยยังคงคืนประเภทสาธารณะเดียวกันที่ middleware ตรวจจับได้\n\ngo
type NotFoundError struct {
Resource string
ID string
Err error // private cause
}
func (e NotFoundError) Error() string { return "not found" } func (e NotFoundError) Unwrap() error { return e.Err } json { "request_id": "req_01HV9N2K6Q7A3W1J9K8B", "error": { "code": "VALIDATION_FAILED", "message": "Some fields need attention.", "details": { "fields": { "email": "must be a valid email address" } } } } json { "request_id": "req_01HV9N3C2D0F0M3Q7Z9R", "error": { "code": "ALREADY_EXISTS", "message": "A customer with this email already exists." } } json { "request_id": "req_01HV9N3X8P2J7T4N6C1D", "error": { "code": "TEMPORARILY_UNAVAILABLE", "message": "We could not save your request right now. Please try again." } } ```\n\nเมื่อมีสัญญาเดียว ไคลเอนต์ตอบสนองสม่ำเสมอ:\n\n- 400: ทำเครื่องหมายฟิลด์โดยใช้ \n- 409: แนะนำผู้ใช้ไปยังขั้นตอนถัดไปที่ปลอดภัย\n- 503: ขอให้ retry และแสดง เป็น ID สำหรับซัพพอร์ต\n\nสำหรับซัพพอร์ต เดียวกันเป็นเส้นทางที่เร็วที่สุดไปยังสาเหตุจริงใน logs ภายใน โดยไม่เปิดเผย stack traces หรือข้อผิดพลาดฐานข้อมูล\n\n## กับดักทั่วไปที่ทำให้การจัดการข้อผิดพลาดแย่ลง\n\nวิธีที่เร็วที่สุดที่จะทำให้ไคลเอนต์รำคาญคือทำให้พวกเขาต้องเดา หาก endpoint หนึ่งคืน และอีก endpoint คืน ไคลเอนต์ทุกตัวจะกลายเป็นกองของกรณีพิเศษ และบักจะหลบซ่อนเป็นเวลาหลายสัปดาห์\n\nข้อผิดพลาดบางอย่างที่พบบ่อย:\n\n- คืน HTTP 200 พร้อมข้อผิดพลาดใน body หรือสลับสกีมาข้อผิดพลาดหลายแบบข้าม endpoint\n- เปิดเผยรายละเอียดภายในในข้อความผู้ใช้ เช่น ข้อผิดพลาด SQL, stack traces, IPs, ชื่อโฮสต์ของ dependency หรือเส้นทางไฟล์\n- ใช้ข้อความภาษามนุษย์เป็นตัวระบุเดียว แทนรหัสที่เสถียรที่ไคลเอนต์จะอ้างอิง\n- เปลี่ยนรหัสข้อผิดพลาดโดยไม่ระมัดระวัง (หรือใช้รหัสเดียวกันกับปัญหาต่างกัน) ทำให้ไคลเอนต์ที่เขียนตามพฤติกรรมเก่าพัง\n- เพิ่ม เฉพาะเมื่อเกิดข้อผิดพลาด ทำให้ไม่สามารถเชื่อมโยงรายงานผู้ใช้กับการเรียกที่สำเร็จซึ่งอาจกระตุ้นปัญหาในภายหลังได้\n\nการรั่วไหลของรายละเอียดภายในเป็นกับดักที่ง่ายที่สุดที่จะตก หลายครั้ง handler คืน เพราะสะดวก แล้วชื่อ constraint หรือข้อความจากบุคคลที่สามไปจบใน production responses เก็บข้อความสำหรับผู้ใช้ให้ปลอดภัยและสั้น และใส่สาเหตุโดยละเอียดไว้ใน logs\n\nการพึ่งพาข้อความเพียงอย่างเดียวเป็นปัญหาที่แอบสะสม หากไคลเอนต์ต้อง parse ประโยคภาษาอังกฤษเช่น “email already exists” คุณจะไม่สามารถเปลี่ยนคำพูดโดยไม่ทำให้ตรรกะพัง รหัสข้อผิดพลาดที่เสถียรให้คุณปรับข้อความ แปลมัน และยังรักษาพฤติกรรมให้คงที่\n\nถือว่ารหัสข้อผิดพลาดเป็นส่วนหนึ่งของสัญญาสาธารณะของคุณ หากจำเป็นต้องเปลี่ยน ให้เพิ่มรหัสใหม่และรักษารหัสเดิมให้ทำงานไประยะหนึ่ง แม้ทั้งสองจะแมปไปยังสถานะ HTTP เดียวกัน\n\nสุดท้าย ให้รวมฟิลด์ เดียวกันในทุกการตอบกลับ ไม่ว่าจะสำเร็จหรือไม่ เมื่อผู้ใช้พูดว่า “มันทำงานแล้วแล้วพัง” ID เดียวมักจะช่วยประหยัดเวลาเดาวิธีการได้มาก\n\n## เช็คลิสต์ด่วนก่อนปล่อยของ\n\nก่อนปล่อย ทำการตรวจสอบความสอดคล้องอย่างรวดเร็ว:\n\n- รูปร่างข้อผิดพลาดเดียวทุกที่ ทุก endpoint คืนช่อง JSON เดียวกัน (เช่น: , , )\n- รหัสข้อผิดพลาดที่เสถียรและครอบคลุม เก็บรหัสสั้นและตรงไปตรงมา (, , , ) เพิ่มการทดสอบเพื่อป้องกันไม่ให้ handlers คืนรหัสไม่รู้จักโดยไม่ได้ตั้งใจ\n- กฎการแมปสถานะเดียวกัน ตัดสินใจว่าแต่ละประเภทข้อผิดพลาดแมปเป็นสถานะ HTTP ใด และใช้มันในที่เดียวร่วมกัน\n- request ID สองทาง ส่ง เสมอและบันทึกมันสำหรับทุกคำขอ รวมทั้ง panic และ timeout\n- ข้อความที่ปลอดภัยเป็นค่าเริ่มต้น ข้อความที่เห็นโดยผู้ใช้ควรสั้น ชัดเจน และปฏิบัติได้ ห้าม stack traces, ข้อผิดพลาด SQL, หรือนามผู้ขาย\n\nหลังจากนั้น ตรวจสอบตัวอย่าง endpoint ด้วยมือ ลองทำให้เกิดข้อผิดพลาดการตรวจสอบ ข้อผิดพลาดทรัพยากรไม่พบ และข้อผิดพลาดไม่คาดคิด หากการตอบกลับดูต่างกันข้าม endpoints (ฟิลด์เปลี่ยน รหัสสถานะเบี่ยงเบน ข้อความเผยข้อมูลภายใน) ให้แก้ pipeline ร่วมก่อนเพิ่มฟีเจอร์ต่อ\n\nกฎปฏิบัติ: ถ้าข้อความใดช่วยผู้โจมตีหรือสับสนผู้ใช้ปกติ ข้อความนั้นควรอยู่ใน logs ไม่ใช่ในการตอบกลับ\n\n## ขั้นตอนถัดไป: มาตรฐานเดี๋ยวนี้และรักษาให้สม่ำเสมอต่อไป\n\nเขียนสัญญาข้อผิดพลาดที่คุณต้องการให้ทุก endpoint ปฏิบัติตาม แม้ API จะใช้งานอยู่แล้วก็ตาม สัญญาร่วม (สถานะ, รหัสข้อผิดพลาดที่เสถียร, ข้อความที่ปลอดภัย, และ request_id) เป็นวิธีที่เร็วที่สุดที่จะทำให้ข้อผิดพลาดคาดเดาได้สำหรับไคลเอนต์\n\nจากนั้นย้ายแบบค่อยเป็นค่อยไป เก็บ handlers เดิมไว้ แต่ส่งความล้มเหลวของพวกมันผ่าน mapper เดียวที่แปลงข้อผิดพลาดภายในเป็นรูปแบบการตอบสาธารณะของคุณ นี่ปรับปรุงความสอดคล้องโดยไม่ต้อง rewrite ครั้งใหญ่ และป้องกันไม่ให้ endpoint ใหม่คิดรูปแบบใหม่\n\nรักษาคลังรหัสข้อผิดพลาดขนาดเล็กและถือว่าเป็นส่วนของ API เมื่อใครต้องการเพิ่มรหัสใหม่ ให้ตรวจสอบสั้น ๆ: มันใหม่จริงหรือไม่? ชื่อชัดเจนหรือไม่? และแมปไปยังสถานะ HTTP ถูกต้องหรือไม่?\n\nเพิ่มการทดสอบไม่กี่อย่างที่จะจับการเบี่ยงเบน:\n\n- ทุกการตอบข้อผิดพลาดมี .\n- รหัสสถานะตรงกับประเภทข้อผิดพลาด (ไม่ใช่กับข้อความข้อผิดพลาด).\n- มีอยู่และมาจากคลังรหัส.\n- ปลอดภัยและไม่รวมรายละเอียดภายใน.\n- ข้อผิดพลาดไม่รู้จักย้อนกลับเป็น 500 พร้อมข้อความทั่วไป.\n\nถ้าคุณกำลังสร้างแบ็กเอนด์ Go ตั้งแต่เริ่ม การล็อกสัญญาให้เร็วช่วยได้มาก ตัวอย่างเช่น Koder.ai (koder.ai) รวมโหมดวางแผนที่คุณสามารถกำหนดข้อบังคับเช่นสกีมาข้อผิดพลาดและคลังรหัสล่วงหน้า แล้วรักษา handlers ให้สอดคล้องเมื่อตัว API เติบโตขึ้น.
ใช้รูปแบบ JSON เดียวสำหรับการตอบข้อผิดพลาดทุกครั้งในทุก endpoint ค่าเริ่มต้นที่ใช้งานได้คือ request_id บนระดับบนสุดบวกกับอ็อบเจ็กต์ error ที่มี code, message และ details (ถ้ามี) เพื่อให้ไคลเอนต์แยกแยะและตอบสนองได้อย่างเชื่อถือได้.
ส่งค่า error.message เป็นประโยคสั้น ๆ ที่ปลอดภัยสำหรับผู้ใช้ และเก็บสาเหตุจริงไว้ในบันทึกเซิร์ฟเวอร์ ห้ามส่งข้อผิดพลาดฐานข้อมูลดิบ, stack trace, ชื่อโฮสต์ภายใน หรือข้อความจากผู้ให้บริการ แม้ในช่วงพัฒนา.
ใช้ error.code ที่เสถียรสำหรับตรรกะของเครื่องและให้ HTTP status บรรยายหมวดกว้าง ๆ ไคลเอนต์ควรตัดสินใจจาก error.code (เช่น ALREADY_EXISTS) และใช้สถานะเป็นแนวทาง (เช่น 409 หมายถึงความขัดแย้งของสถานะ).
ใช้ 400 เมื่อคำขอไม่สามารถแยกแยะหรือแปลความได้อย่างเชื่อถือ (JSON ผิดรูปแบบ, ชนิดข้อมูลผิด) และใช้ 422 เมื่อคำขอถูก parse ได้แต่ขัดกับกฎทางธุรกิจ (เช่น รูปแบบอีเมลไม่ถูกต้อง, รหัสผ่านสั้นเกินไป).
ใช้ 409 เมื่อข้อมูลที่ส่งมาถูกต้องตามรูปแบบแต่ไม่สามารถนำไปใช้ได้เพราะขัดแย้งกับสถานะปัจจุบัน (อีเมลถูกใช้งานแล้ว, เวอร์ชันไม่ตรง) และใช้ 422 สำหรับการตรวจสอบระดับฟิลด์ที่แก้ด้วยการเปลี่ยนค่าเอง.
สร้างชุดข้อผิดพลาด typed เล็ก ๆ (validation, not found, conflict, unauthorized, internal) และให้ handlers คืนค่าเหล่านั้น จากนั้นใช้ translator ร่วมเพื่อแมปประเภทเหล่านี้เป็น status code และรูปแบบ JSON มาตรฐาน.
ต้องส่ง request_id ในทุกการตอบกลับ ไม่ว่าเป็นความสำเร็จหรือความล้มเหลว และบันทึกมันบนทุกบรรทัดของ log ถ้าผู้ใช้รายงานปัญหา ID เดียวมักพอที่จะหาเส้นทางล้มเหลวใน logs ได้อย่างแม่นยำ.
ตอบ 200 ก็ต่อเมื่อการดำเนินการสำเร็จเท่านั้น ให้ใช้ 4xx/5xx สำหรับข้อผิดพลาด การซ่อนข้อผิดพลาดหลัง 200 บังคับให้ไคลเอนต์ parse ฟิลด์ใน body และสร้างพฤติกรรมไม่สอดคล้องกันข้าม endpoints.
อย่า retry โดยอัตโนมัติสำหรับ 400, 401, 403, 404, 409, 422 เพราะการ retry โดยไม่มีการเปลี่ยนแปลงมักไม่ช่วย ให้ retry สำหรับ 503 และบางกรณีของ 429 หลังรอ หากรองรับ idempotency keys การ retry จะปลอดภัยขึ้นสำหรับ POST ที่ล้มชั่วคราว.
ล็อกสัญญาไว้ด้วยการทดสอบ “golden” ที่ยืนยัน status, error.code และการมี request_id เพิ่มรหัสข้อผิดพลาดใหม่โดยไม่เปลี่ยนความหมายเดิม และเพิ่มฟิลด์เป็น optional เพื่อให้ไคลเอนต์เก่าทำงานต่อได้.
\n\nใน handler ของคุณ ให้คืน `NotFoundError{Resource: "user", ID: id, Err: err}` แทนการรั่ว `sql.ErrNoRows` โดยตรง\n\nเมื่อตรวจสอบข้อผิดพลาด ให้ใช้ `errors.As` สำหรับชนิดแบบกำหนดเองและ `errors.Is` สำหรับ sentinel errors Sentinel errors (เช่น `var ErrUnauthorized = errors.New("unauthorized")`) ใช้ได้ในกรณีง่าย ๆ แต่ custom types มีประโยชน์เมื่อต้องการบริบทสาธารณะที่ปลอดภัย (เช่นทรัพยากรใดหายไป) โดยไม่เปลี่ยนสัญญาการตอบสาธารณะ\n\nเข้มงวดในสิ่งที่แนบมาด้วย:\n\n- สิ่งที่เป็นสาธารณะ (ปลอดภัยสำหรับไคลเอนต์): ข้อความสั้น ๆ, รหัสที่เสถียร, และบางครั้งชื่อฟิลด์สำหรับการตรวจสอบ\n- สิ่งที่เป็นส่วนตัว (เก็บใน logs เท่านั้น): `Err` ที่ซ้อนอยู่, ข้อมูล stack, ข้อผิดพลาด SQL ดิบ, โทเค็น, ข้อมูลผู้ใช้\n\nการแยกนี้ช่วยให้คุณช่วยไคลเอนต์โดยไม่เปิดเผยรายละเอียดภายใน\n\n## แมปประเภทข้อผิดพลาดกับรหัสสถานะ HTTP อย่างสอดคล้อง\n\nเมื่อคุณมีข้อผิดพลาดแบบ typed งานต่อไปคือการทำสิ่งน่าเบื่อแต่สำคัญ: ประเภทข้อผิดพลาดเดียวกันควรให้สถานะ HTTP เดียวกันเสมอ ไคลเอนต์จะสร้างตรรกะรอบ ๆ มัน\n\nการแมปปฏิบัติได้ที่ใช้ได้กับ API ส่วนใหญ่:\n\n| Error type (ตัวอย่าง) | Status | เมื่อใดที่ใช้ |\n|---|---:|---|\n| BadRequest (JSON ผิดรูปแบบ, พารามิเตอร์ query หาย) | 400 | คำขอไม่ถูกต้องในระดับโปรโตคอลหรือรูปแบบ |\n| Unauthenticated (ไม่มี/โทเค็นไม่ถูกต้อง) | 401 | ไคลเอนต์ต้องยืนยันตัวตน |\n| Forbidden (ไม่มีสิทธิ์) | 403 | ยืนยันแล้วแต่การเข้าถึงไม่อนุญาต |\n| NotFound (ID ทรัพยากรไม่มีอยู่) | 404 | ทรัพยากรที่ร้องขอไม่มีอยู่ (หรือคุณเลือกซ่อนการมีอยู่) |\n| Conflict (unique constraint, version mismatch) | 409 | คำขอรูปแบบถูกต้องแต่ขัดกับสถานะปัจจุบัน |\n| ValidationFailed (กฎฟิลด์) | 422 | รูปร่าง OK แต่การตรวจสอบทางธุรกิจล้มเหลว |\n| RateLimited | 429 | คำขอเกินจำนวนในหน้าต่างเวลา |\n| Internal (ข้อผิดพลาดไม่คาดคิด) | 500 | บั๊กหรือความล้มเหลวที่ไม่คาดคิด |\n| Unavailable (dependency down, timeout, maintenance) | 503 | ปัญหาฝั่งเซิร์ฟเวอร์ชั่วคราว |\n\nสองความแตกต่างที่ป้องกันความสับสนได้มาก:\n\n- 400 vs 422: ใช้ 400 เมื่อคุณไม่สามารถตีความคำขอได้อย่างเชื่อถือ (JSON ผิดรูปแบบ, ชนิดข้อมูลผิด) ใช้ 422 เมื่อคุณ parse ได้ แต่ค่าที่ให้มาไม่ยอมรับได้\n- 409 vs 422: ใช้ 422 สำหรับการตรวจสอบฟิลด์ (รหัสผ่านสั้นเกินไป) ใช้ 409 เมื่อข้อมูลถูกต้องแต่ไม่สามารถประยุกต์ใช้ได้เพราะสถานะ (อีเมลถูกใช้แล้ว, คำสั่งถูกส่งแล้ว, optimistic lock ล้มเหลว)\n\nคำแนะนำเรื่อง retry สำคัญ:\n\n- ปกติปลอดภัยที่จะ retry: 503 และบางครั้ง 429 (หลังรอ)\n- ปกติไม่ปลอดภัยที่จะ retry โดยไม่มีการเปลี่ยนแปลง: 400, 401, 403, 404, 409, 422\n- หากการดำเนินการเป็น idempotent (PUT ด้วย body เดิม หรือ POST ที่มี idempotency key) การ retry จะปลอดภัยขึ้นแม้หลังข้อผิดพลาดชั่วคราว\n\n## Request IDs: วิธีที่เร็วที่สุดในการดีบั๊กปัญหาฝั่งไคลเอนต์\n\nrequest ID คือค่าสั้นที่ไม่ซ้ำซึ่งระบุการเรียก API ครั้งหนึ่ง ๆ แบบ end-to-end หากไคลเอนต์เห็นมันในทุกการตอบกลับ การซัพพอร์ตจะง่ายขึ้น: “ส่ง request ID มาให้ฉัน” มักเพียงพอที่จะหา log และความล้มเหลวที่แน่นอนได้\n\nนิสัยนี้ให้ผลทั้งคำตอบที่สำเร็จและข้อผิดพลาด\n\n### กฎการสร้างและการส่งผ่าน\n\nใช้กฎชัดเจนอย่างเดียว: ถ้าไคลเอนต์ส่ง request ID มา ให้เก็บมันไว้ ถ้าไม่ ให้สร้างใหม่\n\n- รับ ID เข้ามาจาก header ชื่อเดียว (เลือกชื่อเดียวและเอกสารประกาศ เช่น `X-Request-Id`)\n- ถ้า header หายหรือว่าง ให้สร้าง ID ใหม่ที่ edge (middleware) แล้วแนบไว้กับ context ของ request\n- ห้ามเปลี่ยน ID ระหว่างการประมวลผล ส่งต่อไปยังการเรียก downstream (DB, บริการอื่น) ผ่าน context หรือ headers\n\nใส่ request ID ในสามที่:\n\n- Response header (ชื่อ header เดียวกับที่รับ)\n- Response body (เป็น `request_id` ในสกีมามาตรฐาน)\n- Logs (เป็นฟิลด์เชิงโครงสร้างในทุกบรรทัด log)\n\n### งานแบบแบตช์และแบบอะซิงค์\n\nสำหรับ endpoints ที่เป็นแบตช์หรืองานแบ็กกราวด์ ให้เก็บ parent request ID ตัวอย่าง: ไคลเอนต์อัปโหลด 200 แถว 12 แถวล้มในการตรวจสอบ และคุณ enqueue งาน คืน `request_id` เดียวสำหรับการเรียกทั้งหมด และใส่ `parent_request_id` บนแต่ละงานและข้อผิดพลาดต่อไอเท็ม วิธีนี้จะสามารถสืบจาก “การอัปโหลดหนึ่งครั้ง” แม้มันจะแยกเป็นหลายงานได้\n\n## การบันทึกและเมตริกโดยไม่รั่วไหลรายละเอียดภายใน\n\nไคลเอนต์ต้องการการตอบข้อผิดพลาดที่ชัดเจนและเสถียร ขณะที่บันทึกของคุณต้องการความจริงที่ยุ่งเหยิง แยกสองโลกนี้ไว้: คืนข้อความที่ปลอดภัยและรหัสข้อผิดพลาดสาธารณะให้ไคลเอนต์ ในขณะที่บันทึกรายละเอียดสาเหตุ, stack, และบริบทในฝั่งเซิร์ฟเวอร์\n\nบันทึกเหตุการณ์เชิงโครงสร้างหนึ่งรายการสำหรับทุกการตอบข้อผิดพลาด ให้สามารถค้นหาโดย `request_id`\n\nฟิลด์ที่ควรรักษาให้สอดคล้องมีดังนี้:\n\n- request_id\n- user_id หรือ account_id (เมื่อยืนยันตัวตนแล้ว)\n- รหัสข้อผิดพลาดสาธารณะและสถานะ HTTP\n- ชื่อ handler/route และ method\n- รายละเอียดข้อผิดพลาดภายใน (สาเหตุที่ห่อไว้, ข้อผิดพลาดการตรวจสอบฟิลด์, upstream timeout)\n\nเก็บรายละเอียดภายในไว้เฉพาะในบันทึกเซิร์ฟเวอร์ (หรือที่จัดเก็บข้อผิดพลาดภายใน) ไคลเอนต์ไม่ควรเห็นข้อผิดพลาดฐานข้อมูลดิบ, ข้อความคิวรี, stack traces, หรือข้อความจากผู้ให้บริการ หากคุณมีหลายบริการ ฟิลด์ภายในเช่น `source` (api, db, auth, upstream) จะช่วยให้การไตรเอกซ์เร็วยิ่งขึ้น\n\nสังเกต endpoints ที่เกิดเสียงดังและข้อผิดพลาดที่ถูกจำกัดอัตรา หาก endpoint สามารถผลิต 429 หรือ 400 เดิม ๆ หลายพันครั้งต่อนาที ให้หลีกเลี่ยงการสแปม log: ตัวอย่างเช่น sample เหตุการณ์ที่ซ้ำซ้อน หรือลดความรุนแรงในขณะที่ยังนับในเมตริก\n\nเมตริกจับปัญหาก่อน logs มากกว่า ติดตามจำนวนแยกตามสถานะ HTTP และรหัสข้อผิดพลาด และเตือนเมื่อมีการพุ่งขึ้นอย่างรวดเร็ว ถ้า `RATE_LIMITED` พุ่งขึ้น 10 เท่าหลัง deploy คุณจะเห็นเร็วแม้ logs ถูก sample\n\n## ขั้นตอนทีละขั้น: ปรับใช้ pipeline ข้อผิดพลาดที่สอดคล้องใน Go\n\nวิธีที่ง่ายที่สุดในการทำให้ข้อผิดพลาดสอดคล้องคือหยุดจัดการมัน "ทุกที่" และส่งผ่านมันผ่าน pipeline เล็ก ๆ นั้น Pipeline นี้ตัดสินใจว่าสิ่งใดที่ไคลเอนต์เห็นและสิ่งใดที่คุณเก็บไว้ใน logs\n\n### Pipeline ใน 5 ขั้นตอนปฏิบัติได้\n\nเริ่มด้วยชุดรหัสข้อผิดพลาดเล็ก ๆ ที่ไคลเอนต์พึ่งพาได้ (เช่น: `INVALID_ARGUMENT`, `NOT_FOUND`, `UNAUTHORIZED`, `CONFLICT`, `INTERNAL`) ห่อพวกมันในข้อผิดพลาด typed ที่เปิดเผยเฉพาะฟิลด์สาธารณะที่ปลอดภัย (code, safe message, รายละเอียด optional เช่นฟิลด์ที่ผิด) เก็บสาเหตุภายในเป็นส่วนตัว\n\nแล้ว implement ฟังก์ชัน translator เดียวที่แปลงข้อผิดพลาดใด ๆ เป็น `(statusCode, responseBody)` นี่คือที่ที่ typed errors แมปเป็น HTTP status และข้อผิดพลาดที่ไม่รู้จักกลายเป็น 500 ที่ปลอดภัย\n\nต่อมาเพิ่ม middleware ที่:\n\n- รับรองว่าทุกคำขอมี `request_id`\n- recover จาก panic\n\npanic ไม่ควรเท stack trace ให้ไคลเอนต์ คืน 500 ปกติพร้อมข้อความทั่วไป และบันทึก panic แบบเต็มกับ `request_id` เดียวกัน\n\nสุดท้าย เปลี่ยน handlers ของคุณให้คืน `error` แทนการเขียนการตอบโดยตรง ตัว wrapper เดียวสามารถเรียก handler, รัน translator, และเขียน JSON ในรูปแบบมาตรฐาน\n\nเช็คลิสต์กะทัดรัด:\n\n- กำหนด typed errors พร้อมฟิลด์ปลอดภัยและรหัสที่เสถียร\n- แปลงข้อผิดพลาดเป็นสถานะและ JSON ในที่เดียว\n- เพิ่ม middleware สำหรับ request ID และ recovery จาก panic\n- ให้ handlers คืนค่า error ไม่ใช่ response\n- เพิ่ม golden tests สำหรับ translator และ wrapper\n\nการทดสอบแบบ golden สำคัญเพราะยึดสัญญา หากใครสักคนเปลี่ยนข้อความหรือรหัสสถานะ การทดสอบจะล้มก่อนที่ไคลเอนต์จะถูกประหลาดใจ\n\n## ตัวอย่าง: endpoint เดียว สามกรณีล้มเหลว การตอบที่คาดเดาได้\n\nสมมติ endpoint หนึ่ง: แอปไคลเอนต์สร้างเร็กคอร์ดลูกค้า\n\n`POST /v1/customers` ด้วย JSON เช่น `{ "email": "[email protected]", "name": "Pat" }` เซิร์ฟเวอร์คืนรูปแบบข้อผิดพลาดเดียวกันเสมอและรวม `request_id` เสมอ\n\n### 1) ข้อผิดพลาดการตรวจสอบ (400)\n\nอีเมลหายหรือรูปแบบไม่ถูกต้อง ไคลเอนต์สามารถเน้นฟิลด์ได้\n\n\n\n### 2) ความขัดแย้ง (409)\n\nอีเมลมีอยู่แล้ว ไคลเอนต์สามารถแนะนำให้ลงชื่อเข้าใช้หรือเลือกอีเมลอื่น\n\n\n\n### 3) ความล้มเหลวชั่วคราว (503)\n\ndependency ล่ม ไคลเอนต์สามารถ retry แบบ backoff และแสดงข้อความสุภาพ\n\ndetails.fieldsrequest_idrequest_id{ "error": "..." }{ "message": "..." }request_iderr.Error()request_iderror.codeerror.messagerequest_idVALIDATION_FAILEDNOT_FOUNDCONFLICTUNAUTHORIZEDrequest_idrequest_iderror.codeerror.message