โหมดมืด
บทที่ 7 — Errors
หลังจบบท คุณจะ:
- เข้าใจ 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 เหมือน datago
// 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 error4. 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 requiredgo
// 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 zeroWhen 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 insteadRecover 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: panic ≈ throw ที่ recover ดักได้เหมือน catch แต่สงวนไว้เฉพาะ programmer error, log.Fatal ≈ log แล้ว Environment.Exit(1) — ไม่มี finally/using ใดรันเลยเหมือน os.Exit, os.Exit ≈ Environment.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 wrap | errors.Is(err, ErrFoo) |
| Panic for validation | Return error |
| Don't wrap (lose context) | fmt.Errorf("op: %w", err) |
| Log + return error | Pick one — log at top, wrap elsewhere |
Return nil typed | Return 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