Skip to content

บทที่ 7 — Errors

← บทที่ 6 | สารบัญ | บทที่ 8 →

หลังจบบท คุณจะ:

  • เข้าใจ Go philosophy (ปรัชญาของ Go): "errors are values" (error เป็นค่าธรรมดา)
  • ใช้ errors.Is / errors.As / wrap (ห่อ error ใหม่ให้เก็บ error เดิมไว้ข้างใน เพื่อตามรอย)
  • เขียน custom error type (ชนิด error ที่สร้างเอง)
  • เลือกใช้ error vs panic ถูก

ก่อนอ่านบทนี้ ควรเข้าใจ: บท 3 — Functions (multiple return), บท 5 — Structs + Methods, บท 6 — Interfaces

💡 จากบท 6 ต้องเข้าใจแค่: interface คืออะไร (ข้อ 1-2) และ error interface (ข้อ 4) เท่านั้น — ส่วนขั้นสูงของบท 6 ข้ามได้

⚠️ หมายเหตุเรื่องคำว่า "context": ในบทนี้คำว่า "context" หมายถึง "ข้อมูลแวดล้อมที่ช่วยอธิบาย error" (เช่น operation อะไร, id อะไร) — ไม่ใช่ object context.Context ของ Go ที่จะเรียนในบท 8 (ตัวนั้นใช้พา deadline/cancel ข้าม goroutine)


1. ปรัชญา Go — Errors are Values

เรื่องการจัดการ error คือจุดที่ Go ต่างจาก Java/Python ชัดที่สุด

Go ไม่มี exception (ไม่มี throw/try/catch) — แทนที่ ถือว่า error เป็น "ค่าธรรมดา" ที่ฟังก์ชันคืนกลับมา แล้วผู้เรียกเช็คทันที

(Go มี panic/recover ที่กลไกคล้าย throw/catch อยู่บ้าง แต่สงวนไว้ใช้เฉพาะ "programmer error" ที่ไปต่อไม่ได้จริง ๆ ไม่ใช่ error ทั่วไปแบบ business logic — รายละเอียดเต็มอยู่ข้อ 10 ด้านล่าง)

วิธีนี้ verbose (เขียนยาว) กว่าแต่ explicit (ชัดเจน) — เห็นในลายเซ็นฟังก์ชันเลยว่าอาจ error ได้ ไม่มีเส้นทางการเกิดข้อผิดพลาด (error path) ที่ซ่อนอยู่:

text
Java/Python: exception (throw / try / catch)
   - Hidden control flow (เส้นทางการทำงานซ่อนอยู่ — ไม่เห็นจากการอ่าน signature)
   - Function signature ไม่บอกว่า throw อะไร
     (Java checked exception เป็นข้อยกเว้น —
      Java มีระบบ checked exception ที่บังคับเขียน throws ในลายเซ็น)

Go: error เป็น return value
   - Explicit ใน signature
   - Caller check ทันที (ไม่ propagate ขึ้น stack auto)
   - "Errors are values" — handle เหมือน data
go
// Java
try {
    User u = repo.findById(1);    // throws may?
} catch (NotFoundException e) {
    ...
}

// Go
user, err := repo.FindById(1)
if err != nil {
    return err
}

→ Verbose? ใช่ แต่ explicit + ไม่มี hidden error path


2. error Interface

error ใน Go เป็นแค่ interface ธรรมดาที่มี method เดียว — Error() string หมายความว่า "อะไรก็ตามที่บอกข้อความ error ของตัวเองได้ ถือเป็น error"

เราสร้าง error ง่าย ๆ ได้สองแบบ: errors.New() สำหรับข้อความคงที่ หรือ fmt.Errorf() เมื่อต้องการ format ข้อความ (เช่นแทรกค่าตัวแปรลงไป):

go
type error interface {
    Error() string
}

→ Built-in interface — มี method เดียว

Create Error

go
import "errors"

// Simple error
err := errors.New("something failed")

// Formatted
err := fmt.Errorf("user %d not found", userID)

// Return
func findUser(id int) (User, error) {
    if id < 0 {
        return User{}, errors.New("invalid id")
    }
    // ...
}

3. Standard Error Handling

รูปแบบมาตรฐานของ Go คือ ทุกครั้งที่เรียกฟังก์ชันที่อาจ error ให้รับ err มาแล้วเช็ค if err != nil ทันที ถ้ามีปัญหาก็ส่งต่อขึ้นไป การเช็คติดกับการเรียกแบบนี้คือสำนวนที่เห็นทั่วทุกโค้ด Go

💡 %w คือ verb พิเศษของ fmt.Errorf ที่ "ห่อ" (wrap) error เดิมไว้ข้างใน — จะอธิบายเต็มในข้อ 4 ตอนนี้แค่รู้ว่ามันเพิ่มบริบทให้ error ก็พอ

go
func parseInput(s string) error {
    n, err := strconv.Atoi(s)
    if err != nil {
        return fmt.Errorf("invalid number: %w", err)
    }
    
    if n < 0 {
        return errors.New("number must be positive")
    }
    
    fmt.Println("parsed:", n)   // ใช้ n เพื่อไม่ให้ Go บ่น "declared and not used"
    return nil
}

func main() {
    err := parseInput("abc")
    if err != nil {
        log.Fatal(err)
    }
}

Idiom: Check Error Immediately

go
// ✅ Idiomatic
if err := doSomething(); err != nil {
    return err
}

// ❌ Java-like (avoid)
result := doSomething()
checkError(result)  // throws if error

4. Error Wrapping (Go 1.13+)

%w = wrap error เพื่อรักษา error chain (โซ่ของ error — error ปัจจุบันจะเก็บ error เดิมไว้ข้างใน ทำให้แกะกลับไปดูต้นเหตุได้)

go
func loadConfig() error {
    f, err := os.Open("config.json")
    if err != nil {
        return fmt.Errorf("loadConfig: %w", err)   // wrap
    }
    defer f.Close()
    
    var cfg Config
    if err := json.NewDecoder(f).Decode(&cfg); err != nil {
        return fmt.Errorf("loadConfig: parse: %w", err)
    }
    
    return nil
}

Result:

text
loadConfig: parse: invalid character '}' at line 5

→ Stack trace ของ error — บอกว่า error เริ่มจากไหน


5. errors.Is — Check Specific Error

เมื่อ error ถูกห่อหลายชั้น เราจะเทียบตรง ๆ ด้วย == ไม่ได้ (เพราะ outer ที่ห่ออยู่ไม่ใช่ตัวเดียวกับ error เป้าหมายแล้ว)

errors.Is(err, target) ช่วย "แกะ" (unwrap — แกะห่อออก คือการดึง error ที่ซ่อนอยู่ข้างในออกมาดู) chain ออกทีละชั้น แล้วเทียบกับ error เป้าหมายให้

เหมาะกับการเช็ค sentinel error = error ค่าคงที่ที่ประกาศไว้ล่วงหน้า เช่น ErrNotFound ของ package เรา หรือ os.ErrNotExist ที่ stdlib ให้มา:

go
import (
    "errors"
    "os"
)

func main() {
    _, err := os.Open("missing.txt")
    
    // os.ErrNotExist = sentinel error ที่ package os ประกาศไว้
    // บอกว่า "ไม่พบไฟล์" — เช็คผ่าน errors.Is แม้ถูกห่อมาแล้ว
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("file not found")
    } else if err != nil {
        log.Fatal(err)
    }
}

errors.Is(err, target) = unwrap chain + compare กับ target

Sentinel Error

go
package user

import (
    "database/sql"
    "errors"
)

var (
    ErrNotFound      = errors.New("user not found")
    ErrAlreadyExists = errors.New("user already exists")
    ErrInvalidInput  = errors.New("invalid input")
)

// db คือ *sql.DB ที่ initialize ไว้ที่อื่นในโปรแกรม (ไม่แสดงในตัวอย่างนี้)
func FindByID(id int) (*User, error) {
    user, err := db.Find(id)
    // sql.ErrNoRows มาจาก package "database/sql" (จะเรียนในบท 14)
    // (บอกว่า query แล้วไม่เจอแถว — เดี๋ยวบท 14 อธิบาย database)
    // ตอนนี้แค่ดูรูปแบบการใช้ errors.Is พอ
    // ใช้ errors.Is — ทน wrap ที่ driver บางตัว (เช่น pgx) อาจห่อ
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrNotFound
    }
    return user, err
}

// Use
u, err := user.FindByID(1)
if errors.Is(err, user.ErrNotFound) {
    return c.JSON(404, "Not found")
}

→ "Sentinel errors" = predefined error values สำหรับ check


6. errors.As — Get Concrete Error

ถ้า errors.Is ใช้เช็คว่า "ใช่ error ตัวนี้ไหม" — errors.As ใช้เมื่อเราต้องการ "ดึง error ชนิดเฉพาะออกมาใช้ field ของมัน" เช่นดึง ValidationError ออกมาอ่านว่า field ไหนผิด มันจะแกะ chain แล้ว assign ให้ตัวแปรเป้าหมายถ้าเจอชนิดที่ตรง:

go
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

func validate(u User) error {
    if u.Email == "" {
        return &ValidationError{Field: "email", Message: "required"}
    }
    return nil
}

// Use
err := validate(u)

var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Printf("Field %s: %s\n", ve.Field, ve.Message)
}

errors.As(err, &target) = unwrap + assign concrete error to target


7. Custom Error Types

นอกจาก error ข้อความธรรมดา เราสร้าง error เป็น struct ของตัวเองได้ เพื่อแนบข้อมูลเพิ่ม (เช่น resource, id) — แค่ implement method Error() string และถ้าอยากให้ errors.Is/As แกะ chain ผ่านได้ ก็เพิ่ม method Unwrap():

go
// Simple
type NotFoundError struct {
    Resource string
    ID       any   // Go 1.18+ — เดิมเขียน interface{}
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with id=%v not found", e.Resource, e.ID)
}

// Use
return &NotFoundError{Resource: "user", ID: 42}

With Wrap Support

go
type DatabaseError struct {
    Op  string
    Err error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database %s: %v", e.Op, e.Err)
}

// Support errors.Unwrap
func (e *DatabaseError) Unwrap() error {
    return e.Err
}

// Use
return &DatabaseError{Op: "query", Err: rawErr}

// Check
err := repo.Find(1)
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
    log.Printf("DB op=%s", dbErr.Op)
}

if errors.Is(err, sql.ErrNoRows) {     // unwrap chain → finds sql.ErrNoRows
    // ...
}

8. Error Chains — Real Example

ตัวอย่างจริงของการ "ห่อ error เป็นชั้น ๆ" ผ่านแต่ละเลเยอร์ (DB → Service → Handler) แต่ละชั้นเติม context (บริบท/ข้อมูลแวดล้อมที่ช่วยให้เข้าใจว่าเกิดอะไร) ของตัวเองด้วย %w

ผลคือ error สุดท้ายบอกเล่าเส้นทางทั้งหมดว่าเริ่มพังตรงไหนและผ่านอะไรมาบ้าง ช่วย debug (หาและแก้ข้อผิดพลาด) ได้มหาศาล

โค้ดนี้ใช้ Scan (อ่านค่าจากผลลัพธ์ query ฐานข้อมูลใส่ตัวแปร — เดี๋ยวบท 14 อธิบาย) และ net/http ของ stdlib (บท 11) ตอนนี้โฟกัสที่ "การห่อ error" พอ ไม่ต้องสนใจรายละเอียด DB/web

go
import (
    "database/sql"
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "net/http"
    "strconv"
)

// Layer 1: DB
func (r *UserRepo) FindByID(id int) (*User, error) {
    var u User
    err := r.db.QueryRow("...").Scan(&u.ID, &u.Name)
    if err != nil {
        return nil, fmt.Errorf("UserRepo.FindByID(%d): %w", id, err)
    }
    return &u, nil
}

// Layer 2: Service
func (s *UserService) Get(id int) (*User, error) {
    u, err := s.repo.FindByID(id)
    if err != nil {
        return nil, fmt.Errorf("UserService.Get: %w", err)
    }
    return u, nil
}

// Layer 3: Handler (ใช้ net/http ของ stdlib — เดี๋ยวบท 11 อธิบายเต็ม)
// svc คือ *UserService ที่ initialize ไว้ที่อื่นในโปรแกรม (เช่น ตอน startup)
var svc *UserService

func handler(w http.ResponseWriter, r *http.Request) {
    // แปลง id จาก query string — ตรวจ error ด้วย (ไม่ใช่ _, เพราะ input จากผู้ใช้)
    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil {
        http.Error(w, "invalid id", http.StatusBadRequest)
        return
    }

    u, err := svc.Get(id)

    if errors.Is(err, sql.ErrNoRows) {
        http.Error(w, "not found", http.StatusNotFound)
        return
    }
    if err != nil {
        log.Printf("%+v", err)         // full chain
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(u)
}

// Output:
// UserService.Get: UserRepo.FindByID(42): sql: no rows in result set

→ Full context — ที่เริ่ม + path


9. Multiple Errors (Go 1.20+)

บางครั้งเราต้องรวบหลาย error เข้าด้วยกัน (เช่น validation ที่อยากบอกทุก field ที่ผิดพร้อมกัน ไม่ใช่ทีละตัว) ตั้งแต่ Go 1.20 มี errors.Join() ที่รวม error หลายตัวเป็นก้อนเดียว แล้วยังใช้ errors.Is/As แกะหาแต่ละตัวได้:

go
import "errors"

// ประกาศ sentinel error ไว้ก่อน — จะได้เช็คด้วย errors.Is ได้
var (
    ErrNameRequired = errors.New("name required")
    ErrEmailRequired = errors.New("email required")
    ErrAgeInvalid    = errors.New("age must be positive")
)

func validate(u User) error {
    var errs []error

    if u.Name == "" {
        errs = append(errs, ErrNameRequired)
    }
    if u.Email == "" {
        errs = append(errs, ErrEmailRequired)
    }
    if u.Age < 0 {
        errs = append(errs, ErrAgeInvalid)
    }

    return errors.Join(errs...)        // Go 1.20+
}

// Use
err := validate(u)
fmt.Println(err)
// name required
// email required
go
// Check — errors.Is แกะผ่าน errors.Join ได้
err := validate(u)
if errors.Is(err, ErrNameRequired) {  // works through Join too
    // ...
}

10. panic + recover

panic ใช้เมื่อโปรแกรมเจอสถานการณ์ที่ "ไม่ควรเกิดและไปต่อไม่ได้" (เช่น state ที่เป็นไปไม่ได้) ส่วน recover (เรียกใน defer) ดักจับ panic ไม่ให้โปรแกรมล่ม สำคัญ: panic ไม่ใช่ exception — อย่าใช้กับ error ปกติ (validation, ไฟล์ไม่เจอ) ให้ return error แทน:

go
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    
    divide(10, 0)        // panic
}
// Recovered: division by zero

When to Use Panic

text
✅ Programmer error (impossible state — สถานะที่ไม่ควรเกิด)
- nil dereference (พยายามใช้ค่าที่เป็น nil) ตรงที่ไม่ควรเป็น nil
- index out of bounds (เข้าถึง index เกินขอบเขต)
- uninitialized state (state ที่ยังไม่ initialize)

✅ Init failure (cannot continue)
- Config missing
- Database connection failed at startup

✅ Library / framework boundary
- HTTP handler panic → middleware recover

❌ Don't panic for:
- Validation errors
- File not found
- Network errors
- User-facing errors

→ Return error instead

Recover Pattern in HTTP

ตัวอย่างนี้ใช้ net/http ของ stdlib — เดี๋ยวบท 11 อธิบายเต็ม ตอนนี้โฟกัสที่ defer + recover พอ

go
import (
    "log/slog"
    "net/http"
    "runtime/debug"   // ต้องการสำหรับ debug.Stack()
)

func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                // structured log + stack trace (slog สอนปลายบท)
                slog.Error("panic recovered",
                    "err", rec,
                    "stack", string(debug.Stack()),
                )
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

→ ป้องกัน server crash จาก single bad request


11. log.Fatal vs panic vs os.Exit

มีหลายวิธีที่จะ "หยุดโปรแกรม" และเลือกผิดทำให้เกิดบั๊ก ลองดูทีละตัว:

(เทียบกับ C#/.NET: panicthrow ที่ recover ดักได้เหมือน catch แต่สงวนไว้เฉพาะ programmer error, log.Fatal ≈ log แล้ว Environment.Exit(1) — ไม่มี finally/using ใดรันเลยเหมือน os.Exit, os.ExitEnvironment.Exit ตรงตัว คือข้าม defer เหมือนข้าม finally)

  • panic — หยุดทันที แต่ "กู้คืนได้" ด้วย recover (เห็นในข้อก่อน)
  • log.Fatal — log ข้อความก่อน แล้วออกทันที (ไม่ให้กู้คืน)
  • os.Exit — ออกทันที โดยไม่รัน defer — อันตราย ทำให้ทรัพยากรที่ defer ไว้ (ปิดไฟล์/connection) ไม่ถูกเรียก

มาดูว่าควรใช้ตัวไหนเมื่อไหร่:

go
panic("boom")               // print + stack trace + exit 2
log.Fatal("boom")           // log + os.Exit(1) — no recover possible
log.Panic("boom")           // log แล้ว panic (กู้คืนได้ด้วย recover — ต่างจาก Fatal)
os.Exit(1)                  // immediate exit, no defer run!

Choose

text
panic       — recoverable, programmer error
log.Fatal   — startup failure, non-recoverable
log.Panic   — log + panic ยังกู้ได้ด้วย recover
os.Exit     — immediate quit (avoid in library)

⚠️ os.Exit ไม่ run defer — ใช้ระวัง


12. Error Handling Patterns

รวมแพตเทิร์นการจัดการ error ที่ทำให้โค้ดอ่านง่ายและดูแลง่าย — ที่สำคัญสุดคือ "early return" (เช็ค error แล้ว return ทันที ไม่ซ้อน if ลึก ๆ) ซึ่งเป็นสไตล์มาตรฐานของ Go:

Early Return

go
// ✅ Idiomatic — early return
func process() error {
    a, err := stepA()
    if err != nil {
        return err
    }
    
    b, err := stepB(a)
    if err != nil {
        return err
    }
    
    c, err := stepC(b)
    if err != nil {
        return err
    }
    
    return finalize(c)
}

// ❌ Nested
func process() error {
    if a, err := stepA(); err == nil {
        if b, err := stepB(a); err == nil {
            if c, err := stepC(b); err == nil {
                return finalize(c)
            } else { return err }
        } else { return err }
    } else { return err }
}

Sentinel Errors at Package Level

go
package user

var (
    ErrNotFound      = errors.New("user not found")
    ErrAlreadyExists = errors.New("user already exists")
    ErrInvalidEmail  = errors.New("invalid email")
)

// Consumers can check
if errors.Is(err, user.ErrNotFound) {
    return c.JSON(404, ...)
}

Typed Errors for Rich Info

go
type APIError struct {
    Code    int
    Message string
    Detail  string
}

func (e *APIError) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// Use
return &APIError{Code: 422, Message: "validation failed", Detail: "email required"}

Wrap with Context

go
// ❌ Bare error
file, err := os.Open(path)
if err != nil {
    return err          // ผู้ใช้เห็น "no such file or directory" แต่ไม่รู้ว่าไฟล์ไหน
}

// ✅ Context
file, err := os.Open(path)
if err != nil {
    return fmt.Errorf("opening config file %q: %w", path, err)
}

(ผู้ใช้เห็นข้อความ error เดิม แต่ครั้งนี้รู้ด้วยว่าไฟล์ไหนที่เปิดไม่ได้)


13. Don't Just Log — Or Just Return

กฎสำคัญที่มือใหม่มักผิด: เมื่อเจอ error ให้ "เลือกอย่างใดอย่างหนึ่ง" — log มัน หรือ return มันขึ้นไป อย่าทำทั้งสอง เพราะจะเกิด log ซ้ำซ้อนตามแต่ละชั้น หลักคือ ชั้นกลาง wrap+return ส่วนชั้นบนสุด (handler/main) เป็นคน log:

Anti-pattern: log + return error → double logging

go
func handler() error {
    if err := doSomething(); err != nil {
        log.Printf("error: %v", err)    // logged here
        return err                        // logged again at top
    }
    return nil
}

func top() {
    if err := handler(); err != nil {
        log.Printf("top: %v", err)       // duplicate!
    }
}

→ Pick one:

  • Log + return = ใน top-level (HTTP handler, main)
  • Wrap + return = ใน middle layer
go
// Middle layer: wrap + return
func service() error {
    if err := dao(); err != nil {
        return fmt.Errorf("service: %w", err)
    }
    return nil
}

// Top level: log
func handler(w http.ResponseWriter, r *http.Request) {
    if err := service(); err != nil {
        log.Printf("handler: %+v", err)    // full chain
        http.Error(w, "internal", 500)
    }
}

14. HTTP Error Mapping

ใน web service เราต้องแปลง error ภายในให้เป็น HTTP status code ที่เหมาะสม (not found → 404, invalid → 400, อื่น ๆ → 500) ใช้ errors.Is แยกประเภทแล้ว map เป็น code — หรือทำเป็น error map เพื่อความเป็นระเบียบ ตัวอย่างแรกด้านล่างใช้ stdlib (net/http) ล้วน ๆ เหมาะกับตอนนี้ที่ยังไม่ได้เรียน framework:

Or use an ordered table (อย่าใช้ map!)

⚠️ อย่าใช้ map[error]int แล้ว range — Go สุ่มลำดับการ iterate ของ map (เคยบอกไว้ในบท map) ถ้ามี sentinel หลายตัวที่ errors.Is คืน true ได้พร้อมกัน (เช่น wrap ซ้อนกัน) status code จะออกมาไม่แน่นอน — บั๊กที่หาเจอยาก

go
// ✅ วิธีที่ 1 — switch (อ่านง่าย ตรงไปตรงมา ใช้ในงานจริงบ่อยสุด)
func httpStatusFor(err error) int {
    switch {
    case errors.Is(err, user.ErrNotFound):
        return http.StatusNotFound       // 404
    case errors.Is(err, user.ErrInvalidInput):
        return http.StatusBadRequest     // 400
    case errors.Is(err, user.ErrConflict):
        return http.StatusConflict       // 409
    default:
        return http.StatusInternalServerError  // 500
    }
}

// ✅ วิธีที่ 2 — ordered slice ถ้าอยากเก็บเป็นตารางที่แก้ไขง่าย
var errorTable = []struct {
    sentinel error
    code     int
}{
    {user.ErrNotFound,     http.StatusNotFound},
    {user.ErrInvalidInput, http.StatusBadRequest},
    {user.ErrConflict,     http.StatusConflict},
}

func httpStatusFor2(err error) int {
    for _, e := range errorTable {       // slice = ลำดับแน่นอน
        if errors.Is(err, e.sentinel) {
            return e.code
        }
    }
    return http.StatusInternalServerError
}

Optional: With a Web Framework (Gin)

ตัวอย่างนี้ใช้ Gin framework (c *gin.Context) — เดี๋ยวบท 13 อธิบายเต็ม ถ้ายังไม่รู้จัก Gin ข้ามตัวอย่างนี้ไปได้เลย แนวคิดเรื่องการ map error เป็น status code เหมือนสองตัวอย่างข้างบนทุกอย่าง เปลี่ยนแค่วิธีเขียน response

ถ้าอยากลองโค้ดนี้: go get github.com/gin-gonic/gin (จะเรียนใน บท 13)

go
func handler(c *gin.Context) {
    user, err := svc.GetUser(id)
    
    switch {
    case errors.Is(err, user.ErrNotFound):
        c.JSON(404, ErrorResp{Code: "NOT_FOUND", Message: err.Error()})
    case errors.Is(err, user.ErrInvalidInput):
        c.JSON(400, ErrorResp{Code: "INVALID", Message: err.Error()})
    case err != nil:
        log.Printf("unexpected: %+v", err)
        c.JSON(500, ErrorResp{Code: "INTERNAL", Message: "something went wrong"})
        return
    }
    
    c.JSON(200, user)
}

15. Test Errors

เวลาเทสต์โค้ดที่คืน error อย่าเทียบข้อความ error ด้วย string (เปราะ) ให้ใช้ errors.Is เช็คว่าเป็น sentinel ที่คาดไว้ไหม หรือ errors.As ดึง custom error ออกมาตรวจ field — ทนทานต่อการ wrap และการเปลี่ยนข้อความ:

go
func TestFindUser(t *testing.T) {
    _, err := FindUser(-1)
    
    if !errors.Is(err, ErrInvalidID) {
        t.Errorf("want ErrInvalidID, got %v", err)
    }
}

func TestValidate(t *testing.T) {
    err := validate(User{})
    
    var ve *ValidationError
    if !errors.As(err, &ve) {
        t.Fatal("want *ValidationError")
    }
    if ve.Field != "email" {
        t.Errorf("want field email, got %s", ve.Field)
    }
}

16. Idioms

รวมสำนวนการจัดการ error ที่พบบ่อยในโค้ด Go จริง — continue ข้าม item ที่ผิดใน loop, wrap หนึ่งครั้งต่อหนึ่งเลเยอร์ (ไม่ wrap ซ้ำ) และใช้ named return + defer เพื่อ log/แก้ error ที่จุดเดียวแม้ฟังก์ชันมีหลายทางออก:

Early Continue

go
for _, item := range items {
    if err := validate(item); err != nil {
        log.Printf("invalid item %v: %v", item.ID, err)
        continue
    }
    process(item)
}

Wrap Once Per Layer

text
Layer A: return errors.New(...)
Layer B: return fmt.Errorf("B: %w", err)
Layer C: return fmt.Errorf("C: %w", err)

→ Final: "C: B: original error"

Defer Error Handling

go
func process() (err error) {
    defer func() {
        if err != nil {
            log.Printf("process failed: %v", err)
        }
    }()
    
    // multiple return points
    if cond1 { return errors.New("c1") }
    if cond2 { return errors.New("c2") }
    return nil
}

17. ⚠️ Common Mistakes

สรุปข้อผิดพลาดเรื่อง error ที่พบบ่อยที่สุด คู่กับวิธีที่ถูก — โดยเฉพาะการเทียบ error ด้วย string, ลืม %w ตอน wrap (ทำให้ chain ขาด), และ panic กับเรื่องที่ควร return error:

Ignore error with _Always check (or explicit log why ignore)
if err.Error() == "not found" (string compare)errors.Is(err, ErrNotFound)
err == ErrFoo after wraperrors.Is(err, ErrFoo)
Panic for validationReturn error
Don't wrap (lose context)fmt.Errorf("op: %w", err)
Log + return errorPick one — log at top, wrap elsewhere
Return nil typedReturn nil untyped (interface)
fmt.Errorf("...%s", err) (lost wrap)fmt.Errorf("...%w", err)

18. Modern Go: log/slog (Go 1.21+)

ตั้งแต่ Go 1.21 มี log/slog เป็น logger มาตรฐาน (ตัวบันทึก log = ข้อความบันทึกเหตุการณ์ที่เกิดในโปรแกรม)

จุดเด่นคือรองรับ structured logging (การ log แบบมีโครงสร้าง) — log เป็นคู่ key-value (คู่ ชื่อ-ค่า, ออกเป็น JSON ได้) แทนข้อความเปล่า ๆ

ประโยชน์: ระบบเก็บ log (เช่น Elastic (Elasticsearch — ฐานข้อมูลค้นหา), Loki (ระบบเก็บ log ของ Grafana)) ค้นหาและกรองตาม field ได้ง่าย — เหมาะกับ production (ระบบที่ใช้งานจริง):

go
import "log/slog"

slog.Info("user created",
    "user_id", u.ID,
    "name", u.Name,
)

slog.Error("operation failed",
    "error", err,
    "context", "creating user",
)

→ Structured logging — JSON output ready for production


19. Real-world Example

ตัวอย่างจริงที่รวมทุกอย่างในบทเข้าด้วยกัน — sentinel error, custom error type, การ wrap ข้ามเลเยอร์ (repo → service), และการแยกประเภท error ที่ปลายทางด้วย errors.Is/errors.As พร้อม log แบบ structured ใช้เป็นแม่แบบการจัดการ error ทั้งระบบได้:

go
package main

import (
    "context"
    "errors"
    "fmt"
    "log/slog"
)

// Sentinel errors
var (
    ErrUserNotFound = errors.New("user not found")
    ErrInvalidEmail = errors.New("invalid email")
)

// Custom error
type ValidationError struct {
    Field  string
    Reason string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation: %s: %s", e.Field, e.Reason)
}

// Data type — ประกาศก่อนเพื่อให้ repo/service ด้านล่างใช้ได้
type User struct {
    ID   int
    Name string
}

// Layer
type UserRepo struct{}

// ctx context.Context ในตัวอย่างนี้แค่ผ่านเป็น parameter เฉยๆ ไม่ต้องเข้าใจตอนนี้ — รายละเอียดเต็มอยู่บท 8
func (r *UserRepo) FindByID(ctx context.Context, id int) (User, error) {
    if id == 1 {
        return User{ID: 1, Name: "Anna"}, nil
    }
    return User{}, fmt.Errorf("repo.FindByID(%d): %w", id, ErrUserNotFound)
}

type UserService struct {
    repo *UserRepo
}

func (s *UserService) Get(ctx context.Context, id int) (User, error) {
    if id <= 0 {
        return User{}, &ValidationError{Field: "id", Reason: "must be positive"}
    }
    
    u, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return User{}, fmt.Errorf("service.Get: %w", err)
    }
    return u, nil
}

func main() {
    s := &UserService{repo: &UserRepo{}}
    
    u, err := s.Get(context.Background(), 999)
    if err != nil {
        // Specific error
        if errors.Is(err, ErrUserNotFound) {
            slog.Warn("user not found", "error", err)
            return
        }
        
        // Validation
        var ve *ValidationError
        if errors.As(err, &ve) {
            slog.Warn("validation failed", "field", ve.Field, "reason", ve.Reason)
            return
        }
        
        // Unknown
        slog.Error("unexpected error", "error", err)
        return
    }
    
    slog.Info("user found", "id", u.ID, "name", u.Name)
}

20. Checkpoint

🛠️ Checkpoint 7.1 — Basic Error (error พื้นฐาน)
สร้าง function ParseAge(s string) (int, error):

  • ถ้าไม่ใช่ตัวเลข → error "invalid number"
  • ถ้า < 0 หรือ > 150 → error "out of range" (เกินช่วงที่ยอมรับ)
  • เขียนเทสต์ให้ครอบคลุม

🛠️ Checkpoint 7.2 — Sentinel + Custom (error ค่าคงที่ + error สร้างเอง)
สร้าง UserService:

  • ErrNotFound, ErrAlreadyExists (sentinel = error ค่าคงที่ประกาศไว้ล่วงหน้า)
  • ValidationError (custom struct = error ชนิดที่สร้างเอง)
  • FindByID, Create, Update, Delete
  • ทดสอบด้วย errors.Is + errors.As

🛠️ Checkpoint 7.3 — Wrap Chain (ห่อ error เป็นทอด ๆ)
สร้าง 3 ชั้น (layers):

  • Repo → ห่อ error จากฐานข้อมูล
  • Service → ห่อ error จาก Repo
  • Handler → log chain เต็ม แล้วแปลง (map) เป็น HTTP status

🛠️ Checkpoint 7.4 — Panic Recovery (กู้จาก panic)
HTTP server ที่:

  • Middleware ดักจับ panic
  • log + คืน 500
  • ลองทำให้ panic → ดูว่า server ยังทำงานต่อได้

🛠️ Checkpoint 7.5 — Multiple Errors (รวมหลาย error)
สร้าง validate(user User) error ที่คืน error จากการตรวจทุกข้อรวมกัน (Go 1.20+ ใช้ errors.Join)


21. สรุปบท

ทบทวนภาพรวมการจัดการ error ใน Go — ตั้งแต่ปรัชญา "errors are values", การ wrap ด้วย %w, errors.Is/errors.As, custom error type, panic/recover, ไปจนถึงการ map error เป็น HTTP status:

Errors are values — return + check, no exception
error = interface ที่มี Error() string
errors.New("msg") + fmt.Errorf("ctx: %w", err) for wrapping
errors.Is(err, target) — check sentinel error (unwrap chain)
errors.As(err, &v) — get concrete error type
Sentinel errors: predefined ErrNotFound etc.
Custom types: struct + Error() string method + optional Unwrap()
%w for wrap (not %s / %v) — preserve chain
panic + recover — only for programmer error / startup, not business logic
✅ Log error at top level — wrap at middle
errors.Join (Go 1.20+) — multiple errors
HTTP handler map error → status code


← บทที่ 6 | บทที่ 8 → Goroutines + Channels