ข้ามไปยังเนื้อหา

Error Values

type error ของ Go คือ interface ที่เรียบง่ายที่สุดใน standard library:

type error interface {
Error() string
}

type ใดก็ตามที่ implement method เดียว — Error() string — จะ satisfy interface นี้และสามารถใช้ได้ทุกที่ที่ต้องการ error ค่า nil เป็น error ที่ถูกต้องและหมายถึง “ไม่มี error เกิดขึ้น” โดยสากล ไม่มีอะไรพิเศษเกี่ยวกับ type error ในระดับภาษา เป็นแค่ interface ธรรมดาที่ predeclare ไว้ล่วงหน้าเท่านั้น

convention แบบ idiomatic ของ Go คือการคืนค่า (T, error) เป็นสองค่าสุดท้ายจากฟังก์ชันที่อาจล้มเหลวได้ caller รับทั้งคู่เสมอและต้องตรวจสอบ error ก่อนที่จะเชื่อถือผลลัพธ์:

result, err := riskyOperation()
if err != nil {
// ห้ามใช้ result — ไม่มีความหมาย
return fmt.Errorf("riskyOperation: %v", err)
}
// err เป็น nil: result ถูกต้องใช้ได้

การคืนค่า error ให้ caller เป็นบรรทัดฐาน การกลืน error อย่างเงียบ ๆ — ละเว้น err ด้วย _ — ควรทำอย่างตั้งใจและเกิดขึ้นน้อยมาก ใช้เฉพาะในกรณีที่ความล้มเหลวไม่มีผลจริง ๆ (เช่น การปิดไฟล์แบบ read-only หลังจากอ่านข้อมูลทั้งหมดสำเร็จแล้ว)

Go มีสองวิธีมาตรฐานในการสร้าง error value ใหม่:

errors.New สร้าง error จาก string คงที่ ใช้เมื่อข้อความไม่เคยเปลี่ยนแปลง:

var errTimeout = errors.New("operation timed out")

fmt.Errorf สร้างข้อความ error แบบ formatted ใช้เมื่อต้องการรวม runtime values เช่น ชื่อ, key หรือจำนวน:

err := fmt.Errorf("user %d not found in database %q", userID, dbName)

หมายเหตุ: %v format ค่าแบบ inline และไม่แนบ wrapping metadata การ wrap — ใช้ %w เพื่อเชื่อม error ต่อกันเพื่อให้ errors.Is และ errors.As สามารถ unwrap chain ได้ — จะกล่าวถึงในบทเรียนถัดไป

sentinel error คือตัวแปรระดับ package ที่แทนเงื่อนไขความล้มเหลวเฉพาะที่รู้จักกันดี:

var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")

การประกาศ sentinel ในระดับ package ให้ caller ทดสอบด้วย identity ใช้ errors.Is แทน == เพื่อให้การตรวจสอบยังทำงานได้แม้ว่า error จะถูก wrap ด้วย context เพิ่มเติม:

if errors.Is(err, ErrNotFound) {
// จัดการกรณี not-found
}

Sentinel เหมาะที่สุดสำหรับเงื่อนไขที่ caller ต้องการ branch แยกทาง หลีกเลี่ยงการสร้าง sentinel สำหรับทุกความล้มเหลวที่เป็นไปได้ — ใช้เฉพาะสำหรับ error ที่ caller จัดการแตกต่างจากกรณีล้มเหลวทั่วไปอย่างแท้จริง

การตรวจสอบ if err != nil อย่างชัดเจนอาจดูซ้ำซากในตอนแรก แต่ออกแบบมาอย่างตั้งใจ Go ถือว่าทุกความล้มเหลวเป็นจุดตัดสินใจ: คุณต้องเลือกว่าจะ propagate error, wrap พร้อม context, retry, fallback ไปยังค่า default หรือ log แล้วดำเนินต่อ ภาษาไม่ได้ตัดสินใจแทนคุณ

วินัยนี้ทำให้โค้ดมองเห็นทุก call ที่อาจเกิด error ได้ที่ callsite เมื่ออ่านฟังก์ชัน Go คุณจะเห็นชัดเจนว่า operation ใดที่อาจล้มเหลวได้และ caller ทำอะไรกับแต่ละกรณี — ไม่มีการ propagate แบบ implicit ไม่มีเส้นทาง exception ที่มองไม่เห็น

package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func lookup(key string) (int, error) {
switch key {
case "age":
return 30, nil
case "secret":
return 0, ErrPermission
default:
return 0, fmt.Errorf("lookup %q: %w", key, ErrNotFound)
}
}
func main() {
v, err := lookup("age")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("age:", v)
_, err = lookup("x")
if err != nil {
fmt.Println("error:", err)
fmt.Println("is ErrNotFound:", errors.Is(err, ErrNotFound))
}
_, err = lookup("secret")
if err != nil {
fmt.Println("error:", err)
fmt.Println("is ErrPermission:", errors.Is(err, ErrPermission))
}
}
สิ่งที่ได้ประโยชน์ต้นทุน
error interface (1 method)implement ง่าย, compose ได้ไม่มี stack trace โดยตรง
errors.Is()match sentinel ใน chain ลึกแค่ไหนก็ได้ต้อง wrap error ด้วย %w ให้ถูกต้องก่อน
errors.As()extract concrete error type พร้อม fieldต้อง type assertion ใน chain
sentinel errorsเปรียบเทียบง่าย, self-documentingเป็น public API — refactor ยากถ้า name ต้องเปลี่ยน
  • return nil error หมายถึง function สำเร็จเสมอ — ถ้า caller ละเว้น error ด้วย _ โดยไม่ตั้งใจ error ถูก ignore โดยสมบูรณ์
  • errors.New() และ fmt.Errorf() เหมือนกัน — fmt.Errorf กับ %w สามารถ wrap error chain ได้, errors.New ไม่สามารถ
  • error message ควร capitalize — Go convention: error message ไม่ capitalize และไม่มี punctuation ท้าย เช่น "not found" ไม่ใช่ "Not found."
  • panic ดีกว่า return error สำหรับ critical error — ใช้ panic เฉพาะสำหรับ programming error ที่ should not happen, ไม่ใช่ expected failure

💡 ตัวอย่างจากของจริง

Kubernetes คืน error chain ที่ wrap context ของทุก layer — apiserver error → controller error → client error — ทำให้ log มี context ครบ

net/http: http.ErrServerClosed เป็น sentinel error สำหรับ graceful shutdown — caller ตรวจด้วย errors.Is(err, http.ErrServerClosed)

database/sql: sql.ErrNoRows เป็น sentinel ที่ทุก Go database driver ใช้ร่วมกัน

การคืนค่า nil เป็น error หมายความว่าอะไร?
ฟังก์ชันใดสร้าง error value จาก string คงที่?
sentinel error ประกาศด้วยรูปแบบใด?
ควรใช้ fmt.Errorf แทน errors.New เมื่อใด?