Go Error Handling ในปี 2026: รูปแบบการจัดการ Error, Wrapping และแนวปฏิบัติที่ดีที่สุดสำหรับการสัมภาษณ์งาน

เจาะลึกรูปแบบการจัดการ error ใน Go ปี 2026 ครอบคลุม error interface, custom error types, error wrapping ด้วย fmt.Errorf, sentinel errors, domain errors และคำถามสัมภาษณ์งานที่พบบ่อยสำหรับนักพัฒนา Go

Go Error Handling Patterns

การจัดการ error เป็นหัวใจสำคัญของการเขียนโปรแกรมภาษา Go ที่แตกต่างจากภาษาอื่นอย่างชัดเจน Go ไม่ใช้ exception แต่ใช้ค่า error ที่ส่งคืนจากฟังก์ชันเป็นค่าปกติ ซึ่งบังคับให้นักพัฒนาจัดการกับสถานการณ์ที่ผิดพลาดอย่างชัดเจนในทุกจุดของโค้ด ในปี 2026 ระบบนิเวศของ Go ได้พัฒนาเครื่องมือและรูปแบบที่สมบูรณ์สำหรับการจัดการ error ตั้งแต่ sentinel errors ไปจนถึง error wrapping และ domain-specific error types

หลักการสำคัญของ Go Error Handling

Error ใน Go คือ value ไม่ใช่ exception การจัดการ error อย่างชัดเจนทำให้โค้ดอ่านง่าย ติดตามปัญหาได้สะดวก และหลีกเลี่ยงสถานการณ์ที่ error ถูกซ่อนไว้โดยไม่มีใครสังเกตเห็น ความเข้าใจในรูปแบบเหล่านี้เป็นสิ่งจำเป็นสำหรับการสัมภาษณ์งานตำแหน่ง Go developer

ทำความเข้าใจ Error Interface ของ Go

พื้นฐานของระบบ error ทั้งหมดใน Go คือ error interface ที่เรียบง่ายอย่างยิ่ง interface นี้กำหนดเพียง method เดียวคือ Error() ที่ส่งคืน string ความเรียบง่ายนี้ทำให้ทุก type ที่ implement method Error() string สามารถใช้เป็น error ได้ทันที

builtin.gogo
type error interface {
    Error() string
}

การออกแบบนี้สะท้อนปรัชญาของ Go ที่เน้นความเรียบง่ายและ composition คือการประกอบส่วนเล็ก ๆ เข้าด้วยกัน แทนที่จะสร้าง exception hierarchy ที่ซับซ้อน Go ให้ interface ที่เรียบง่ายและปล่อยให้นักพัฒนาสร้าง error type ที่เหมาะกับ domain ของตนเอง

การสร้าง Custom Error Types

เมื่อ application มีความซับซ้อนมากขึ้น string error ธรรมดาไม่เพียงพอต่อการสื่อสารบริบทของปัญหา Custom error types ช่วยให้สามารถแนบข้อมูลเพิ่มเติมเข้ากับ error ได้ เช่น resource ที่เกี่ยวข้อง, ID ที่ค้นหาไม่พบ หรือ error code เฉพาะ

apperror.gogo
type NotFoundError struct {
    Resource string
    ID       string
}

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

Custom error type ทำให้สามารถใช้ errors.As เพื่อตรวจสอบว่า error เป็น type ใดได้อย่างแม่นยำ ซึ่งเหนือกว่าการเปรียบเทียบ string message ที่เปราะบางและเปลี่ยนแปลงได้ง่าย

Sentinel Errors: ค่า Error คงที่ที่ใช้ซ้ำได้

Sentinel errors คือตัวแปร error ระดับ package ที่ประกาศไว้ล่วงหน้าเพื่อใช้เปรียบเทียบกับ errors.Is รูปแบบนี้เหมาะสำหรับ error ที่เกิดซ้ำบ่อยและไม่ต้องการบริบทเพิ่มเติม เช่น record not found, unauthorized access หรือ resource conflict

errors.gogo
var (
    ErrNotFound     = errors.New("record not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrConflict     = errors.New("resource conflict")
)

ข้อดีของ sentinel errors คือการเปรียบเทียบด้วย errors.Is ทำงานได้แม้ error จะถูก wrap ด้วย fmt.Errorf หลายชั้น ทำให้ handler layer สามารถจับ error ที่เกิดจาก layer ลึก ๆ ได้โดยไม่สูญเสียความสามารถในการระบุประเภท

Error Wrapping ด้วย fmt.Errorf และ %w

Error wrapping เป็นรูปแบบที่สำคัญที่สุดรูปแบบหนึ่งใน Go การใช้ verb %w ใน fmt.Errorf จะห่อ error เดิมไว้ใน error ใหม่ที่มีบริบทเพิ่มเติม เช่น ชื่อฟังก์ชัน, พารามิเตอร์ และ layer ที่เกิด error ขึ้น

repository.gogo
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("UserRepo.FindByID(%s): %w", id, err)
    }
    return user, nil
}

เมื่อ error ถูก wrap ด้วย %w ฟังก์ชัน errors.Is และ errors.As สามารถ unwrap error chain ทั้งหมดเพื่อตรวจสอบ error ต้นทาง การ wrap error ในแต่ละ layer ทำให้ได้ stack trace แบบ manual ที่ระบุจุดเกิด error ได้อย่างชัดเจน เช่น "handler.GetUser: service.GetByID: UserRepo.FindByID(abc123): connection refused"

การจัดการ Error ใน HTTP Handler

HTTP handler เป็นจุดที่ error จากทุก layer มารวมกัน handler ที่ดีควรแปลง error เป็น HTTP status code ที่เหมาะสมและส่ง response ที่ปลอดภัยกลับไปยัง client โดยไม่เปิดเผยรายละเอียดภายในของระบบ

handler.gogo
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := userService.GetByID(r.Context(), chi.URLParam(r, "id"))
    if errors.Is(err, ErrNotFound) {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, "Internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

รูปแบบนี้แสดงหลักการสำคัญสองประการ ประการแรก ใช้ errors.Is เพื่อจับ error ที่รู้จัก เช่น ErrNotFound แล้ว map เป็น status code ที่ถูกต้อง ประการที่สอง error ที่ไม่รู้จักจะถูกจัดเป็น internal error โดยอัตโนมัติ เพื่อไม่ให้ข้อมูลภายในรั่วไหลออกไป

พร้อมที่จะพิชิตการสัมภาษณ์ Go แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Error Middleware และ Error Mapping

เมื่อ application มี handler จำนวนมาก การเขียน error mapping ซ้ำในทุก handler ไม่มีประสิทธิภาพ middleware ช่วยรวมศูนย์การจัดการ error และ panic recovery ไว้ที่จุดเดียว ทำให้ handler แต่ละตัวโฟกัสที่ business logic เท่านั้น

middleware.gogo
func errorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                http.Error(w, "Internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func mapErrorToHTTP(err error) int {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
        return http.StatusNotFound
    }
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        return http.StatusBadRequest
    }
    return http.StatusInternalServerError
}

ฟังก์ชัน mapErrorToHTTP ใช้ errors.As เพื่อตรวจสอบ type ของ error แทน errors.Is เพราะ custom error types มีข้อมูลเพิ่มเติมที่แตกต่างกันในแต่ละ instance รูปแบบนี้ทำให้สามารถเพิ่ม error type ใหม่ได้ง่ายโดยไม่ต้องแก้ไข handler ที่มีอยู่

Domain Errors: การออกแบบ Error สำหรับ Business Logic

Application ขนาดใหญ่ต้องการ error type ที่แยก business logic error ออกจาก infrastructure error อย่างชัดเจน Domain error ประกอบด้วย error code สำหรับ programmatic handling, message สำหรับ logging และ wrapped error สำหรับ root cause analysis

domain/errors.gogo
type DomainError struct {
    Code    string
    Message string
    Err     error
}

func (e *DomainError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

func (e *DomainError) Unwrap() error {
    return e.Err
}

Method Unwrap() ทำให้ errors.Is และ errors.As สามารถมองทะลุ DomainError เพื่อตรวจสอบ error ที่อยู่ภายในได้ ตัวอย่างเช่น หาก DomainError ห่อ ErrNotFound ไว้ การเรียก errors.Is(err, ErrNotFound) จะยังคงคืนค่า true

การใช้งาน Error ใน Service Layer

Service layer เป็นจุดที่ business rule ถูกบังคับใช้ และเป็นจุดที่ error handling มีความซับซ้อนมากที่สุด service ที่ดีจะ wrap error จาก repository เพื่อเพิ่มบริบท และสร้าง domain error สำหรับ business rule violation

service/user.gogo
func (s *UserService) Deactivate(ctx context.Context, userID string) error {
    user, err := s.repo.FindByID(ctx, userID)
    if err != nil {
        return fmt.Errorf("deactivating user %s: %w", userID, err)
    }
    if user.Status == StatusInactive {
        return &DomainError{
            Code:    "ALREADY_INACTIVE",
            Message: fmt.Sprintf("user %s is already inactive", userID),
        }
    }
    return s.repo.UpdateStatus(ctx, userID, StatusInactive)
}

สังเกตว่า error จาก repo.FindByID ถูก wrap ด้วย %w เพื่อรักษา error chain ในขณะที่ business rule violation สร้าง DomainError ใหม่โดยไม่ wrap error อื่น เพราะไม่มี underlying error การแยกประเภทนี้ช่วยให้ handler layer ตัดสินใจได้ว่า error ใดควรแสดงผลต่อผู้ใช้และ error ใดควร log เพื่อ debugging

การจัดการ Error ในงาน Batch และ Concurrent Operations

การประมวลผลหลายรายการพร้อมกันต้องการกลยุทธ์ error handling ที่แตกต่างจาก sequential code Package errgroup จาก standard library extension เป็นเครื่องมือมาตรฐานสำหรับการจัดการ goroutine กลุ่มที่อาจส่งคืน error

batch.gogo
func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10)

    for _, id := range orderIDs {
        g.Go(func() error {
            if err := s.processOrder(ctx, id); err != nil {
                return fmt.Errorf("processing order %s: %w", id, err)
            }
            return nil
        })
    }

    return g.Wait()
}

errgroup.WithContext สร้าง context ที่จะถูก cancel เมื่อ goroutine ใดก็ตามส่งคืน error ซึ่งหมายความว่า goroutine อื่นที่กำลังทำงานอยู่จะได้รับ signal ให้หยุดผ่าน context cancellation g.SetLimit(10) จำกัดจำนวน goroutine ที่ทำงานพร้อมกันเพื่อป้องกัน resource exhaustion g.Wait() จะส่งคืน error แรกที่เกิดขึ้น ซึ่งเพียงพอสำหรับกรณีส่วนใหญ่ที่ต้องการ fail-fast behavior

คำถามสัมภาษณ์งาน: Go Error Handling

คำถามเหล่านี้ปรากฏบ่อยในการสัมภาษณ์งานตำแหน่ง Go developer ตั้งแต่ระดับ junior ถึง senior คำตอบแต่ละข้อควรแสดงความเข้าใจในเชิงลึก ไม่ใช่เพียงท่องจำ

ความแตกต่างระหว่าง errors.Is กับ errors.As คืออะไร?

errors.Is เปรียบเทียบ error กับ target value ที่เฉพาะเจาะจง เช่น sentinel error โดย unwrap error chain ทั้งหมด เหมาะสำหรับตรวจสอบว่า error chain มี error ที่รู้จักหรือไม่ errors.As ตรวจสอบว่า error ใน chain ตรงกับ type ที่ระบุหรือไม่ และ assign ค่าให้กับตัวแปร target เหมาะสำหรับดึงข้อมูลเพิ่มเติมจาก custom error type ตัวอย่างเช่น errors.Is(err, ErrNotFound) ตรวจสอบ value ส่วน errors.As(err, &notFoundErr) ตรวจสอบ type และดึง field ต่าง ๆ ออกมาใช้งานได้

ทำไม Go จึงไม่ใช้ exception?

Go ออกแบบมาให้ error เป็น value ที่ส่งคืนจากฟังก์ชัน ไม่ใช่ control flow mechanism อย่าง exception ปรัชญานี้มีข้อดีหลายประการ ได้แก่ นักพัฒนาถูกบังคับให้จัดการ error อย่างชัดเจน ไม่มี hidden control flow ที่กระโดดข้าม stack frame ทำให้โค้ดอ่านง่ายและ debug ง่าย และ compiler สามารถเตือนเมื่อ error ถูกละเลย (ผ่าน linter อย่าง errcheck) Go มี panic และ recover สำหรับสถานการณ์ที่ไม่คาดคิดจริง ๆ แต่ไม่ควรใช้แทน error handling ปกติ

%w ต่างจาก %v ใน fmt.Errorf อย่างไร?

%w สร้าง error ที่สามารถ unwrap ได้ด้วย errors.Is และ errors.As กล่าวคือ error เดิมยังคงอยู่ใน chain และสามารถตรวจสอบได้ %v แปลง error เป็น string เท่านั้น ทำให้ error เดิมหายไป ไม่สามารถ unwrap ได้ ควรใช้ %w เมื่อต้องการให้ caller สามารถตรวจสอบ error ต้นทาง และใช้ %v เมื่อต้องการซ่อน implementation detail ของ layer ภายใน

เมื่อใดควรใช้ panic แทน error return?

panic เหมาะสำหรับสถานการณ์ที่โปรแกรมไม่สามารถดำเนินต่อได้อย่างสมเหตุสมผล เช่น bug ที่ไม่ควรเกิดขึ้นในขณะ runtime (nil map access ที่ควรถูก initialize แล้ว) หรือการละเมิด invariant ที่ร้ายแรง ใน production code ควรใช้ recover ใน middleware หรือ goroutine boundary เพื่อจับ panic และแปลงเป็น error ที่จัดการได้ กฎทั่วไปคือ library ไม่ควร panic แต่ควรส่งคืน error ให้ caller ตัดสินใจ

อธิบายรูปแบบ error wrapping ที่ดีในแต่ละ layer ของ application

Repository layer ควร wrap error พร้อมชื่อ method และ parameter ที่สำคัญ เช่น fmt.Errorf("UserRepo.FindByID(%s): %w", id, err) Service layer ควร wrap error พร้อม operation context เช่น fmt.Errorf("deactivating user %s: %w", userID, err) Handler layer ไม่ควร wrap error แต่ควรแปลง error เป็น HTTP response ที่เหมาะสม การ wrap ที่ดีสร้าง error message ที่อ่านจากซ้ายไปขวาเหมือน breadcrumb trail

Anti-patterns ที่ต้องหลีกเลี่ยง

รูปแบบต่อไปนี้เป็นข้อผิดพลาดที่พบบ่อยในโค้ด Go ที่ทำให้ error handling ไม่มีประสิทธิภาพหรือซ่อนปัญหาไว้

การกลืน Error โดยไม่จัดการ

go
// anti-pattern: silent error swallowing
result, _ := riskyOperation()

การใช้ _ เพื่อละเลย error หมายความว่าหาก riskyOperation ล้มเหลว โปรแกรมจะทำงานต่อด้วยค่า zero value ของ result ซึ่งอาจนำไปสู่ bug ที่ debug ยากมาก มีเพียงไม่กี่กรณีที่การละเลย error เป็นที่ยอมรับ เช่น fmt.Fprintf ที่เขียนไปยัง bytes.Buffer ซึ่งไม่มีทางล้มเหลว

การจัดการ Error ซ้ำซ้อน

go
// anti-pattern: double handling
if err != nil {
    log.Printf("operation failed: %v", err)
    return err
}

รูปแบบนี้ทั้ง log และ return error ทำให้ error เดียวกันปรากฏหลายครั้งใน log เมื่อถูก handle ซ้ำในแต่ละ layer แนวปฏิบัติที่ดีคือ เลือกอย่างใดอย่างหนึ่ง: wrap แล้ว return ให้ layer บนจัดการ หรือ log แล้วจัดการ error ที่จุดนั้นโดยไม่ return error ต่อไป

สรุป

  • Error ใน Go คือ value ที่ implement error interface เพียง method Error() string ความเรียบง่ายนี้ทำให้สามารถสร้าง custom error type ได้อย่างยืดหยุ่น
  • Sentinel errors เหมาะสำหรับ error ที่เกิดซ้ำบ่อยและตรวจสอบด้วย errors.Is ส่วน custom error types เหมาะสำหรับ error ที่ต้องการข้อมูลเพิ่มเติมและตรวจสอบด้วย errors.As
  • Error wrapping ด้วย %w รักษา error chain ทำให้ handler layer สามารถตรวจสอบ error ต้นทางได้ ในขณะที่ %v ตัด chain และซ่อน implementation detail
  • Domain error ที่มี Unwrap() method ช่วยแยก business logic error ออกจาก infrastructure error อย่างชัดเจน
  • Service layer ควร wrap error เพื่อเพิ่มบริบท ส่วน handler layer ควร map error เป็น HTTP status code โดยไม่เปิดเผยรายละเอียดภายใน
  • errgroup เป็นเครื่องมือมาตรฐานสำหรับ concurrent error handling พร้อม context cancellation และ concurrency limiting
  • หลีกเลี่ยง anti-patterns สำคัญ: การกลืน error ด้วย _ และการจัดการ error ซ้ำซ้อนด้วยการ log แล้ว return
  • เตรียมตัวสำหรับคำถามสัมภาษณ์ Go error handling โดยทำความเข้าใจความแตกต่างระหว่าง errors.Is กับ errors.As และรูปแบบ error wrapping ที่เหมาะสมในแต่ละ layer

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#go
#error-handling
#best-practices
#interview

แชร์

บทความที่เกี่ยวข้อง

ภาพประกอบดีไซน์แพตเทิร์นของ Go ด้วยรูปทรงเรขาคณิตนามธรรมที่สื่อถึงสถาปัตยกรรมซอฟต์แวร์

ดีไซน์แพตเทิร์นใน Go: แพตเทิร์นสำคัญและคำถามสัมภาษณ์สำหรับนักพัฒนา Go

เชี่ยวชาญดีไซน์แพตเทิร์นของ Go ทั้ง Functional Options, Strategy, Factory และ Observer พร้อมตัวอย่างโค้ดใช้งานจริง แนวปฏิบัติที่ดีแบบ idiomatic และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Go

Go 1.26 Green Tea GC, go fix และการเพิ่มประสิทธิภาพ Stack

Go 1.26 สัมภาษณ์งาน: Green Tea GC, go fix และการเพิ่มประสิทธิภาพ Stack สำหรับนักพัฒนา

เตรียมตัวสัมภาษณ์งาน Go 1.26 ครอบคลุม Green Tea garbage collector ลด overhead 10-40%, เครื่องมือ go fix พร้อม modernizers, การจัดสรร slice บน stack, ตรวจจับ goroutine leak และระบบรักษาความปลอดภัย post-quantum พร้อมตัวอย่างโค้ดและคำตอบที่คาดหวัง

คำถามสัมภาษณ์ Go - คู่มือเตรียมตัวฉบับสมบูรณ์

25 คำถามสัมภาษณ์ Go ยอดนิยม: คู่มือฉบับสมบูรณ์สำหรับนักพัฒนา

พิชิตการสัมภาษณ์ Go ด้วย 25 คำถามที่ถูกถามบ่อย goroutine, channel, interface และรูปแบบการทำงานพร้อมกันพร้อมตัวอย่างโค้ด