Errors & Generics
การจัดการ Error ใน Go
หัวข้อที่มีชื่อว่า “การจัดการ Error ใน Go”ภาษาส่วนใหญ่จัดการกับความล้มเหลวผ่าน 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 Values | interface 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 ป้องกันจากภายนอกไม่ได้ |
| Generics | Type parameters, constraints, comparable, cmp.Ordered, generic method (Go 1.27) |
| Iterators | iter.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 มองเห็นได้ชัดเจนใน code | verbose กว่า try/catch |
| error as value (not exception) | composable, predictable control flow | caller ต้องตรวจ error ทุกครั้ง |
| generics (Go 1.18+) | type-safe reuse, ไม่ต้อง interface{} | compile time เพิ่ม, syntax ซับซ้อนกว่า |
| panic/recover | abort ทันทีเมื่อ 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 จึงเร็ว
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ทุกบทเรียนในโมดูลนี้ใช้การตรวจสอบชุดเดียวกันเพื่อจับ bug จากโค้ดที่ generate มาก่อนจะเอาไปใช้จริง:
- function คืนตัวแปร pointer แบบมี type ตรง ๆ ผ่าน result ที่เป็น
errorแทนที่จะคืนnilเปล่า ๆ ในเส้นทางสำเร็จหรือเปล่า? นั่นคือกับดัก typed-nil interface และerr == nilจะผิด errors.Asถูกเรียกด้วยตัวแปร target เองแทนที่จะเป็น address ของมัน (&target) หรือเปล่า? รูปแบบแรก panic ไม่ใช่แค่คืนfalserecover()ถูกเรียกข้างใน 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