Home > Blog > tech

Go Error Handling คืออะไร? Patterns และ Best Practices 2026

golang error handling patterns
Go Error Handling คืออะไร? Patterns และ Best Practices 2026
2026-04-16 | tech | 1128 words


Go Error Handling คืออะไร? พื้นฐานที่นักพัฒนาทุกคนต้องเข้าใจ

ในโลกของการพัฒนาโปรแกรม การจัดการข้อผิดพลาด (Error Handling) ถือเป็นหัวใจสำคัญของการสร้างซอฟต์แวร์ที่เสถียรและเชื่อถือได้ สำหรับภาษา Go (หรือ Golang) ปรัชญาการจัดการข้อผิดพลาดมีความชัดเจนและแตกต่างจากภาษาอื่นๆ อย่างมีนัยสำคัญ Go ไม่มีกลไกแบบ "try-catch" ที่พบในภาษาอย่าง Java หรือ Python แต่เลือกใช้การคืนค่า (return value) เป็นหลัก ซึ่งทำให้การไหลของโปรแกรม (flow) โปร่งใสและควบคุมได้ง่ายกว่า

Go ถือว่า error เป็นเพียง "ค่า" (value) ชนิดหนึ่ง ซึ่งเป็น interface ที่มีชื่อว่า error โดยมีเมธอดเดียวคือ Error() string วิธีการนี้บังคับให้นักพัฒนาให้ความสำคัญกับข้อผิดพลาดอย่างชัดเจนในทุกขั้นตอน ต้องตรวจสอบ error ทุกครั้งที่ฟังก์ชันคืนค่ามา ไม่สามารถเพิกเฉยได้ง่ายๆ ซึ่งช่วยลดโอกาสที่ข้อผิดพลาดจะหลุดรอดไปโดยไม่ได้รับการจัดการ (silent failure) นี่คือหนึ่งในปรัชญาการออกแบบที่ทำให้ Go ได้รับความนิยมในระบบที่ต้องการความเสถียรสูง เช่น ระบบคลาวด์, DevOps tools, และ microservices

ทำไม Error Handling ใน Go ถึงสำคัญนัก?

การจัดการข้อผิดพลาดที่ดีคือเกราะป้องกันแรกของแอปพลิเคชัน มันช่วยให้ระบบสามารถฟื้นตัวจากสถานการณ์ที่ไม่คาดคิดได้ เก็บ log ได้ถูกต้อง และสื่อสารกับผู้ใช้หรือระบบอื่นๆ อย่างเหมาะสม ในระบบที่ต้องทำงานต่อเนื่องสูง (high availability) เช่น ระบบเทรดใน แพลตฟอร์มการเงิน หรือระบบจัดการคิวออเดอร์ใน แพลตฟอร์มอีคอมเมิร์ซ การที่ error หลุดรอดอาจหมายถึงการสูญเสียข้อมูลหรือรายได้โดยตรง การออกแบบ error handling ของ Go ส่งเสริมให้เขียนโค้ดที่รับผิดชอบต่อข้อผิดพลาดเหล่านี้ตั้งแต่เริ่มต้น

Patterns การจัดการ Error ใน Go ที่คุณต้องรู้

เมื่อทำงานกับ Go ไปสักพัก คุณจะพบว่ามีรูปแบบหรือ patterns มาตรฐานหลายแบบที่ชุมชนนิยมใช้กัน การเลือกใช้ pattern ให้เหมาะสมกับบริบทของงานจะทำให้โค้ดอ่านง่าย, บำรุงรักษาง่าย, และมีประสิทธิภาพมากขึ้น

1. Sentinel Errors (การคืนค่า Error ที่กำหนดขึ้น)

Pattern นี้คือการประกาศตัวแปร error ระดับแพ็กเกจ (package-level variable) และใช้การเปรียบเทียบ error กับตัวแปรนั้นเพื่อตรวจสอบประเภทของข้อผิดพลาด มักใช้กับ error ที่คาดการณ์ได้และต้องการให้ผู้เรียกตรวจสอบได้

package main

import (
    "errors"
    "fmt"
    "os"
)

// ประกาศ sentinel error
var ErrFileNotFound = errors.New("file not found")
var ErrPermissionDenied = errors.New("permission denied")

func openConfigFile(path string) error {
    // จำลองการตรวจสอบไฟล์
    if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
        return ErrFileNotFound
    }
    // จำลองการตรวจสอบ permission
    if path == "/etc/config.secure" {
        return ErrPermissionDenied
    }
    // เปิดไฟล์สำเร็จ
    return nil
}

func main() {
    err := openConfigFile("/etc/config.secure")
    if err != nil {
        if errors.Is(err, ErrFileNotFound) {
            fmt.Println("กรุณาตรวจสอบ path ของไฟล์ config")
        } else if errors.Is(err, ErrPermissionDenied) {
            fmt.Println("ไม่มีสิทธิ์อ่านไฟล์นี้ ต้องการ sudo หรือไม่?")
        } else {
            fmt.Printf("เกิดข้อผิดพลาดอื่นๆ: %v
", err)
        }
        return
    }
    fmt.Println("เปิดไฟล์ config สำเร็จ")
}

ข้อดีของ pattern นี้คือความเรียบง่ายและชัดเจน อย่างไรก็ตาม ข้อเสียคือมันสร้างการ coupling ระหว่างแพ็กเกจ เพราะผู้เรียกต้องรู้จัก sentinel error จากแพ็กเกจที่เรียก ซึ่งอาจทำให้ยากต่อการเปลี่ยนแปลงในอนาคต

2. Error Wrapping และ Unwrapping

ตั้งแต่ Go 1.13 เป็นต้นมา มีฟีเจอร์ที่สำคัญคือการ "ห่อ" error (wrapping) ซึ่งช่วยสร้าง chain ของ error ได้ ทำให้สามารถติดตามที่มาของ error ตั้งแต่จุดเริ่มต้นจนถึงจุดที่จัดการได้ โดยใช้ฟังก์ชัน fmt.Errorf ร่วมกับ verb %w และตรวจสอบด้วยฟังก์ชัน errors.Is และ errors.As

package main

import (
    "database/sql"
    "errors"
    "fmt"
)

var ErrUserNotFound = errors.New("user not found")

func getUserFromDB(userID int) error {
    // จำลอง error จาก database layer
    dbErr := sql.ErrNoRows // สมมติว่าค้นหาไม่เจอ

    // Wrap error ด้วย context เพิ่มเติม
    return fmt.Errorf("getUserFromDB failed for userID %d: %w", userID, dbErr)
}

func getUserProfile(userID int) error {
    err := getUserFromDB(userID)
    if err != nil {
        // Wrap ต่ออีกชั้น พร้อมเพิ่ม context ของ business layer
        return fmt.Errorf("getUserProfile: failed to retrieve profile: %w", err)
    }
    return nil
}

func main() {
    err := getUserProfile(123)

    if err != nil {
        // ตรวจสอบว่า error chain มี sql.ErrNoRows อยู่หรือไม่
        if errors.Is(err, sql.ErrNoRows) {
            fmt.Println("จัดการเฉพาะกรณีไม่พบข้อมูลใน DB:")
            fmt.Println("-> ส่งคืนค่า default หรือสร้าง user ใหม่")
            // หรือแมปเป็น business error ของเราเอง
            fmt.Printf("-> หรือแปลงเป็น: %v
", ErrUserNotFound)
        }

        // แสดง error chain ทั้งหมด
        fmt.Printf("
Full error chain:
%v
", err)
    }
}

Pattern นี้มีประโยชน์อย่างมากในการดีบัก เพราะเรารู้ว่า error เกิดขึ้นที่ไหนบ้างใน call stack โดยไม่ต้องพึ่งพา stack trace ที่อาจยาวเกินจำเป็น มันเป็นเทคนิคที่ระบบขนาดใหญ่เช่น ระบบคลาวด์แพลตฟอร์ม นิยมใช้เพื่อเพิ่มความสามารถในการติดตามปัญหา (traceability)

3. Custom Error Types (การสร้าง Struct Error)

เมื่อต้องการเก็บข้อมูลเพิ่มเติมไปพร้อมกับ error เช่น HTTP status code, error code, หรือ timestamp เราสามารถสร้าง struct ของเราเองที่ implement interface error ได้

package main

import (
    "fmt"
    "time"
)

// Custom error type
type APIError struct {
    Message    string
    StatusCode int
    ErrCode    string
    Timestamp  time.Time
    Details    map[string]interface{}
}

// Implement error interface
func (e *APIError) Error() string {
    return fmt.Sprintf("[%s] %s (HTTP %d)", e.ErrCode, e.Message, e.StatusCode)
}

// เมธอดเพิ่มเติมสำหรับดึงข้อมูลเฉพาะ
func (e *APIError) IsClientError() bool {
    return e.StatusCode >= 400 && e.StatusCode < 500
}

func (e *APIError) IsServerError() bool {
    return e.StatusCode >= 500
}

func fetchFromAPI(url string) error {
    // จำลองการเรียก API ที่ล้มเหลว
    return &APIError{
        Message:    "failed to connect to upstream service",
        StatusCode: 502,
        ErrCode:    "UPSTREAM_BAD_GATEWAY",
        Timestamp:  time.Now(),
        Details: map[string]interface{}{
            "url":         url,
            "timeout_sec": 30,
            "retry_count": 3,
        },
    }
}

func main() {
    err := fetchFromAPI("https://api.example.com/data")
    if err != nil {
        // ใช้ errors.As เพื่อดึงข้อมูลจาก custom type
        var apiErr *APIError
        if errors.As(err, &apiErr) {
            fmt.Printf("เกิดข้อผิดพลาด API: %v
", apiErr)
            fmt.Printf("เป็น client error? %v
", apiErr.IsClientError())
            fmt.Printf("เวลาเกิดเหตุ: %v
", apiErr.Timestamp.Format(time.RFC3339))
            fmt.Printf("รายละเอียด: %v
", apiErr.Details)

            // สามารถจัดการตามประเภท error code ได้
            if apiErr.ErrCode == "UPSTREAM_BAD_GATEWAY" {
                fmt.Println("กลยุทธ์แก้ไข: ลองเรียกใช้บริการสำรอง (fallback)...")
                // เช่น เรียกข้อมูลจาก cache ใน ระบบแนะนำสินค้า
            }
        } else {
            fmt.Printf("ข้อผิดพลาดทั่วไป: %v
", err)
        }
    }
}

Custom error type เหมาะสมกับระบบที่ต้องการ categorization และการจัดการ error ที่ซับซ้อน เช่น REST API หรือระบบกระจาย (distributed systems) ซึ่งจำเป็นต้องส่งข้อมูล error ระหว่าง services อย่างมีโครงสร้าง

4. Defer, Panic, และ Recover

แม้ Go จะเน้นการคืนค่า error แต่ก็มีกลไก panic/recover สำหรับสถานการณ์ที่ "ฟื้นตัวไม่ได้" (unrecoverable) หรือข้อผิดพลาดระดับระบบ โดยทั่วไปแล้ว panic ควรใช้กับเงื่อนไขที่โปรแกรมไม่สามารถดำเนินต่อได้จริงๆ เช่น index out of range, nil pointer dereference หรือสถานการณ์ที่ผิดปกติอย่างรุนแรง

package main

import (
    "fmt"
)

func processTask(data []int) {
    // defer ที่จะรันเสมอเมื่อฟังก์ชันจบ ไม่ว่าจะ panic หรือไม่
    defer func() {
        if r := recover(); r != nil {
            // Recover จาก panic และจัดการแทนที่จะให้โปรแกรมตาย
            fmt.Printf("ฟื้นตัวจาก panic ได้สำเร็จ: %v
", r)
            fmt.Println("ทำการ cleanup resources, ปิด connection, บันทึก log...")
            // อาจส่ง notification ไปยังระบบ monitoring
        }
    }()

    // จำลองสถานการณ์ที่อาจเกิด panic
    if len(data) == 0 {
        panic("ข้อมูลว่างเปล่า ไม่สามารถประมวลผลได้")
    }

    // การคำนวณที่เสี่ยง
    result := 100 / data[0] // ถ้า data[0] เป็น 0 จะ panic
    fmt.Printf("ผลลัพธ์: %d
", result)

    fmt.Println("ประมวลผลงานสำเร็จ")
}

func main() {
    fmt.Println("กรณีที่ 1: ข้อมูลปกติ")
    processTask([]int{2})

    fmt.Println("
กรณีที่ 2: ข้อมูลว่างเปล่า")
    processTask([]int{})

    fmt.Println("
กรณีที่ 3: ข้อมูลที่ทำให้หารด้วยศูนย์")
    processTask([]int{0})

    fmt.Println("
โปรแกรมยังทำงานต่อได้ปกติ ถึงแม้จะมี panic เกิดขึ้น")
}

การใช้ panic/recover ควรจำกัดอยู่ในขอบเขตที่แคบ เช่น ในฟังก์ชันเริ่มต้นของ goroutine เพื่อป้องกัน goroutine ตายทั้งกระบวนการ หรือในส่วนที่ต้องทำ cleanup ทรัพยากรสำคัญ อย่าใช้เป็นกลไกจัดการ error ทั่วไป เพราะจะทำให้การไหลของโปรแกรมเข้าใจยากและผิดกับปรัชญาของ Go

Best Practices สำหรับปี 2026 และอนาคต

แนวทางการจัดการ error ยังคงพัฒนาต่อไป ต่อไปนี้คือ best practices ที่คาดว่าจะยังคงความสำคัญและอาจมีบทบาทมากขึ้นในปี 2026

1. ใช้ errors.Is() และ errors.As() แทนการเปรียบเทียบตรง

เพื่อรองรับ error wrapping อย่างถูกต้อง ควรใช้ errors.Is(err, targetErr) แทน err == targetErr และใช้ errors.As(err, &targetType) เพื่อตรวจสอบและดึงค่า custom error type

2. เพิ่ม Context ที่มีประโยชน์ ไม่ใช่แค่เพิ่มข้อความ

เมื่อ wrap error ให้เพิ่มข้อมูลที่ช่วยในการดีบักได้จริง เช่น parameter ที่เกี่ยวข้อง, state ที่สำคัญ, หรือ identifier ของ operation

// แบบไม่ดี
return fmt.Errorf("operation failed: %v", err)

// แบบดี (เพิ่ม context ที่มีประโยชน์)
return fmt.Errorf("failed to process order %s for user %s: %w", 
    orderID, userID, err)

3. กำหนด Error Contract ให้ชัดเจนใน Public API

ถ้าคุณเขียนไลบรารีหรือแพ็กเกจสำหรับผู้อื่นใช้ ควรเอกสารอย่างชัดเจนว่าแต่ละฟังก์ชันสามารถคืน error ประเภทใดบ้าง และ sentinel error ใดที่ผู้เรียกควรตรวจสอบ การทำเช่นนี้ช่วยลดความไม่แน่นอนและทำให้ integration ง่ายขึ้น โดยเฉพาะเมื่อทำงานร่วมกับระบบภายนอก เช่น API ของ บริการส่งสัญญาณการซื้อขาย

4. ใช้ Structured Logging ร่วมกับ Error

ในระบบ production การ log error แบบมีโครงสร้าง (JSON lines) พร้อม field ที่สอดคล้องกันจะทำให้การวิเคราะห์ผ่าน tools อย่าง Elasticsearch หรือ Datadog มีประสิทธิภาพมากยิ่งขึ้น

log.Error().
    Err(err).
    Str("function", "ProcessPayment").
    Str("transaction_id", txnID).
    Int("retry_count", retryCount).
    Msg("failed to process payment")

5. Handle Errors ณ ขอบเขต (Boundary) ที่เหมาะสม

จัดการ error ให้ใกล้กับจุดที่สามารถตัดสินใจและดำเนินการแก้ไขได้อย่างมีประสิทธิภาพที่สุด ตัวอย่างเช่น ใน HTTP handler อาจแปลง business error ต่างๆ เป็น HTTP status code และ message ที่เหมาะสมให้กับ client ในขณะที่ error ระดับ infrastructure (เช่น database connection ล้มเหลว) อาจต้องมีกลไก retry หรือ circuit breaker แทน

ตารางเปรียบเทียบ Error Handling Patterns ใน Go

Pattern เหมาะกับ ข้อดี ข้อเสีย ตัวอย่างการใช้งาน
Sentinel Errors Error ที่คาดการณ์ได้และต้องการให้ caller ตรวจสอบ เรียบง่าย, ตรวจสอบได้ง่าย, โค้ดอ่านเข้าใจเร็ว สร้าง coupling ระหว่างแพ็กเกจ, ยากต่อการเพิ่ม context io.EOF, sql.ErrNoRows
Error Wrapping ระบบหลายเลเยอร์, ต้องการ traceability ติดตามที่มาของ error ได้, เพิ่ม context ได้ง่าย, ใช้กับ errors.Is/As ได้ ข้อความ error อาจยาว, ต้องใช้ Go 1.13+ การเรียกฟังก์ชันจาก API ลงไปถึง DB layer
Custom Error Types ระบบที่ต้องการข้อมูลประกอบ error เยอะ, REST API เก็บข้อมูลเพิ่มเติมได้, จัดกลุ่ม/ categorize error ได้ ซับซ้อนกว่า, caller ต้องรู้ type เพื่อใช้ errors.As API error ที่มี status code, error code, details
Panic/Recover ข้อผิดพลาดที่ฟื้นตัวไม่ได้, ต้องทำ cleanup หยุด execution ทันที, ร่วมกับ defer ได้ดี ทำให้ flow เข้าใจยาก, ผิดปรัชญา Go ถ้าใช้เยอะ Out of memory, nil pointer ใน goroutine หลัก

FAQ (คำถามที่พบบ่อย)

Q: ควรใช้ panic สำหรับ error ที่เกิดจาก business logic หรือไม่?

A: ไม่ควรอย่างยิ่ง panic ควรสงวนไว้สำหรับสถานการณ์ที่โปรแกรมไม่สามารถดำเนินการต่อได้โดยสมบูรณ์ (เช่น memory หมด, index out of bounds) ส่วน error จาก business logic (เช่น user ไม่พบ, ยอดเงินไม่พอ) ควรจัดการผ่านการคืนค่า error ธรรมดา การใช้ panic สำหรับ business logic จะทำให้การไหลของโปรแกรมยากต่อการติดตามและผิดกับ convention ของชุมชน Go

Q: วิธีจัดการกับ error จาก goroutine อย่างไร?


Back to Blog | iCafe Forex | SiamLanCard | Siam2R