การ Wrap Error
ทำไมต้อง Wrap Error?
หัวข้อที่มีชื่อว่า “ทำไมต้อง Wrap Error?”เมื่อฟังก์ชันเรียกใช้อีกฟังก์ชันแล้วล้มเหลว การคืน error เดิมทำให้เสียบริบท ผู้เรียกเห็นแค่ “database error” แต่ไม่รู้ว่า operation ใดที่ทำให้เกิด request ใดที่เกี่ยวข้อง หรือเกิดที่ layer ไหน การ wrap เพิ่มชั้น context ว่ากำลังทำอะไรอยู่ โดยยังเก็บ error เดิมไว้ให้ตรวจสอบได้
รูปแบบ idiomatic ใน Go:
if err != nil { return fmt.Errorf("description: %w", err)}%w คือกุญแจสำคัญ เพราะ wrap error เดิมไว้ภายใน error ใหม่ รักษา chain ทั้งหมดไว้สำหรับ caller ที่ต้องการตรวจสอบ
fmt.Errorf และ %w
หัวข้อที่มีชื่อว่า “fmt.Errorf และ %w”fmt.Errorf สร้าง error ใหม่ที่มี message รวมค่าอื่น แต่วิธีรวม error argument ขึ้นอยู่กับ verb ที่เลือก:
%vจัดรูปแบบ message string ของ error และฝังเป็น plain text type ของ error เดิมหายไป chain ถูกตัด%wบันทึก error เดิมไว้ใน error ใหม่ message ยังมี message เดิม แต่ error เดิมยังสามารถเข้าถึงได้โดยใช้โปรแกรม
base := errors.New("connection refused")
// %v — message only, chain is severede1 := fmt.Errorf("dial: %v", base)fmt.Println(errors.Is(e1, base)) // false
// %w — wraps, chain is preservede2 := fmt.Errorf("dial: %w", base)fmt.Println(errors.Is(e2, base)) // trueใช้ %w เมื่อต้องการให้ caller upstream สามารถระบุหรือดึง error เดิมออกมาได้ ใช้ %v เฉพาะเมื่อตั้งใจจะทิ้ง error type เดิม เช่น เมื่อ log และ chain ไม่มีความสำคัญ
errors.Is — ตรวจสอบ identity ผ่าน chain
หัวข้อที่มีชื่อว่า “errors.Is — ตรวจสอบ identity ผ่าน chain”errors.Is(err, target) เดิน unwrap chain ทั้งหมดของ err และคืนค่า true ถ้า error ใดใน chain ตรงกับ target การ match ทำโดย identity (==) ตามค่าเริ่มต้น ไม่ใช่ตาม message string
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("getUser: %w", fmt.Errorf("queryDB: %w", ErrNotFound))
fmt.Println(errors.Is(err, ErrNotFound)) // true — found deep in the chainทำงานได้ไม่ว่า target จะถูก wrap ลึกแค่ไหน นิยาม sentinel error ครั้งเดียว (เป็น var ระดับ package) และตรวจสอบได้ทุกที่ใน call stack
errors.As — ดึง type จาก chain
หัวข้อที่มีชื่อว่า “errors.As — ดึง type จาก chain”errors.As(err, &target) เดิน chain เช่นกัน แต่แทนที่จะตรวจสอบ identity จะตรวจสอบ type ถ้า error ใดใน chain สามารถ assign ให้ type ของ target ได้ จะทำการ assign และคืน true
var ve *ValidationErrorif errors.As(err, &ve) { fmt.Println(ve.Field) // access fields on the concrete type}ใช้ errors.As เมื่อมี custom error type ที่เก็บข้อมูลเพิ่มเติม เช่น field names, HTTP status codes, retry hints และต้องการเข้าถึง field เหล่านั้น ไม่ใช่แค่ตรวจว่า error มีอยู่
Custom error types
หัวข้อที่มีชื่อว่า “Custom error types”struct ใดก็ตามที่ implement Error() string ตรงตาม error interface custom types ช่วยให้เก็บ structured context ควบคู่กับ message:
type ValidationError struct { Field string Message string}
func (e *ValidationError) Error() string { return fmt.Sprintf("validation: field %q %s", e.Field, e.Message)}เพื่อให้ custom error type เป็นส่วนหนึ่งของ wrapping chain ให้ implement Unwrap() error:
type AppError struct { Code int Err error}
func (e *AppError) Error() string { return fmt.Sprintf("code %d: %v", e.Code, e.Err) }func (e *AppError) Unwrap() error { return e.Err }เมื่อมี Unwrap แล้ว errors.Is และ errors.As สามารถมองผ่าน AppError เพื่อไปถึง error ที่ครอบอยู่ข้างใน
package main
import ( "errors" "fmt")
var ErrDatabase = errors.New("database error")
type ValidationError struct { Field string Message string}
func (e *ValidationError) Error() string { return fmt.Sprintf("validation: field %q %s", e.Field, e.Message)}
func queryDB(id int) (string, error) { if id < 0 { return "", ErrDatabase } if id == 0 { return "", &ValidationError{Field: "id", Message: "must be positive"} } return "Alice", nil}
func getUser(id int) (string, error) { name, err := queryDB(id) if err != nil { return "", fmt.Errorf("getUser %d: %w", id, err) } return name, nil}
func main() { _, err := getUser(-1) if err != nil { fmt.Println(err) fmt.Println("is ErrDatabase:", errors.Is(err, ErrDatabase)) }
_, err = getUser(0) if err != nil { fmt.Println(err) var ve *ValidationError if errors.As(err, &ve) { fmt.Printf("field=%q msg=%q\n", ve.Field, ve.Message) } }
name, err := getUser(1) if err != nil { fmt.Println("error:", err) return } fmt.Println("name:", name)}Loading Go runtime (first run only, ~8 MB)…
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| fmt.Errorf(“%w”, err) | wrap พร้อม context, errors.Is/As ทำงานได้ | verbose กว่า return err เฉยๆ |
| errors.Is() ใน chain | ตรวจ sentinel ใน chain ลึกแค่ไหนก็ได้ | ช้ากว่า == comparison (traversal) |
| custom error type | เก็บ structured context (field, code, status) | ต้อง maintain type, implement Error() |
| %v vs %w ใน fmt.Errorf | %v แสดง message ใน string, %w preserve chain | สับสนง่ายระหว่างสอง verb |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- wrap error ทุกชั้นยิ่งให้ context ยิ่งดี — wrapping ลึกเกินไปทำ error message อ่านยาก ให้ wrap เฉพาะเมื่อ caller ต้องการ context นั้น
- errors.Unwrap() คืน nil เสมอถ้าไม่ได้ wrap — errors.Unwrap() เรียกบน non-wrapped error คืน nil — ไม่ panic
- %w ใน fmt.Errorf รองรับ error เดียวเท่านั้น — Go 1.20+ รองรับ
fmt.Errorf("msg: %w and %w", e1, e2)เพื่อ wrap หลาย error - errors.Is เปรียบเทียบ message string — errors.Is ตรวจ identity (
==) ไม่ใช่ message string — sentinel errors ต้องเป็น package-level var
💡 ตัวอย่างจากของจริง
Kubernetes controller ทุก layer เพิ่ม context ด้วย wrapping —
fmt.Errorf("reconcile pod %s: %w", pod.Name, err)— ทำให้ stack ของ log มีความหมายGo database drivers wrap sql error เพิ่ม query, table, column context ให้ developer debug ได้ทันที