1 · error เป็นแค่ interface ที่มี method เดียว
// นิยามจริงใน Go — built-in ไม่ต้อง import อะไร
type error interface {
Error() string
}
แค่นั้นจริง ๆ อะไรก็ตามที่มี method Error() string
คือ error ได้ทันที — ตรงตาม duck typing จาก
บทเรียน 10
แปลว่าคุณสร้าง error ที่พก field อะไรก็ได้ ติดไปด้วย
(field = ช่องข้อมูลใน struct เหมือนช่องกรอกในแบบฟอร์ม)
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func ValidateAge(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Message: "must not be negative",
}
}
return nil
}
ตอนนี้ผู้เรียกได้มากกว่าข้อความ — ได้ Field ไปใช้
highlight ช่องกรอกที่ผิดในหน้าเว็บได้
2 · errors.New() ต่างจาก fmt.Errorf() ยังไง
errors.New() | fmt.Errorf() | |
|---|---|---|
| ใช้เมื่อ | ข้อความคงที่ ไม่มีตัวแปร | ต้องแทรกข้อมูล หรือห่อ error เดิม |
| ตัวอย่าง | errors.New("insufficient balance") | fmt.Errorf("read %q: %w", name, err) |
| ห่อ error ได้ไหม | ❌ | ✅ ด้วย %w |
เทียบด้วย == | ✅ (จึงทำ sentinel ได้) | ❌ สร้างใหม่ทุกครั้ง |
สไลด์หน้านี้แสดง func Withdraw(balance, amount int) error
แล้วขึ้นบรรทัดใหม่ค่อยเปิด { — ถ้าพิมพ์ตามนั้นตรง ๆ
จะ compile ไม่ผ่าน ผมรันแล้วได้
syntax error: unexpected semicolon or newline before {
เพราะ Go เติม semicolon ท้ายบรรทัดให้อัตโนมัติ
ปีกกาเปิดต้องอยู่บรรทัดเดียวกับ func เสมอ
(น่าจะเป็นแค่การตัดบรรทัดตอนทำสไลด์ แต่ถ้าลอกไปจะงงว่าพังเพราะอะไร)
errors.New() — ข้อความคงที่
func Withdraw(balance, amount int) error {
if amount > balance {
return errors.New(
"insufficient balance",
)
}
return nil
}
fmt.Errorf() — เติมข้อมูล/ห่อ
func LoadConfig(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf(
"read config %q: %w", filename, err,
)
}
_ = data
return nil
}
3 · Sentinel Error — error ที่มีชื่อให้เทียบได้
ปัญหาที่หัวข้อนี้แก้: ชั้นบนอยากรู้ว่า “พังเพราะอะไร” จะได้ตัดสินใจถูก
แต่ถ้าไปดูจากข้อความของ error วันไหนมีคนแก้ข้อความ โค้ดก็พังตาม
ทางออกคือประกาศตัว error ไว้ล่วงหน้าเป็นตัวแปรที่มีชื่อ
แล้วเทียบที่ตัวมันเอง ไม่ใช่ที่ข้อความ · ตัวแปรแบบนี้เรียกว่า sentinel
package user
// นิยมประกาศระดับ package และตั้งชื่อขึ้นต้นด้วย Err
var (
ErrUserNotFound = errors.New("user not found")
ErrEmailAlreadyUsed = errors.New("email already used")
ErrPermissionDenied = errors.New("permission denied")
)
ประโยชน์คือผู้เรียกตัดสินใจจาก error ได้ โดยไม่ต้องเทียบข้อความ:
if err == user.ErrUserNotFound { // ใช้ได้ แต่เปราะมาก
if errors.Is(err, user.ErrUserNotFound) { // ✓ ใช้อันนี้เสมอ
ทำไมต้อง errors.Is ทั้งที่ == ก็ได้:
== เทียบได้เฉพาะตอน error ตัวนั้นลอยขึ้นมาถึงคุณตรง ๆ
แต่ในระบบจริงชั้นกลางมักห่อมันด้วย fmt.Errorf("...: %w", err)
ก่อน (หัวข้อ 4) — พอถูกห่อแล้ว err คือ error ตัวใหม่
== จึงได้ false ทันที ส่วน errors.Is
จะแกะห่อไล่ลงไปเรื่อย ๆ จนเจอต้นตอ
เขียน errors.Is ไว้ตั้งแต่แรกดีกว่า
เพราะวันที่มีคนเพิ่มการห่อเข้ามา โค้ดคุณจะไม่พังเงียบ ๆ
if errors.Is(err, user.ErrUserNotFound) {
// ตอบ 404
} else if errors.Is(err, user.ErrPermissionDenied) {
// ตอบ 403
}
if err.Error() == "user not found" { // ✗ อย่าทำ
ข้อความ error คือสิ่งที่มนุษย์อ่าน ไม่ใช่ API
วันไหนมีคนแก้เป็น "User not found" (ตัว U ใหญ่)
หรือใส่ id ต่อท้าย โค้ดคุณจะพังเงียบ ๆ โดยไม่มี compile error เตือน
ตอนจ่ายเงินที่ซูเปอร์ฯ แคชเชียร์ยิงบาร์โค้ด ไม่ได้อ่านชื่อบนป้ายแล้วพิมพ์เอง
เพราะป้ายเปลี่ยนเมื่อไหร่ก็ได้ — วันนี้ “นมสด 200 มล.” พรุ่งนี้พิมพ์ใหม่เป็น “นมโคสด 200ml”
ถ้าระบบผูกกับข้อความบนป้าย วันที่ป้ายเปลี่ยนก็คิดเงินผิดทันที ส่วนบาร์โค้ดยังเป็นตัวเดิม
· ป้ายชื่อสินค้า = err.Error() — มีไว้ให้คนอ่าน
· บาร์โค้ด = ErrUserNotFound — มีไว้ให้โค้ดเทียบ
· การยิงบาร์โค้ด = errors.Is(err, ErrUserNotFound)
ที่ไม่เหมือนกัน: บาร์โค้ดเป็นตัวเลข สินค้าคนละชิ้นแต่รุ่นเดียวกันใช้เลขร่วมกันได้
แต่ sentinel เทียบกันที่ตัวมันเอง ตัวเดียวในหน่วยความจำ —
ต่อให้ประกาศ errors.New("user not found") ขึ้นใหม่อีกตัวที่ข้อความเหมือนกันเป๊ะ
errors.Is ก็ยังบอกว่าคนละตัวกัน
(ข้อยกเว้นที่เจอไม่บ่อย แต่บอกไว้ก่อนจะได้ไม่ตกใจ: error บางชนิดเขียน method
Is(error) bool ของตัวเองไว้ เพื่อยอมให้ match ข้ามตัวได้ —
เช่น errors.Is(syscall.ENOENT, fs.ErrNotExist) ได้ true
ทั้งที่สองตัวนี้คนละชนิดคนละก้อนกันเลย)
- sentinel error
- “บาร์โค้ด” ของ error — error ที่ประกาศไว้ล่วงหน้าเป็นตัวแปรระดับ package เพื่อให้คนอื่นเทียบได้ · เวลาถามให้พูดว่า “package นี้มี sentinel error ตัวไหนให้เช็คบ้าง” จะได้คำตอบตรงกว่าถามว่า “มัน error อะไรได้บ้าง”
- errors.Is
- ฟังก์ชันที่ถามว่า “error ก้อนนี้ใช่ตัวนั้นไหม” —
และมันไล่ดูให้ทั้งโซ่ ไม่ใช่ดูแค่ชั้นนอกสุด
(ยังไม่เห็นภาพว่า “โซ่” คืออะไรก็ไม่เป็นไร หัวข้อ 4 อธิบายให้) ·
ประโยคที่ใช้ได้จริง: “ตรงนี้เช็คด้วย errors.Is นะ อย่าใช้
==” - เปราะ (brittle)
- คำที่ reviewer ใช้ตอนติโค้ดที่พังง่ายเวลามีคนแก้อย่างอื่นที่ดูไม่น่าเกี่ยวกัน · การเทียบ error ด้วยข้อความคือตัวอย่างคลาสสิกของคำนี้
4 · Error Chain — ห่อ error โดยไม่ทำต้นตอหาย
ระบบจริงมีหลายชั้น แต่ละชั้นควรเติมบริบทของตัวเองลงไป
โดยไม่กลบข้อมูลของชั้นล่าง
ชั้นที่พูดถึงในโค้ดข้างล่างคือ repository (ชั้นที่คุยกับฐานข้อมูล) →
service (ชั้นที่ตัดสินใจตามกฎของงาน) →
handler (ชั้นที่รับคำขอจากผู้ใช้และตอบกลับ)
สั่งของออนไลน์ กล่องที่มาถึงบ้านมักมีกล่องเล็กซ้อนอยู่ข้างใน —
ร้านห่อกล่องของโรงงานอีกที แล้วขนส่งแปะใบปะหน้าทับอีกชั้น
ใบปะหน้าชั้นนอกบอกว่า “ผ่านมือใครมาบ้าง” แต่ของจริงยังอยู่ข้างในครบ
· %w = ห่อทับ เก็บกล่องเดิมไว้ข้างในจริง ๆ
· %v = ถ่ายรูปกล่องเดิมมาแปะหน้ากล่องใหม่ แล้วทิ้งของเดิม —
หน้ากล่องอ่านเหมือนกันเป๊ะ แต่แกะเข้าไปไม่มีอะไร นี่คือเหตุผลที่บั๊กตัวนี้หายากมาก
ที่ไม่เหมือนกัน: พัสดุจริงต้องฉีกกล่องถึงจะรู้ว่าข้างในมีอะไร และฉีกแล้วห่อคืนไม่ได้
แต่ errors.Is/errors.As มองทะลุได้โดยไม่ทำลายอะไร —
ตรวจเสร็จ error ก้อนเดิมยังส่งต่อขึ้นไปให้ชั้นบนตรวจซ้ำได้อีก
// Repository:
func (r UserRepository) Find(id string) (User, error) {
user, err := queryDatabase(id)
if err != nil {
return User{}, fmt.Errorf("query user: %w", err)
}
return user, nil
}
// Service: เติมบริบทอีกชั้น
func (s UserService) GetProfile(id string) (User, error) {
user, err := s.repository.Find(id)
if err != nil {
return User{}, fmt.Errorf("get profile %s: %w", id, err)
}
return user, nil
}
ข้อความสุดท้ายที่ handler เห็น: get profile U-101: query user: user not found └──── Service ────┘└─ Repository ─┘└─ ต้นตอ ─┘ แต่ละชั้นเติมคำนำหน้า ต้นตอยังอยู่ท้ายสุดเสมอ และ errors.Is() ยังมองทะลุถึงต้นตอได้
err := fmt.Errorf("query user: %v", ErrUserNotFound)
// ↑ %v ไม่ใช่ %w
fmt.Println(errors.Is(err, ErrUserNotFound))
พิมพ์ true หรือ false?
false — ผมรันทดสอบแล้ว
%v แค่แปลง error เป็นข้อความ แล้วโยนตัว error เดิมทิ้ง —
ข้อความสุดท้ายอ่านเหมือนกันเป๊ะ แต่โซ่ขาด
ส่วน %w เก็บ error เดิมไว้ข้างในให้ errors.Is ตามไปเจอ
นี่คือบั๊กที่หาไม่เจอที่สุดในเรื่อง error เพราะ log สวยงามทุกอย่าง
แต่ errors.Is คืน false เงียบ ๆ
- wrap / ห่อ error
- เอา error เดิมใส่ไว้ข้างใน error ใหม่ · ประโยคที่ใช้ได้จริง:
“ตรงนี้ wrap ด้วย
%wหรือยัง” — สั้น ๆ แค่นี้ senior เข้าใจทันทีว่าถามอะไร - error chain
- “กล่องซ้อนกล่อง” ทั้งสาย ตั้งแต่ชั้นนอกสุดลงไปถึงต้นตอ · พูดว่า “ต้นตอใน error chain คืออะไร” จะสื่อกว่าถามว่า “มัน error อะไรกันแน่”
- unwrap
- การแกะออกทีละชั้น ·
errors.Is/Asแกะให้อัตโนมัติ และโดยปกติจะแกะต่อลงไปได้เมื่อชั้นนั้นมี methodUnwrap()(หัวข้อ 6) - context ของ error
- ข้อมูลที่แต่ละชั้นเติมลงไปว่า “ตอนพังกำลังทำอะไรอยู่” เช่น
get profile U-101:· โดนติว่า “error นี้ไม่มี context” = อ่านแล้วไม่รู้ว่าพังตอนไหน ไม่ได้แปลว่าข้อความสั้นไป
ระวัง — คนละเรื่องกับcontext.Contextที่ใช้สั่ง cancel/timeout นะ ชื่อพ้องกันเฉย ๆ
5 · errors.Is กับ errors.As — คนละงานกัน
errors.Is(err, target) | errors.As(err, &target) | |
|---|---|---|
| ถามว่า | "ในโซ่นี้มี error ตัวนี้ ไหม" | "ในโซ่นี้มี error ชนิดนี้ ไหม" |
| คู่กับ | sentinel (errors.New) | custom error struct |
| ได้อะไรกลับ | bool อย่างเดียว | bool + ตัว error จริงพร้อม field |
| อาร์กิวเมนต์ 2 | ตัว error | pointer ไปยังตัวแปร (ต้องมี &) |
กระเป๋าใบเดียวกัน เจ้าหน้าที่ถามได้สองแบบ
· “มีไฟแช็กไหม” — ตอบ มี/ไม่มี ก็จบ ไม่ต้องเอาออกมา → นี่คือ errors.Is
· “มีของเหลวไหม ถ้ามีขอเอาออกมาวางในถาด” —
ต้องหยิบออกมาก่อน ถึงจะอ่านฉลากได้ว่ากี่มิลลิลิตร → นี่คือ errors.As
และนี่คือเหตุผลที่ errors.As ต้องมี &:
คุณต้องยื่น “ถาด” ให้เขาวางของ ·
var validationErr ValidationError = ถาดเปล่า ·
&validationErr = การยื่นถาดใบนั้นให้เขา
ถ้าไม่ยื่นถาด เจ้าหน้าที่ไม่ทำงานต่อให้เลย ตะโกนหยุดทันที —
ในโค้ดจริงคือเขียน errors.As(err, validationErr) ตกเครื่องหมาย & ไป
อันนี้ คอมไพล์ผ่านนะ แต่พอรันแล้วโปรแกรม panic ว่า
errors: target must be a non-nil pointer — ไม่ได้คืน false
และไม่ได้ลดชั้นไปทำงานแบบ errors.Is ให้
เคสนี้คนละเรื่องกับกล่อง “value กับ pointer ใช้แทนกันไม่ได้” ที่อยู่ถัดลงไป:
อันนั้นคือยื่นถาดผิดชนิด ซึ่งได้ false เงียบ ๆ จริง
ส่วนอันนี้คือไม่ยื่นถาดเลย โปรแกรมหยุดกลางคัน
ที่ไม่เหมือนกัน: เจ้าหน้าที่รื้อกระเป๋าเองได้ทุกซอก
แต่ errors.As เข้าไปชั้นในได้ก็ต่อเมื่อแต่ละชั้นยอมเปิดให้ —
โดยปกติคือชั้นนั้นต้องมี Unwrap() ถ้าชั้นไหนไม่เปิด การค้นจบตรงนั้นทันที
// errors.Is — เช็คว่าใช่ตัวนั้นไหม
err := FindUser("U-101")
if errors.Is(err, ErrUserNotFound) {
fmt.Println("show 404 page")
}
// errors.As — ดึงตัวจริงออกมาใช้ field
// ⚠️ ชนิดของตัวแปรต้องตรงกับที่ "คืนมาจริง" — หัวข้อ 1 คืน ValidationError{} (ค่า)
var validationErr ValidationError
err := ValidateAge(-1)
if errors.As(err, &validationErr) {
fmt.Println("invalid field:", validationErr.Field)
// ↑ ได้ field มาใช้ต่อ
}
ถ้าโค้ด return ValidationError{...} (ค่า) แต่รับด้วย
var e *ValidationError — ผมรันแล้วได้ false
ไม่มี error ไม่มี panic (โปรแกรมหยุดกลางคัน) เงียบสนิท เพราะ errors.As ถามว่า
"ในโซ่มีตัวที่เป็น ชนิดนี้เป๊ะ ๆ ไหม" และ ValidationError
กับ *ValidationError คือคนละชนิดกัน (กฎเดียวกับ
บทเรียน 09)
วิธีกัน: ดูว่าโค้ดคุณ return อะไร แล้วประกาศตัวรับให้ตรงแบบนั้น
— คืน &AppError{} ก็รับด้วย var e *AppError
คืน ValidationError{} ก็รับด้วย var e ValidationError
จำง่าย ๆ: Is = "ใช่อันนี้ไหม" ตอบได้แค่ใช่/ไม่ใช่ ·
As = "เป็นชนิดนี้ไหม ถ้าใช่ขอตัวจริงด้วย"
สังเกตว่า errors.As ก็คือ type assertion จาก
บทเรียน 11
ที่เดินตามโซ่ให้ด้วยนั่นเอง
- errors.As
- “ในโซ่มี error ชนิดนี้ ไหม ถ้ามีขอตัวจริงด้วย” · ประโยคที่ใช้ได้จริง: “ตรงนี้ต้องใช้ errors.As เพราะผมอยากได้ field ข้างใน ไม่ใช่แค่รู้ว่าใช่หรือเปล่า”
- target
- “ถาด” ที่เรายื่นให้ — ตัวแปรที่
errors.Asจะเอาค่าไปใส่ให้ · เอกสาร Go เรียกอาร์กิวเมนต์ตัวที่สองว่าtargetตรง ๆ - custom error type
- error ที่เราสร้างชนิดขึ้นมาเอง — ส่วนใหญ่เป็น struct เพื่อพก field เพิ่ม
(เช่น
ValidationError) แต่จริง ๆ จะเป็นชนิดอะไรก็ได้ที่มี methodError() stringเช่นtype ErrCode int· ตรงข้ามกับ sentinel ที่มีแค่ข้อความ · ถามว่า “ตัวนี้เป็น sentinel หรือ custom error type” จะได้คำตอบตรงกว่ามาก
6 · Custom Error Struct — แบบที่ใช้จริงในโปรดักชัน
หัวข้อนี้เอาทุกอย่างมารวมกัน: แทนที่จะคืน error ที่มีแค่ข้อความ เราคืน struct ที่พกทุกอย่างที่ชั้นบนต้องใช้ตัดสินใจไปด้วย (โปรดักชัน = ระบบตัวจริงที่ผู้ใช้ใช้อยู่ ไม่ใช่เครื่องเราตอนพัฒนา)
type AppError struct {
Op string // เกิดที่ไหน — "UserService.GetUser"
Code string // รหัสให้เครื่องอ่าน — "USER_NOT_FOUND"
HTTPStatus int // ตอบ client ยังไง — 404
Message string // ข้อความให้ "ผู้ใช้" อ่าน
Err error // error เดิมที่ห่อไว้
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s: %v", e.Op, e.Message, e.Err)
}
return fmt.Sprintf("%s: %s", e.Op, e.Message)
}
// ⚠️ ขาดอันนี้ไม่ได้ — ไม่งั้น errors.Is/As มองทะลุไม่ได้
func (e *AppError) Unwrap() error {
return e.Err
}
ใช้ที่ชั้น service
func GetUser(id string) (User, error) {
user, err := repository.FindByID(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
return User{}, &AppError{
Op: "UserService.GetUser",
Code: "USER_NOT_FOUND",
HTTPStatus: http.StatusNotFound,
Message: "ไม่พบผู้ใช้งาน",
Err: err,
}
}
return User{}, &AppError{
Op: "UserService.GetUser",
Code: "INTERNAL_ERROR",
HTTPStatus: http.StatusInternalServerError,
Message: "ไม่สามารถโหลดข้อมูลผู้ใช้ได้",
Err: err,
}
}
return user, nil
}
ใช้ที่ชั้น handler
user, err := service.GetUser(id)
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
http.Error(w, appErr.Message, appErr.HTTPStatus)
return
}
// error ที่ไม่รู้จัก — อย่าเผยรายละเอียดออกไป
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
ผลรันจริงของโครงนี้:
err.Error() = UserService.GetUser: ไม่พบผู้ใช้งาน: query user U-101: user not found
errors.Is(err, ErrUserNotFound) = true ← ทะลุถึงต้นตอ
errors.As(err, &appErr) = true
appErr.Code = USER_NOT_FOUND | HTTPStatus = 404
แยกข้อความสำหรับผู้ใช้ ออกจากข้อมูลสำหรับ developer — ผู้ใช้เห็น "ไม่พบผู้ใช้งาน" ส่วน log เก็บโซ่เต็ม ๆ ว่าพังตรงไหน และ handler ตัดสิน HTTP status ได้โดยไม่ต้องรู้จัก database เลยสักนิด
7 · กับดักร้ายแรง: nil ที่ไม่ nil
func doWork() error {
var e *AppError = nil
return e // คืน pointer ที่เป็น nil
}
if doWork() == nil {
fmt.Println("ไม่มี error")
} else {
fmt.Println("มี error!")
}
พิมพ์อะไรออกมา?
มี error! — ผมรันทดสอบแล้ว
doWork() == nil ได้ false
ย้อนกลับไปดูรูปกล่องใน บทเรียน 11 — interface เก็บ สองช่อง:
┌──────────────┬────────────┐
│ *AppError │ nil │ ← ช่อง type ไม่ว่าง!
└──────────────┴────────────┘
interface จะ == nil ก็ต่อเมื่อ ทั้งสองช่อง ว่าง
ทางแก้: อย่าประกาศตัวแปรเป็น *AppError แล้วคืนมันไปตรง ๆ
ให้ return nil ตรง ๆ ในเส้นทางที่สำเร็จ:
func doWork() error {
if somethingWrong {
return &AppError{...}
}
return nil // ✓ nil ของแท้
}
ตกลงกับเพื่อนไว้ว่า “ถ้ามีปัญหาให้ส่งจดหมายมา ถ้าไม่มีก็ไม่ต้องส่ง”
วันหนึ่งเปิดตู้จดหมายแล้วเจอซอง ก็สรุปทันทีว่า “มีปัญหาแล้ว”
ทั้งที่แกะออกมาข้างในไม่มีกระดาษสักแผ่น
Go มองแบบเดียวกันเป๊ะ ๆ · error เป็น interface ที่มีสองช่อง:
ช่อง “ซองแบบไหน” (type) กับ ช่อง “ข้างในมีอะไร” (value)
การคืน var e *AppError = nil ออกไป = ส่งซองเปล่ามา —
ช่อง type มีคำว่า *AppError อยู่ ไม่ว่าง ผู้เรียกจึงเห็นว่า err != nil
ที่ไม่เหมือนกัน: ซองเปล่ายังแกะดูได้อย่างปลอดภัย
แต่ error ก้อนนี้ถ้าเผลอเรียก err.Error() แล้วข้างในไปแตะ field
โปรแกรมจะหยุดกลางคัน (panic) ทันที — ไม่ใช่แค่ตอบผิด แต่ล้มทั้งคำขอนั้นไปเลย
- typed nil (nil ที่ไม่ nil)
- “ซองเปล่าที่ยังนับว่ามีซอง” · ถ้าเจออาการนี้ ให้พูดกับ senior หรือพิมพ์ถาม AI ว่า
“น่าจะโดน typed nil ตอนคืน
*AppErrorเป็นerror” — คำนี้คำเดียวพาไปหาคำตอบได้เลย - interface value
- ค่าที่อยู่ใน interface ซึ่งเก็บสองช่องคู่กันเสมอ (type, value) ·
จะ
== nilก็ต่อเมื่อว่างทั้งสองช่อง - panic
- โปรแกรมหยุดกลางคันเพราะเจอสิ่งที่ไปต่อไม่ได้ · ต่างจาก error ตรงที่ error คือค่าที่คืนมาให้เราจัดการ ส่วน panic คือล้มไปเลย ถ้าไม่มีใครรับไว้
- nil pointer dereference
- สาเหตุของ panic ที่เจอบ่อยที่สุด — ไปหยิบของจากที่อยู่ที่ไม่มีอะไรอยู่ ·
ใน log จะเห็นเป็น
invalid memory address or nil pointer dereference
8 · ทดสอบความจำ
1. ใช้ %v แทน %w ใน fmt.Errorf มีผลยังไง?
นี่คือกับดักที่อันตรายที่สุด เพราะ log ออกมาสวยงามเหมือนเดิมทุกอย่าง
แต่ errors.Is คืน false เงียบ ๆ →
handler ตอบ 500 แทนที่จะเป็น 404 %w เท่านั้นที่เก็บ error เดิมไว้ในโซ่
2. custom error struct ขาด method Unwrap() จะเป็นยังไง?
errors.Is/As เดินตามโซ่โดยเรียก Unwrap()
ไปเรื่อย ๆ — ไม่มี Unwrap() = โซ่จบตรงนั้น
field Err ยังอยู่ครบ แต่ไม่มีใครตามไปดูได้
3. อยากได้ Field จาก ValidationError ในโซ่ ใช้อะไร?
errors.Is ตอบได้แค่ bool เอา field มาไม่ได้ ·
errors.As(err, &validationErr) เดินตามโซ่ให้แล้ว
ยัดตัวจริงลงในตัวแปรที่คุณส่ง pointer ไป →
ใช้ validationErr.Field ได้เลย · อย่าลืม &
4. คืน var e *AppError = nil จากฟังก์ชันที่ประกาศคืน error?
interface เก็บ type + value — ช่อง type มี *AppError
อยู่ ไม่ว่าง จึงไม่เท่ากับ nil ทั้งที่ค่าข้างในเป็น nil
วิธีกัน: เขียน return nil ตรง ๆ ในเส้นทางที่สำเร็จ
อย่าคืนตัวแปรที่เป็น pointer type
9 · ลงมือทำ
ภารกิจ: ระบบค้นหาสินค้าและจัดการ Error Chain (workshop จากสไลด์)
- ประกาศ sentinel:
var ErrProductNotFound = errors.New("product not found") - ชั้น repository:
findProduct(code string) (Product, error)— ถ้าไม่พบให้คืนErrProductNotFound - ชั้น service:
getProduct(code string) (Product, error)— ห่อด้วยfmt.Errorf("find product %q: %w", code, err)แล้วครอบด้วย&AppError{Code: "PRODUCT_NOT_FOUND", Message: "ไม่พบสินค้าที่ต้องการ", Err: ...}
ภารกิจนี้ใช้AppErrorแบบย่อ (แค่ 3 field) และError()คืนfmt.Sprintf("%s: %v", e.Message, e.Err)— เพิ่มOp/HTTPStatusทีหลังได้ - อย่าลืมเขียน
func (e *AppError) Unwrap() error - ชั้น main: ตรวจด้วย
errors.Is()และerrors.As()ให้ได้ผลลัพธ์:พบ ErrProductNotFound ใน Error Chain code: PRODUCT_NOT_FOUND message: ไม่พบสินค้าที่ต้องการ error: ไม่พบสินค้าที่ต้องการ: find product "P999": product not found - ห้ามตรวจ error ด้วยการเทียบข้อความ string — กฎจากสไลด์
- ท้าทาย 1: เปลี่ยน
%wเป็น%vในข้อ 3 แล้วรันใหม่ — ข้อความเปลี่ยนไหม?errors.Isเปลี่ยนไหม? นี่คือการพิสูจน์กับดักในหัวข้อ 4 ด้วยมือตัวเอง - ท้าทาย 2: ลบ
Unwrap()ออก แล้วดูว่าerrors.Isกับerrors.Asอันไหนพัง
สปอยล์ผลรันจริง:errors.Is→falseแต่errors.As→trueเหมือนเดิม ทำไมAsถึงยังรอด? คำใบ้:errตัวนอกสุดคือ*AppErrorอยู่แล้ว ไม่ต้องเดินโซ่ก็เจอ
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ:
Working with Errors in Go 1.13
— เขียนโดยทีม Go ตอนที่เพิ่ม %w/Is/As เข้ามา
จึงอธิบายว่าแต่ละอันเกิดมาแก้ปัญหาอะไร ไม่ใช่แค่ syntax
ประมาณ 15 นาที และเป็นแหล่งที่ควรอ่านก่อนออกแบบ error ของโปรเจกต์จริง
💬 ติดตรงไหนถามได้เลย — โครง AppError
มี 5 field ซึ่งอาจดูเยอะเกินไปสำหรับโปรเจกต์เล็ก
ถ้าอยากคุยว่าโปรเจกต์ของคุณควรมี field ไหนบ้าง บอกมาได้
เป็นการตัดสินใจที่ขึ้นกับว่าใครเป็นคนอ่าน error ของคุณ
อ้างอิง
- Working with Errors in Go 1.13 — ที่มาของ
%w,errors.Is,errors.As - Error handling and Go — ปรัชญาว่าทำไม error เป็นค่า
- pkg.go.dev — errors — เอกสาร
Is/As/Join - Go Code Review Comments — Error Strings — ข้อความ error ควรหน้าตายังไง
- Effective Go — Errors