ข้ามไปยังเนื้อหา

Errors & Generics

ภาษาส่วนใหญ่จัดการกับความล้มเหลวผ่าน exceptions — กลไกพิเศษที่ควบคุมการไหลของโปรแกรมโดยการ unwind call stack จนกว่า catch block จะดักจับปัญหาไว้ได้ Go เลือกแนวทางที่แตกต่างอย่างสิ้นเชิง: errors คือค่าธรรมดา ที่คืนกลับมาพร้อมกับผลลัพธ์และตรวจสอบตรง ๆ ด้วย if err != nil ไม่มี hidden control flow และไม่มี unchecked exceptions — ทุก failure point มองเห็นได้ที่ callsite บทเรียนถัดไป Error Values จะครอบคลุม interface error และ pattern นี้แบบละเอียด

ครึ่งหลังของโมดูลนี้คือเครื่องมือคนละแบบไปเลย: generics ให้ function หรือ type เดียวทำงานกับหลาย concrete type ได้ โดยตรวจสอบตอน compile time แทนที่จะเขียนซ้ำด้วยมือหรือ type assert ตอน runtime panic และ recover อยู่ตรงกลาง — เป็นทางออกฉุกเฉินสำหรับ programmer error และสถานะที่เป็นไปไม่ได้ ไม่ใช่ตัวแทนของการคืน (T, error)

สิ่งหนึ่งที่ควรพกติดตัวมาจาก Functions ก่อนไปถึง Panic & Recover: deferred call ทำงานเป็น LIFO order — defer ตัวสุดท้ายที่ลงทะเบียนไว้ทำงานก่อน กฎนี้ไม่เปลี่ยนระหว่าง panic เลย และนั่นคือเหตุผลที่ recover() ต้องอยู่ข้างใน deferred function ตั้งแต่แรก

ทุกกฎในโมดูลนี้ลงเอยที่สิ่งใดสิ่งหนึ่งจากไม่กี่อย่างตอนเขียนโค้ดจริง: nil check ที่ทำงานทั้งที่ไม่ควร, compiler error ที่ระบุ pointer ที่ขาดหรือ constraint ที่ไม่ผ่าน, หรือ panic message ที่บอกตรง ๆ ว่าอะไรผิดพลาด การฝึกอ่านข้อความจริง ไม่ใช่แค่จำกฎแบบนามธรรม คือทักษะจริงที่โมดูลนี้สร้างขึ้น

บทเรียนสิ่งที่จะได้เรียน
Error Valuesinterface error, errors.New, sentinel errors, กับดัก typed-nil interface, errors.Is
Wrapping Errors%w, errors.As, errors.Join, custom error types, ทำไม receiver choice ถึงสำคัญ
Panic and Recoverเมื่อไหร่ควร panic, recover, runtime.Error, panic(nil), ทำไม goroutine ป้องกันจากภายนอกไม่ได้
GenericsType parameters, constraints, comparable, cmp.Ordered, generic method (Go 1.27)
Iteratorsiter.Seq, range-over-func, iter.Pull, helper แบบ iterator ใน slices/maps (Go 1.23+)

function เล็ก ๆ ที่คืนค่า (string, error) เพื่อแสดงรูปแบบพื้นฐานของการจัดการ error ใน Go:

package main
import (
"errors"
"fmt"
)
func greet(name string) (string, error) {
if name == "" {
return "", errors.New("name must not be empty")
}
return fmt.Sprintf("Hello, %s!", name), nil
}
func main() {
msg, err := greet("Gopher")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(msg) // Hello, Gopher!
_, err = greet("")
if err != nil {
fmt.Println("error:", err) // error: name must not be empty
}
}

errors.New สร้าง error value ธรรมดาจาก string คงที่ function คืนค่า nil เป็น error ในเส้นทางปกติ และคืนค่า error ที่ไม่ใช่ nil ในเส้นทางที่ล้มเหลว caller ตรวจสอบ if err != nil ทันทีหลังการเรียก — นี่คือ idiom ที่คุณจะเขียนนับร้อยครั้งใน Go

สิ่งที่ได้ประโยชน์ต้นทุน
explicit error handlingทุก failure point มองเห็นได้ชัดเจนใน codeverbose กว่า try/catch
error as value (not exception)composable, predictable control flowcaller ต้องตรวจ error ทุกครั้ง
generics (Go 1.18+)type-safe reuse, ไม่ต้อง interface{}compile time เพิ่ม, syntax ซับซ้อนกว่า
panic/recoverabort ทันทีเมื่อ invariant ถูกละเมิดไม่ใช่ error handling ทั่วไป
  • error handling ใน Go verbose เกินไป — verbosity นี้ตั้งใจ: ทุก failure path มองเห็นได้ในโค้ด ไม่มี hidden exception
  • panic = exception ที่ควรใช้แทน return error — panic ใช้สำหรับ programmer error เท่านั้น ไม่ใช่ expected failure
  • generics ทำ code ช้าลง — Go generics ใช้ GC-shape stenciling: type ที่มี pointer/interface shape เดียวกันแชร์ instantiation เดียวกันและใช้ dictionary ส่ง type info ตอน runtime กรณีนี้ generics อาจช้ากว่า interface ด้วยซ้ำ; ส่วน value type (int, struct) ได้ code เฉพาะของตัวเองต่อ type จึงเร็ว

ทุกบทเรียนในโมดูลนี้ใช้การตรวจสอบชุดเดียวกันเพื่อจับ bug จากโค้ดที่ generate มาก่อนจะเอาไปใช้จริง:

  • function คืนตัวแปร pointer แบบมี type ตรง ๆ ผ่าน result ที่เป็น error แทนที่จะคืน nil เปล่า ๆ ในเส้นทางสำเร็จหรือเปล่า? นั่นคือกับดัก typed-nil interface และ err == nil จะผิด
  • errors.As ถูกเรียกด้วยตัวแปร target เองแทนที่จะเป็น address ของมัน (&target) หรือเปล่า? รูปแบบแรก panic ไม่ใช่แค่คืน false
  • recover() ถูกเรียกข้างใน helper function ที่ deferred function เรียกอีกที แทนที่จะเรียกตรง ๆ ข้างใน body ของ defer func() { ... }() หรือเปล่า? แค่ระดับความห่างชั้นเดียวก็เพียงพอที่จะทำให้มันคืน nil เสมอ
  • constraint interface (ที่มี type term คั่นด้วย | หรือ ~) ถูกใช้เป็น type ของตัวแปรธรรมดา แทนที่จะใช้แค่ข้างใน [T Constraint] หรือเปล่า?
  • โค้ดเรียก sort.Strings หรือคล้ายกันตรง ๆ กับผลลัพธ์ของ maps.Keys หรือเปล่า? ตั้งแต่ Go 1.23 มันคืน iter.Seq ไม่ใช่ slice และ compile ไม่ผ่าน

🐛 หา bug ในโค้ดนี้

type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func mightFail(fail bool) error {
var e *MyError
if fail {
e = &MyError{msg: "boom"}
}
return e
}
func main() {
err := mightFail(false)
fmt.Println(err == nil)
}
เฉลย

mightFail คืนตัวแปร e ที่มี type เป็น *MyError ตรง ๆ แม้ e จะเป็น nil ก็ตาม error interface ที่ return กลับมามี concrete type (*MyError) และ value เป็น nil — นั่นคือ interface ที่ไม่ nil ที่ถือ pointer เป็น nil อยู่ข้างใน ไม่ใช่ nil interface ตรวจสอบแล้วบน 1.26.5: โค้ดนี้พิมพ์ false ไม่ใช่ true ทางแก้คือถือ *MyError แค่ใน local variable แล้วคืน nil เปล่า ๆ ที่ไม่มี type ในเส้นทางสำเร็จ — ดูคำอธิบายเต็มพร้อม playground ที่รันได้ใน Error Values

ใน Go เมื่อ function คืนค่า error ที่ไม่ใช่ nil หมายความว่าอะไร?
โค้ดนี้พิมพ์อะไร?
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }

func mightFail(fail bool) error {
    var e *MyError
    if fail {
        e = &MyError{msg: "boom"}
    }
    return e
}

fmt.Println(mightFail(false) == nil)
จะเกิดอะไรขึ้น?
var ve *VErr
errors.As(err, ve) // not &ve
interface error ในตัว (built-in) นิยามว่าอย่างไร?
package ใดที่ให้ errors.New และ errors.Is?
Generics ถูกเพิ่มเข้ามาใน Go เวอร์ชันใด?