error เป็นค่า ไม่ใช่ข้อยกเว้น
บทที่ 5 ทำให้ orderctl คิดส่วนลดได้ แต่มันยังเชื่อทุกอย่างที่อ่านเข้ามา
บทนี้เติมชั้นที่ขาด — order.Validate ที่ตรวจออเดอร์แล้วบอกว่า ผิดกี่ข้อ ข้อไหนบ้าง
ไม่ใช่หยุดที่ข้อแรกแล้วเงียบ
ErrNoLinesและErrBadTable— ค่า error ระดับ package ที่ผู้เรียกเทียบได้*order.FieldError— error ชนิดของตัวเอง ที่พก field, ค่าที่ผิด และเหตุผลมาด้วยOrder.Validateที่คืน error ตัวเดียวซึ่งห่อทุกข้อที่ผิดไว้ข้างใน- exit code ของโปรแกรมบรรทัดคำสั่ง ที่ผูกกับชนิดของความผิดพลาด
Go ไม่มี exception และจะไม่มี
หัวข้อที่มีชื่อว่า “Go ไม่มี exception และจะไม่มี”ทีม Go ปิดประตูเรื่องไวยากรณ์ใหม่สำหรับ error ไปแล้ว
if err != nilจึงเป็นรูปสุดท้าย
ทุกภาษาที่คุณเคยใช้มีทางลัดให้ความผิดพลาดเด้งขึ้นไปหาผู้เรียกเอง Go ไม่มีทางนั้นเลย
error เป็นแค่ interface ที่มี method เดียว ฟังก์ชันคืนมันกลับมาเหมือนคืนตัวเลข
และผู้เรียกต้องเขียนว่าจะทำอะไรกับมัน ณ จุดที่ได้รับ
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์type error interface { Error() string}หลายปีที่ผ่านมามีข้อเสนอให้เพิ่มไวยากรณ์ย่อ เช่น try หรือตัวดำเนินการ ?
Robert Griesemer เขียนไว้เมื่อ 2025-06-03 ว่า “For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling” และจะปิดข้อเสนอที่ว่าด้วย ไวยากรณ์ของ error handling เป็นหลักทุกฉบับ “without further investigation”
ประกาศนั้นพูดถึง ไวยากรณ์ และพูดถึง อนาคตที่มองเห็นได้ ไม่ได้บอกว่า error handling ของ Go จะไม่เปลี่ยนอีกเลย · แต่มันพอจะบอกได้ว่าการรอไวยากรณ์ที่สั้นกว่านี้ ไม่ใช่แผนที่ดี
| สิ่งที่คุณคาดจากภาษาเดิม | สิ่งที่ Go ให้จริง |
|---|---|
| ความผิดพลาดลอยขึ้นไปเองจนกว่าจะมีใครรับ | ไม่ลอย ถ้าคุณไม่คืนมันต่อ มันหายไปตรงนั้น |
| error พก stack trace มาด้วย | มีแค่ข้อความจาก Error() string |
เทียบชนิดด้วย catch (FooException e) | เทียบด้วย errors.Is หรือ errors.As |
มี AggregateException ให้ใช้ | errors.Join ในไลบรารีมาตรฐานตั้งแต่ Go 1.20 |
ถ้าคุณมาจาก C# หรือ Java คุณเคยชินกับการเปิด log แล้วเห็นสายการเรียกทั้งสาย
Go ไม่ให้สิ่งนั้น สิ่งที่แทนมันคือบริบทที่คุณห่อเองทุกชั้นด้วย %w
· error ที่เขียนว่า "open failed" เฉยๆ จึงแทบไม่มีประโยชน์ตอนตีสาม
เอกสาร Effective Go ยกตัวอย่างรูปที่ควรเป็น — "image: unknown format"
คือชื่อ package แล้วตามด้วยสิ่งที่ผิดจริง
sentinel error กับเหตุผลที่ == พัง
หัวข้อที่มีชื่อว่า “sentinel error กับเหตุผลที่ == พัง”ค่า error ระดับ package หนึ่งตัวต่อหนึ่งกรณี เทียบด้วย
errors.Isเพราะการห่อทำให้==เป็นเท็จ
sentinel errorsentinel errorค่า error ที่ประกาศเป็นตัวแปรระดับ package ตัวเดียว ใช้เทียบด้วย `errors.Is` เพื่อแยกกรณี คือค่า error ที่ประกาศเป็นตัวแปรระดับ package ตัวเดียว
แล้วผู้เรียกเทียบกับมันเพื่อแยกกรณี ชื่อขึ้นต้นด้วย Err เป็นธรรมเนียม
ปัญหาเกิดตอนที่มีใครสักคนกลางทางห่อมันเพื่อเติมบริบท
package main
import ( "errors" "fmt")
var ErrNoLines = errors.New("order: ออเดอร์ไม่มีรายการ")
func load(name string) error { return fmt.Errorf("load %q: %w", name, ErrNoLines)}
func main() { err := load("dinner.json")
fmt.Println(err) fmt.Println("err == ErrNoLines:", err == ErrNoLines) fmt.Println("errors.Is:", errors.Is(err, ErrNoLines))}load "dinner.json": order: ออเดอร์ไม่มีรายการerr == ErrNoLines: falseerrors.Is: trueerr ตัวที่ผู้เรียกได้รับ ไม่ใช่ ErrNoLines อีกต่อไป มันคือ error ตัวใหม่ที่ ห่อ
ตัวเดิมไว้ข้างใน · == จึงเป็นเท็จ
errors.Is เดินตามสายการห่อลงไปทีละชั้นแล้วเทียบทุกชั้น เอกสารของ Go
เขียนไว้ตรงๆ ว่ามันควรถูกใช้แทนการเทียบด้วยความเท่ากันธรรมดา
fmt.Errorf มีกริยาสองตัวที่ดูคล้ายกันแต่ผลต่างกันสิ้นเชิง
%v เอาแต่ข้อความมาต่อ สายการห่อขาดตรงนั้น ผู้เรียกเทียบอะไรไม่ได้อีก
· %w เก็บตัวเดิมไว้จริง errors.Is และ errors.As จึงเดินต่อได้
ราคาที่จ่ายคือสิ่งที่ blog ทางการเรียกว่า การห่อ error ทำให้ error ตัวนั้นกลายเป็น
ส่วนหนึ่งของ API ของคุณ · ถ้าวันหนึ่งคุณเลิกห่อ ErrNoLines ผู้เรียกที่เช็กมันอยู่จะพัง
โดยที่ compiler ไม่เตือนสักคำ
error ชนิดของตัวเอง เมื่อผู้เรียกต้องการข้อมูล
หัวข้อที่มีชื่อว่า “error ชนิดของตัวเอง เมื่อผู้เรียกต้องการข้อมูล”sentinel บอกได้แค่ว่า “กรณีไหน” ถ้าผู้เรียกต้องรู้ว่า field ไหนผิด ต้องใช้ชนิดของตัวเอง
ErrBadTable บอกว่าหมายเลขโต๊ะผิด แต่บอกไม่ได้ว่าค่าที่ส่งมาคืออะไร
และช่วงที่ยอมรับคือเท่าไร
เมื่อผู้เรียกต้องการ ข้อมูล ไม่ใช่แค่ชนิด ให้ประกาศ struct แล้วให้มันมี method Error
package main
import ( "errors" "fmt")
var ErrBadTable = errors.New("order: หมายเลขโต๊ะไม่ถูกต้อง")
// FieldError พก field ค่าที่ผิด และเหตุผล มาให้ผู้เรียกอ่านได้type FieldError struct { Field string Value string Reason string Err error}
func (e *FieldError) Error() string { return fmt.Sprintf("field %s = %q: %s", e.Field, e.Value, e.Reason)}
// Unwrap ต้องเขียนเอง ไม่ได้มาฟรีจากการมี field ชื่อ Errfunc (e *FieldError) Unwrap() error { return e.Err }
func checkTable(n int) error { if n < 1 || n > 40 { return &FieldError{ Field: "table", Value: fmt.Sprint(n), Reason: "ต้องอยู่ระหว่าง 1 ถึง 40", Err: ErrBadTable, } } return nil}
func main() { err := fmt.Errorf("validate: %w", checkTable(0))
var fe *FieldError if errors.As(err, &fe) { fmt.Println("field:", fe.Field, "· เหตุผล:", fe.Reason) } fmt.Println("errors.Is(err, ErrBadTable):", errors.Is(err, ErrBadTable))}field: table · เหตุผล: ต้องอยู่ระหว่าง 1 ถึง 40errors.Is(err, ErrBadTable): trueerrors.As ต่างจาก errors.Is ตรงที่มันไม่ได้ตอบว่าใช่หรือไม่ใช่เท่านั้น
· มันเดินหาชั้นที่เข้ากับ target แล้ว เขียนค่าลงใน target ให้คุณอ่าน field ต่อได้
สังเกตว่า checkTable ยังคืน error ไม่ใช่ *FieldError ทั้งที่ข้างในสร้าง *FieldError
· เหตุผลอยู่ในหัวข้อ typed nil ข้างล่าง และมันสำคัญกว่าที่หน้าตาบอก
target ต้องเป็น pointer ไปยังชนิดที่เข้ากับ error หรือ pointer ไปยังชนิด interface ใดก็ได้
ข้อความ panic ที่คุณจะเจอมีสามแบบคนละกรณีกัน:
errors: target cannot be nil— ส่งnilเข้าไปตรงๆerrors: target must be a non-nil pointer— ส่งค่าที่ไม่ใช่ pointererrors: *target must be interface or implement error— ชี้ไปยังชนิดที่ไม่มี methodError
ในตัวอย่างข้างบน Error() ใช้ pointer receiver ชนิดที่เข้ากับ error จึงเป็น *FieldError
และ target จึงต้องเป็น **FieldError ซึ่งคือสิ่งที่ &fe ให้ · ถ้า Error() ใช้ value receiver
กฎนี้จะเปลี่ยนไป
errors.Join รายงานทุกข้อที่ผิด
หัวข้อที่มีชื่อว่า “errors.Join รายงานทุกข้อที่ผิด”ผู้ใช้ที่กรอกผิดหลายช่อง ควรเห็นทุกช่องในรอบเดียว ไม่ใช่แก้ทีละรอบ
errors.Joinerrors.Joinรวม error หลายตัวเป็นตัวเดียวที่มี `Unwrap() []error` และคืน `nil` เองเมื่อทุกตัวเป็น nil ใช้รายงานทุกข้อที่ผิดพร้อมกัน รวม error หลายตัวเป็นตัวเดียวที่มี Unwrap() []error
· errors.Is และ errors.As เดินทุกกิ่งของมันให้
นี่คือส่วนที่บทนี้เพิ่มลงใน internal/order/order.go · Line กับ Order มาจากบทที่ 2
และย่อเหลือเฉพาะ field ที่หัวข้อนี้ใช้ ของจริงมี field มากกว่านี้
package order
import ( "errors" "fmt" "strconv")
// sentinel: ตัวแปรระดับ package ตัวเดียว ขึ้นต้นด้วย Errvar ( ErrNoLines = errors.New("order: ออเดอร์ไม่มีรายการ") ErrBadTable = errors.New("order: หมายเลขโต๊ะไม่ถูกต้อง"))
// Line กับ Order มาจากบทที่ 2 · ย่อเหลือเฉพาะ field ที่หัวข้อนี้ใช้type Line struct { SKU string Qty int}
type Order struct { ID string Table int Lines []Line}
// FieldError คือ error ชนิดของตัวเอง ผู้เรียกอ่าน field ได้type FieldError struct { Field string Value string Reason string Err error}
func (e *FieldError) Error() string { return fmt.Sprintf("field %s = %q: %s", e.Field, e.Value, e.Reason)}
func (e *FieldError) Unwrap() error { return e.Err }
// Validate รายงานทุกข้อที่ผิด ไม่ใช่ข้อแรกfunc (o Order) Validate() error { var errs []error
if o.Table < 1 || o.Table > 40 { errs = append(errs, &FieldError{ Field: "table", Value: strconv.Itoa(o.Table), Reason: "ต้องอยู่ระหว่าง 1 ถึง 40", Err: ErrBadTable, }) } if len(o.Lines) == 0 { errs = append(errs, &FieldError{ Field: "lines", Value: "[]", Reason: "ต้องมีอย่างน้อยหนึ่งรายการ", Err: ErrNoLines, }) } for i, l := range o.Lines { if l.Qty <= 0 { errs = append(errs, &FieldError{ Field: "lines[" + strconv.Itoa(i) + "].qty", Value: strconv.Itoa(l.Qty), Reason: "ต้องมากกว่าศูนย์", }) } }
return errors.Join(errs...) // คืน nil เองเมื่อ errs ว่าง ไม่ต้องเช็ก len}บรรทัดสุดท้ายคือที่ที่คนพลาดบ่อยที่สุด และมันพลาดในทางที่เกินจำเป็น
errors.Join คืน nil เองเมื่อทุกตัวที่ส่งเข้าไปเป็น nil หรือไม่ได้ส่งอะไรเลย
· การเขียน if len(errs) == 0 { return nil } ครอบไว้อีกชั้นจึงไม่ผิด แต่ไม่จำเป็น
package main
import ( "errors" "fmt")
func main() { fmt.Println("ไม่ส่งอะไรเลย:", errors.Join() == nil) fmt.Println("ส่ง nil ล้วน:", errors.Join(nil, nil) == nil)
err := errors.Join(errors.New("ข้อแรก"), errors.New("ข้อที่สอง")) fmt.Println(err)}ไม่ส่งอะไรเลย: trueส่ง nil ล้วน: trueข้อแรกข้อที่สองเอกสารของ errors.Join ระบุว่าข้อความที่ได้คือข้อความของแต่ละตัวต่อกัน
โดยมีการขึ้นบรรทัดใหม่คั่นระหว่างกัน · สองข้อจึงได้สองบรรทัด ไม่ใช่บรรทัดเดียวคั่นด้วยจุลภาค
flowchart TD A["error ตัวเดียวที่ Validate คืนออกมา"] --> B["ค่าจาก errors.Join<br/>มี Unwrap() []error"] B --> C["FieldError<br/>field = table"] B --> D["FieldError<br/>field = lines"] C -->|"Unwrap()"| E["ErrBadTable"] D -->|"Unwrap()"| F["ErrNoLines"] G["errors.Is(err, ErrNoLines)"] -.->|"เดินลงทุกกิ่ง จนกว่าจะเจอ"| B
คำบรรยายภาพ: Validate คืน error ตัวเดียว ข้างในเป็นค่าจาก errors.Join ที่แตกออกเป็นสองกิ่ง · แต่ละกิ่งเป็น FieldError ที่ห่อ sentinel ของตัวเองไว้อีกชั้น · errors.Is จึงต้องเดินทั้งความกว้างและความลึก
errors.Join เข้ามาตั้งแต่ Go 1.20 ซึ่งออกเมื่อ 2023-02
บทความหรือคำตอบใน Stack Overflow ที่ยังบอกให้ลง go.uber.org/multierr
หรือ github.com/hashicorp/go-multierror จึงเขียนก่อนหน้านั้น
· ของแบบนี้ยังใช้ได้อยู่ แต่ในคอร์สนี้เราไม่เพิ่มสิ่งที่พึ่งพาจากข้างนอกเลยสักตัว
กับดัก typed nil
หัวข้อที่มีชื่อว่า “กับดัก typed nil”
err != nilเป็นจริงได้ทั้งที่ไม่มีความผิดพลาดเกิดขึ้น และgo vetไม่จับให้
typed niltyped nilค่า interface ที่ถือ pointer ที่เป็น nil อยู่ข้างใน ทำให้ `err != nil` เป็นจริงทั้งที่ไม่มีความผิดพลาดเกิดขึ้นจริง คือค่า interface ที่ถือ pointer ซึ่งเป็น nil อยู่ข้างใน
ค่าแบบ interface ใน Go มีสองช่อง คือชนิด กับค่า · มันเท่ากับ nil ก็ต่อเมื่อ
ทั้งสองช่องว่าง · การยัด *FieldError ที่เป็น nil เข้าไป ทำให้ช่องชนิดไม่ว่างแล้ว
package main
import "fmt"
type FieldError struct{ Field string }
func (e *FieldError) Error() string { return "field " + e.Field }
// ❌ แบบดิบ ห้ามลอกไปใช้ — ประกาศชนิดที่คืนเป็นชนิดคอนกรีตfunc validateWrong(id string) *FieldError { if id == "" { return &FieldError{Field: "id"} } return nil // nil ตัวนี้เป็น *FieldError ไม่ใช่ error}
func callerWrong(id string) error { return validateWrong(id) // ยัด nil pointer ลงในช่องชนิดของ interface}
// ✅ ถูก — ประกาศชนิดที่คืนเป็น error เสมอfunc validateRight(id string) error { if id == "" { return &FieldError{Field: "id"} } return nil}
func main() { fmt.Println("callerWrong:", callerWrong("A-1") != nil) fmt.Println("validateRight:", validateRight("A-1") != nil)}callerWrong: truevalidateRight: falsecallerWrong("A-1") ผ่านการตรวจทุกข้อ ไม่มีอะไรผิดเลย แต่ผู้เรียกที่เขียน
if err != nil จะเข้าไปในกิ่งที่ผิดพลาด
กฎที่กันเรื่องนี้ได้ทั้งหมดมีข้อเดียว — ฟังก์ชันที่รายงานความผิดพลาด ให้ประกาศชนิดที่คืนเป็น
error เสมอ ไม่ใช่ชนิดคอนกรีตของ error นั้น
go vet ไม่มีตัวตรวจสำหรับกรณีนี้ · โปรแกรมข้างบนจึงผ่านทั้ง go build และ go vet
ตัวที่จับได้คือ staticcheck รหัส SA4023 ซึ่งรายงานว่าการเทียบแบบนี้ไม่มีทางเป็นจริง
หรือไม่มีทางเป็นเท็จ
แต่มันไม่ทำงานให้เองถ้าคุณใช้ gopls ตามค่าเริ่มต้น · เอกสารตัววิเคราะห์ของ gopls
ระบุ SA4023 ว่า Default: off คุณต้องเปิดเอง หรือรัน staticcheck เป็นคำสั่งแยก
table-driven test
หัวข้อที่มีชื่อว่า “table-driven test”Go ไม่มี
assertEqualsในไลบรารีมาตรฐาน สิ่งที่แทนมันคือ slice ของกรณีทดสอบกับt.Run
package testing ไม่มี assertEquals และไม่มี assert ตัวไหนเลย
· สิ่งที่คุณเขียนแทนคือ if ธรรมดา แล้วเรียก t.Errorf เมื่อค่าที่ได้ไม่ตรงกับค่าที่คาด
table-driven testtable-driven testสำนวนทดสอบของ Go ที่เก็บกรณีทดสอบเป็น slice ของ struct แล้ววนด้วย `t.Run` ทีละกรณี เก็บกรณีทดสอบเป็น slice ของ struct แล้ววนทีละกรณี · มันเป็นสำนวนที่คุณจะเจอในไลบรารีมาตรฐานเองแทบทุก package
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์// วางไว้ที่ internal/order/order_test.go ข้างๆ order.go ของหัวข้อก่อนหน้าfunc TestValidate(t *testing.T) { tests := []struct { name string order Order wantErr error }{ {"ok", Order{Table: 4, Lines: []Line{{SKU: "a", Qty: 1}}}, nil}, {"table too low", Order{Table: 0, Lines: []Line{{SKU: "a", Qty: 1}}}, ErrBadTable}, {"no lines", Order{Table: 4}, ErrNoLines}, }
for _, tt := range tests { // ตั้งแต่ Go 1.22 ไม่ต้องเขียน tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel()
err := tt.order.Validate() if !errors.Is(err, tt.wantErr) { t.Errorf("Validate() error = %v, want %v", err, tt.wantErr) } }) }}สามอย่างในนั้นเป็นธรรมเนียมที่คุ้มค่าจะจำ
t.Run(tt.name, ...)ทำให้ผลลัพธ์ที่ล้มบอกชื่อกรณีตรงๆ เช่นTestValidate/no_linest.Parallel()ปล่อยกรณีย่อยให้รันพร้อมกัน · ตั้งแต่ Go 1.22 ตัวแปรของลูปเป็นของใหม่ทุกรอบ บรรทัดtt := ttแบบเก่าจึงไม่ต้องมีแล้วerrors.Is(err, tt.wantErr)ใช้ได้ทั้งกรณีที่คาดว่าผิดและกรณีที่คาดว่าไม่ผิด เพราะerrors.Is(nil, nil)เป็นจริง
ข้อความในบรรทัด t.Errorf ก็มีรูปที่ทางการแนะนำ — เขียนสิ่งที่เรียก แล้วตามด้วย
ค่าที่ได้จริงกับค่าที่คาด f(x) = got, want
เอกสาร Code Review Comments กำหนดรูปของข้อความไว้ เพราะข้อความของ error มักถูกเอาไปต่อท้ายบริบทอื่น
fmt.Errorf("load %q: %w", name, err) จะได้ผลอ่านออกก็ต่อเมื่อข้อความข้างในไม่ขึ้นต้น
ด้วยตัวใหญ่และไม่มีจุดปิดท้าย
ข้อยกเว้นคือชื่อเฉพาะและตัวย่อ เช่น JSON หรือ HTTP ที่ขึ้นต้นประโยคได้ตามปกติ
error ที่ไปถึงผู้ใช้ และ exit code
หัวข้อที่มีชื่อว่า “error ที่ไปถึงผู้ใช้ และ exit code”ผู้เรียกชั้นนอกสุดของโปรแกรมบรรทัดคำสั่งคือ
mainและหน้าที่เดียวของมันคือแปล error เป็นตัวเลข
main ไม่ควรมีตรรกะ มันควรเรียกฟังก์ชันที่ทดสอบได้ตัวหนึ่ง แล้วตัดสินใจจากค่าที่ได้
package main
import ( "errors" "fmt" "os")
var ErrNoLines = errors.New("order: ออเดอร์ไม่มีรายการ")
func run(args []string) error { if len(args) == 0 { return errors.New("ต้องระบุชื่อ file ของออเดอร์") } return fmt.Errorf("validate %q: %w", args[0], ErrNoLines)}
func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "orderctl:", err)
if errors.Is(err, ErrNoLines) { os.Exit(2) // ออเดอร์ไม่ผ่านการตรวจ } os.Exit(1) // ผิดพลาดอย่างอื่น }}$ orderctlorderctl: ต้องระบุชื่อ file ของออเดอร์$ echo $?1
$ orderctl dinner.jsonorderctl: validate "dinner.json": order: ออเดอร์ไม่มีรายการ$ echo $?2สังเกตว่าข้อความที่ run คืนออกมาไม่มีคำว่า orderctl: นำหน้า เพราะ main เติมให้แล้ว
· ถ้าใส่ทั้งสองที่ ผู้ใช้จะเห็นชื่อโปรแกรมซ้ำสองครั้งในบรรทัดเดียว
ตัวเลข 1 กับ 2 เป็นข้อตกลงของโปรแกรมนี้เอง ไม่ใช่มาตรฐานของ Go
· สิ่งที่เป็นกฎจริงมีข้อเดียว คือ 0 แปลว่าสำเร็จ
os.Exit ไม่รัน defer ที่ค้างอยู่ · นี่คือเหตุผลที่ทุกอย่างต้องอยู่ใน run
แล้วให้ main เป็นชั้นบางที่สุดที่ทำได้
การแยกแบบนี้ยังทำให้เขียนเทสต์ให้ run ได้ตรงๆ โดยไม่ต้องรันโปรแกรมจริง
· บทที่ 7 จะต่อยอดจาก run ตัวนี้ด้วย flag.FlagSet แล้วเพิ่ม argument
สำหรับที่เขียนออก เพื่อให้เทสต์อ่านสิ่งที่โปรแกรมพิมพ์ได้
คนที่ย้ายมาจากภาษาที่มี exception มักเผลอใช้ panic กับ recover แทน try กับ catch
เอกสาร Code Review Comments มีหัวข้อชื่อ #dont-panic แยกต่างหากจากหัวข้อเรื่อง
การจัดการ error · หลักคือความผิดพลาดที่คาดหมายได้ ให้คืนเป็นค่า
recover มีที่ทางของมันจริง แต่เป็นที่ขอบของระบบ เช่นตัวรับคำขอที่ไม่ควรล้มทั้ง
โปรแกรมเพราะคำขอเดียว · เรื่องนั้นอยู่ในบทที่ 8 ห้ามเอา defer recover() ไปครอบทุกฟังก์ชัน
คอร์สนี้ตั้งขั้นต่ำไว้ที่ go 1.25.0 ทุกอย่างข้างบนจึงใช้ errors.As แบบเดิม
Go 1.26 เพิ่ม errors.AsType[E error](err) ที่คืนค่ากับ bool แทนการเขียน target
· มันอ่านง่ายกว่า แต่ E ต้องเข้ากับ error ดังนั้นการเอามันไปดึงกิ่งของ errors.Join
ด้วย errors.AsType[interface{ Unwrap() []error }] คอมไพล์ไม่ผ่าน
การดึงกิ่งยังต้องเขียนแบบเดิม คือประกาศตัวแปร interface{ Unwrap() []error }
แล้วส่ง pointer ของมันให้ errors.As
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”
orderctlตรวจออเดอร์ได้แล้ว และบอกทุกข้อที่ผิดในรอบเดียว
คุณเลือกได้แล้วว่ากรณีไหนใช้ sentinel และกรณีไหนต้องใช้ error ชนิดของตัวเอง · เกณฑ์คือผู้เรียกต้องการแค่แยกกรณี หรือต้องการข้อมูลไปแสดงต่อ
คุณรู้ว่า errors.Join คืน nil เองเมื่อไม่มีอะไรผิด และรู้ว่า typed nil
ทำให้การเช็กที่ดูปลอดภัยที่สุดเชื่อไม่ได้
บทหน้าเราทำให้ orderctl เป็นเครื่องมือจริง — subcommand ด้วย flag.FlagSet,
log แบบมีโครงสร้างด้วย log/slog และการเขียนสถานะลง disk แบบที่ล้มกลางทางแล้วไม่ทำของเดิมพัง
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- The Go Programming Language Specification — Errors (2026-05-26) —
errorเป็น interface ที่มี method เดียว และnilคือค่าที่แทน “ไม่มีความผิดพลาด” - pkg.go.dev — fmt.Errorf (เข้าถึง 2026-08-27) —
%wหลายตัวในข้อความเดียวทำให้ผลลัพธ์มีUnwrap() []error - pkg.go.dev — errors.Join (เข้าถึง 2026-08-27) — คืน
nilเมื่อทุกตัวเป็น nil และต่อข้อความโดยคั่นด้วยการขึ้นบรรทัดใหม่ - Working with Errors in Go 1.13 (2019-10-17) — การห่อ error ทำให้ error ตัวนั้นเป็นส่วนหนึ่งของ API
- [ On | No ] syntactic support for error handling (Robert Griesemer, 2025-06-03) — “the Go team will stop pursuing syntactic language changes for error handling”
- Go Code Review Comments — error-strings และ dont-panic (เข้าถึง 2026-08-27) — รูปของข้อความ error และหัวข้อแยกเรื่องการไม่ใช้
panic - Effective Go — Errors (เข้าถึง 2026-08-27) — ตัวอย่าง
"image: unknown format" - gopls analyzers (เข้าถึง 2026-08-27) —
SA4023มีสถานะDefault: off
เช็กความเข้าใจ — บทที่ 6
ข้อ 1 / 3ฟังก์ชันกลางทางเขียน `return fmt.Errorf("load %q: %w", name, ErrNoLines)` แล้วผู้เรียกเช็กด้วย `err == ErrNoLines` จะได้อะไร และทำไม