Go errors .Is กับ .As เป็นเพื่อนกัน

go dev.to

Go errors .Is กับ .As เป็นเพื่อนกัน

เวลาเริ่มเขียน Go — เรามักเช็ค error ด้วย == ตรง ๆ:

if err == sql.ErrNoRows {
    // handle
}
Enter fullscreen mode Exit fullscreen mode

แต่พอเริ่มใช้ fmt.Errorf("...: %w", err) wrap error — == ใช้ไม่ได้แล้ว

err := getUser()
// err = "GetUser: sql: no rows in result set"
// err == sql.ErrNoRows → false! เพราะมันถูก wrap แล้ว
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

%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 ไว้!
// }
Enter fullscreen mode Exit fullscreen mode

%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")
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

ตัวอย่างเพิ่มเติม:

// เช็คหลายระดับ
err := fmt.Errorf("handler: %w",
          fmt.Errorf("service: %w",
              sql.ErrNoRows))

errors.Is(err, sql.ErrNoRows) // ✅ true — ลงลึก 2 ชั้น
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

errors.As ถามว่า: "error นี้ (หรือ error ที่ถูก wrap ไว้ข้างใน) เป็น type *os.PathError หรือไม่? — ถ้าใช่ ใส่ค่าให้ตัวแปรที่ส่งเข้าไป"

หลักการทำงาน:

1. err เป็น *os.PathError? → ใช่!
2. ใส่ค่า err เข้าไปใน pathErr (ผ่าน pointer ที่ส่งเข้า)
3. ตอบ true
Enter fullscreen mode Exit fullscreen mode

ข้อแตกต่างสำคัญ: 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) { ... }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

ตัวอย่างที่ใช้ทั้งคู่

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode
// ใช้ 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)
}
Enter fullscreen mode Exit fullscreen mode

🧠 Hint สำหรับจำ

Is  →  "Is this error the SAME as...?"
        → เปรียบเทียบค่า เหมือน == แต่เดิน chain ให้

As  →  "As WHAT TYPE?"
        → ตรวจ type + extract value เหมือน type assertion แต่เดิน chain ให้
Enter fullscreen mode Exit fullscreen mode

สองคำ: 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 นี้ไหม + ดึงค่า

📚 อ่านต่อ:

Source: dev.to

arrow_back Back to Tutorials