Go errors .Is กับ .As เป็นเพื่อนกัน
เวลาเริ่มเขียน Go — เรามักเช็ค error ด้วย == ตรง ๆ:
if err == sql.ErrNoRows {
// handle
}
แต่พอเริ่มใช้ fmt.Errorf("...: %w", err) wrap error — == ใช้ไม่ได้แล้ว
err := getUser()
// err = "GetUser: sql: no rows in result set"
// err == sql.ErrNoRows → false! เพราะมันถูก wrap แล้ว
Go เลยมี 2 ฟังก์ชันให้: errors.Is และ errors.As — หน้าตาคล้ายกัน แต่หน้าที่ต่างกัน
fmt.Errorf กับ %w — จุดเริ่มต้นของทุกอย่าง
ก่อนจะเข้าใจ Is กับ As — ต้องเข้าใจ %w ก่อน
import "fmt"
func getUser(id string) (*User, error) {
user, err := db.QueryRow(...)
if err != nil {
return nil, fmt.Errorf("getUser(%s): %w", id, err)
// ↑↑↑↑↑↑↑↑↑↑↑
// wrap err ด้วย %w
}
return user, nil
}
%w คือ verb พิเศษที่ "ห่อ" error ไว้ข้างใน — ไม่ใช่แค่ต่อ string แบบ %v
original := sql.ErrNoRows
wrapped := fmt.Errorf("getUser: %w", original)
fmt.Println(wrapped.Error())
// "getUser: sql: no rows in result set"
// หน้าตาภายใน:
// wrapped = &wrapError{
// msg: "getUser",
// err: sql.ErrNoRows ← เก็บ original ไว้!
// }
%w ทำให้เกิด error chain — error ลูกถูกซ้อนอยู่ใน error แม่ — และ Is กับ As รู้วิธีเดิน chain นี้
errors.Is — "ใช่ error นี้หรือเปล่า?"
import "errors"
err := getUser("42")
// err = "getUser(42): sql: no rows in result set"
if errors.Is(err, sql.ErrNoRows) {
// ✅ true — Is เดินตาม chain แล้วเจอ sql.ErrNoRows ข้างใน
fmt.Println("not found")
}
errors.Is ถามว่า: "error นี้ (หรือ error ที่ถูก wrap ไว้ข้างใน) คือ sql.ErrNoRows หรือไม่?"
หลักการทำงาน:
1. เปรียบเทียบ err == sql.ErrNoRows? → false (คนละ type)
2. err มี Unwrap() ไหม? → มี (เพราะใช้ %w)
3. unwrap → "sql: no rows..."
4. เปรียบเทียบ unwrapped == sql.ErrNoRows? → ✅ true!
5. ตอบ true
ตัวอย่างเพิ่มเติม:
// เช็คหลายระดับ
err := fmt.Errorf("handler: %w",
fmt.Errorf("service: %w",
sql.ErrNoRows))
errors.Is(err, sql.ErrNoRows) // ✅ true — ลงลึก 2 ชั้น
errors.As — "เป็น error type นี้หรือเปล่า?"
var pathErr *os.PathError
err := os.Open("/nonexistent/file")
if errors.As(err, &pathErr) {
// ✅ true — err เป็น *os.PathError (หรือมี *os.PathError อยู่ใน chain)
fmt.Println("path:", pathErr.Path) // "/nonexistent/file"
fmt.Println("op:", pathErr.Op) // "open"
}
errors.As ถามว่า: "error นี้ (หรือ error ที่ถูก wrap ไว้ข้างใน) เป็น type *os.PathError หรือไม่? — ถ้าใช่ ใส่ค่าให้ตัวแปรที่ส่งเข้าไป"
หลักการทำงาน:
1. err เป็น *os.PathError? → ใช่!
2. ใส่ค่า err เข้าไปใน pathErr (ผ่าน pointer ที่ส่งเข้า)
3. ตอบ true
ข้อแตกต่างสำคัญ: As รับ pointer to error type — ไม่ใช่ error value — เพราะมันต้อง assign ค่ากลับมา
// ❌ ผิด — As ต้องการ pointer
if errors.As(err, os.PathError{}) { ... }
// ✅ ถูก — pointer to error type
var target *os.PathError
if errors.As(err, &target) { ... }
// ✅ ถูก — ใช้ interface ก็ได้
var netErr net.Error
if errors.As(err, &netErr) { ... }
Is vs As — ต่างกันตรงไหน?
errors.Is |
errors.As |
|
|---|---|---|
| คำถาม | "ใช่ error นี้หรือเปล่า?" | "เป็น type นี้หรือเปล่า?" |
| เทียบกับ | value (sentinel error) | type (error struct) |
| รับ parameter | error value โดยตรง | pointer to error type |
| ได้อะไรกลับ | true/false |
true/false + ใส่ค่าให้ตัวแปร |
| ใช้เมื่อ | อยากรู้ว่าใช่ error ที่รู้จักไหม | อยากรู้ว่าเป็น type อะไร และเข้าถึง field |
| ตัวอย่าง | errors.Is(err, sql.ErrNoRows) |
errors.As(err, &pathErr) |
วิธีจำ — "Is เช็คชื่อ, As เช็ค type"
Is → "Is this THAT error?" → เปรียบเทียบ value
As → "As WHAT type?" → ตรวจสอบ type + extract
ตัวอย่างที่ใช้ทั้งคู่
func handleError(err error) {
// 1. เช็คก่อนว่า "ใช่ not found ไหม?"
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("not found")
return
}
// 2. เช็คต่อว่า "เป็น path error ไหม?" — ถ้าใช่ ดูว่าไฟล์ไหน
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("can't open %s\n", pathErr.Path)
return
}
// 3. เช็ค transient network error
var netErr net.Error
if errors.As(err, &netErr) && netErr.Temporary() {
fmt.Println("temporary — retry")
return
}
fmt.Printf("unexpected: %v\n", err)
}
Custom Error Types — ใช้กับ Is และ As
// 1. Sentinel error — ใช้กับ Is
var ErrPermission = errors.New("permission denied")
// 2. Custom error type — ใช้กับ As
type ValidationError struct {
Field string
Value any
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %v", e.Field, e.Value)
}
// ใช้ Is กับ sentinel
if errors.Is(err, ErrPermission) {
fmt.Println("no permission")
}
// ใช้ As กับ custom type
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("field %s is invalid\n", valErr.Field)
}
🧠 Hint สำหรับจำ
Is → "Is this error the SAME as...?"
→ เปรียบเทียบค่า เหมือน == แต่เดิน chain ให้
As → "As WHAT TYPE?"
→ ตรวจ type + extract value เหมือน type assertion แต่เดิน chain ให้
สองคำ: Is = identity, As = extraction
📊 สรุป
| ฟังก์ชัน | ใช้กับ | ใช้เมื่อ |
|---|---|---|
fmt.Errorf("...: %w", err) |
— | สร้าง error chain |
errors.Is(err, target) |
Sentinel errors (var ErrX = errors.New(...)) |
เช็คว่าใช่ error นี้ไหม |
errors.As(err, &target) |
Error types (*PathError, net.Error) |
เช็คว่าเป็น type นี้ไหม + ดึงค่า |
📚 อ่านต่อ: