# Go Error Handling ในปี 2026: รูปแบบการจัดการ Error, Wrapping และแนวปฏิบัติที่ดีที่สุดสำหรับการสัมภาษณ์งาน > เจาะลึกรูปแบบการจัดการ error ใน Go ปี 2026 ครอบคลุม error interface, custom error types, error wrapping ด้วย fmt.Errorf, sentinel errors, domain errors และคำถามสัมภาษณ์งานที่พบบ่อยสำหรับนักพัฒนา Go - Published: 2026-06-15 - Updated: 2026-06-15 - Author: SharpSkill - Tags: go, error-handling, best-practices, interview - Reading time: 8 min --- การจัดการ 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 ได้ทันที ```go // builtin.go 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 เฉพาะ ```go // apperror.go 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 ```go // errors.go 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 ขึ้น ```go // repository.go 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 โดยไม่เปิดเผยรายละเอียดภายในของระบบ ```go // handler.go 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 โดยอัตโนมัติ เพื่อไม่ให้ข้อมูลภายในรั่วไหลออกไป ## Error Middleware และ Error Mapping เมื่อ application มี handler จำนวนมาก การเขียน error mapping ซ้ำในทุก handler ไม่มีประสิทธิภาพ middleware ช่วยรวมศูนย์การจัดการ error และ panic recovery ไว้ที่จุดเดียว ทำให้ handler แต่ละตัวโฟกัสที่ business logic เท่านั้น ```go // middleware.go 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, ¬Found) { 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 ```go // domain/errors.go 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 ```go // service/user.go 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 ```go // batch.go 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, ¬FoundErr)` ตรวจสอบ 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](/technologies/go/interview-questions/error-handling) โดยทำความเข้าใจความแตกต่างระหว่าง `errors.Is` กับ `errors.As` และรูปแบบ error wrapping ที่เหมาะสมในแต่ละ layer --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/go/go-error-handling-patterns-wrapping-best-practices