1 · ปัญหาที่ defer แก้
ลองดูโค้ดแบบไม่มี defer ก่อน — นับดูว่า Close() ต้องเขียนกี่ที
func readFile(name string) error {
f, err := os.Open(name)
if err != nil { return err }
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil {
f.Close() // ← 1
return err
}
if n == 0 {
f.Close() // ← 2 (ลืมข้อนี้ = file leak)
return errEmpty
}
f.Close() // ← 3
return nil
}
เขียนใหม่ด้วย defer:
func readFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err // ยังไม่ได้เปิด → ไม่ต้องปิด
}
defer f.Close() // ✓ เขียนครั้งเดียว ติดกับ Open เลย
data := make([]byte, 100)
n, err := f.Read(data)
if err != nil { return err } // Close ทำงานที่นี่
if n == 0 { return errEmpty } // และที่นี่
return nil // และที่นี่
}
ไม่ใช่แค่ "โค้ดสั้นลง" แต่คือ Open กับ Close
อยู่ติดกันในสายตา — reviewer เห็นสองบรรทัดนี้คู่กันแล้วรู้ทันทีว่า
ไม่รั่ว ไม่ต้องไล่อ่านทั้งฟังก์ชัน
2 · กฎ 3 ข้อ (จาก blog ทางการ)
Defer, Panic, and Recover ระบุกฎไว้ 3 ข้อพอดี ท่องให้ได้ทั้งสามข้อ
กฎ 1 — argument ถูกประเมิน ตอนบรรทัด defer ไม่ใช่ตอนทำงาน
"A deferred function's arguments are evaluated when the defer statement is evaluated."
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
พิมพ์เลขอะไรออกมา?
0 — ไม่ใช่ 1
ค่า i ถูก "แช่แข็ง" เป็น 0 ตั้งแต่วินาทีที่เจอบรรทัด
defer การ i++ ทีหลังไม่มีผลอะไรเลย
นี่คือกฎที่หลอกคนมากที่สุดในสามข้อ
func(){ … }() คืออะไร
ใน Go เขียนฟังก์ชันที่ไม่มีชื่อ ตรงกลางโค้ดได้ เรียกว่า
anonymous function หน้าตาแบบนี้ func(){ fmt.Println("hi") }
— เขียนแค่นี้ยังไม่ทำงาน มันเป็นแค่ "ก้อนฟังก์ชัน" เฉย ๆ
ต้องเติมวงเล็บ () ต่อท้ายถึงจะเรียกมัน:
func(){ fmt.Println("hi") }() ← สังเกตวงเล็บคู่สุดท้าย
และเพราะก้อนฟังก์ชันนี้ถูกเขียนอยู่ข้างในฟังก์ชันอื่น มันจึงมองเห็นและ
อ่านตัวแปรรอบ ๆ ตัวมันได้ (เช่น i) ฟังก์ชันที่ "จำตัวแปรรอบตัวได้"
แบบนี้ภาษาโปรแกรมเรียกว่า closure — และมันไปอ่านค่าจริง
ตอนที่มันทำงาน ไม่ใช่ตอนที่มันถูกเขียน
ถ้าอยากได้ค่า ล่าสุด ให้ห่อด้วย closure แทน:
i := 0
defer fmt.Println(i) // พิมพ์ 0 — ส่งค่าเข้าไป
defer func() { fmt.Println(i) }() // พิมพ์ 1 — closure อ่านตอนทำงาน
i++
วิธีจำ: defer f(x) → x ถ่ายรูปเก็บไว้เลย ·
defer func(){ …x… }() → ไม่มี argument จึงไม่มีอะไรให้ถ่ายรูป
ต้องไปอ่านตัวแปรจริงตอนทำงาน
คุณสั่ง "ข้าวผัด 2 จาน" พนักงานจดใส่ใบสั่งทันทีตอนที่คุณพูด แล้วเดินไปแปะไว้ที่ครัว
อีกสิบนาทีคุณนึกในใจว่าอยากได้ 3 จาน แต่ไม่ได้เดินไปบอกใคร
ของที่มาเสิร์ฟก็ยังเป็น 2 จาน อยู่ดี เพราะใบสั่งจดค่าไว้ตั้งแต่ตอนสั่งแล้ว
· defer fmt.Println(i) = จดค่าลงใบสั่ง จดตอนเจอบรรทัดนั้นเลย
· defer func(){ fmt.Println(i) }() = สั่งว่า
"เดี๋ยวก่อนจะทำ ค่อยมาถามผมอีกทีว่าเอากี่จาน" จึงได้ค่าล่าสุดเสมอ
ที่ไม่เหมือนกัน: ในร้านจริง ถ้าวิ่งไปบอกครัวทันก็ยังแก้ใบสั่งได้
แต่ใน Go พอผ่านบรรทัด defer ไปแล้ว ตัวค่าที่จดลงใบสั่ง
เปลี่ยนไม่ได้อีกเลย
· ข้อยกเว้นที่ต้องรู้ไว้: สิ่งที่ถูกแช่แข็งคือตัว argument เท่านั้น
ไม่ใช่ข้อมูลที่มันชี้ไป · ถ้าค่าที่จดไว้เป็นที่อยู่ของของ
(pointer, slice, map) ของที่ปลายทางยังถูกแก้ทีหลังได้อยู่ — เช่น
s := []int{1} แล้ว defer fmt.Println(s) ตามด้วย
s[0] = 9 จะพิมพ์ [9] ไม่ใช่ [1]
เพราะตัว slice ถูกก๊อป แต่ช่องเก็บข้อมูลจริงยังเป็นก้อนเดียวกัน
คำพวกนี้คือคำที่ dev ใช้เรียกสิ่งที่คุณเพิ่งเข้าใจไป ใช้คำให้ตรง จะได้คำตอบที่ตรง
- argument
- ค่าที่เราใส่ในวงเล็บตอนเรียกฟังก์ชัน · ใน
fmt.Println(i)ตัวiคือ argument - evaluate (ประเมินค่า)
- “คิดออกมาเป็นค่าจริง ๆ” · ประโยคมาตรฐานของกฎนี้คือ “argument ถูก evaluate ตอน defer statement ไม่ใช่ตอนที่ deferred call ทำงาน” — พูดกับ senior ได้เลย เขาจะรู้ทันทีว่าคุณเข้าใจกฎ 1 แล้ว
- closure
- ฟังก์ชันที่อ้างถึงตัวแปรนอกตัวมันได้ และจำ ตัวแปร ตัวนั้นไว้ (ไม่ใช่จำสำเนาของค่า) ·
ไม่จำเป็นต้องเป็นฟังก์ชันไม่มีชื่อ — เก็บก้อนฟังก์ชันใส่ตัวแปรแล้ว
deferตัวแปรนั้น ก็ยังเป็น closure และยังเห็นค่าล่าสุดเหมือนกัน · แนะนำคนอื่นว่า “ห่อด้วย closure สิ จะได้ค่าล่าสุด” · ถาม AI ว่า “closure ใน Go capture ตัวแปรเอง หรือ capture สำเนาของค่า” จะตรงจุดกว่าถามว่า “ทำไมได้ค่าไม่เหมือนกัน”
กฎ 2 — ทำงานแบบ LIFO หลังฟังก์ชัน return
"Deferred function calls are executed in Last In First Out order after the surrounding function returns."
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
พิมพ์อะไรออกมา?
3210
กองซ้อนกันเหมือน stack — ตัวที่ประกาศทีหลังทำงานก่อน
(และสังเกตว่ากฎ 1 ทำงานด้วย: แต่ละรอบถ่ายรูปค่า i ของรอบนั้นไว้)
LIFO (Last In First Out = เข้าทีหลัง ออกก่อน) ไม่ใช่เรื่องบังเอิญ — ของที่เปิดทีหลัง มักต้องปิดก่อน ดูตัวอย่างจริงจากงาน backend:
func processOrder(db *sql.DB) error {
logFile, _ := os.OpenFile("log.txt", os.O_APPEND, 0644)
defer logFile.Close() // ประกาศก่อน → ทำทีหลังสุด
tx, _ := db.Begin()
defer tx.Rollback() // ประกาศทีหลัง → ทำก่อน ✓ ถูกลำดับ
if _, err := tx.Exec("INSERT INTO orders ..."); err != nil {
return err // rollback อัตโนมัติ
}
return tx.Commit() // commit สำเร็จ → Rollback ที่ตามมากลายเป็น no-op
}
defer tx.Rollback() ทันทีหลัง Begin() แปลว่า
"ถ้าไม่ commit สำเร็จ ให้ย้อนกลับ" — ไม่ต้องเขียน rollback ซ้ำในทุกจุดที่
return error
แล้วถ้า Commit() สำเร็จล่ะ — Rollback() ที่ตั้ง defer ไว้
ก็ยังทำงานอยู่ดีไม่ใช่เหรอ? ใช่ มันทำงานจริง แต่ไม่มีอะไรให้ย้อนแล้ว
เพราะ transaction (กลุ่มคำสั่งฐานข้อมูลที่ต้อง "สำเร็จทั้งชุด หรือไม่ก็ยกเลิกทั้งชุด")
หนึ่งชุดจบได้ครั้งเดียว
พอ commit ไปแล้ว Rollback() จะคืน error sql.ErrTxDone
กลับมาเฉย ๆ แล้วไม่แตะฐานข้อมูลเลย เราจงใจไม่รับ error ตัวนั้น
เพราะมันคือกรณีปกติที่คาดไว้อยู่แล้ว
ตอนแต่งตัว คุณใส่ถุงเท้าก่อน แล้วค่อยใส่รองเท้าทับ
ตอนจะถอด ไม่มีใครถอดถุงเท้าก่อน ต้องถอดรองเท้าออกก่อน เสมอ
ของที่สวมทีหลัง ต้องถอดก่อน — นี่คือ LIFO ที่เราทำกันอยู่ทุกวันโดยไม่รู้ตัว
โค้ดข้างบนก็ลำดับเดียวกัน: logFile เปิดก่อน ปิดทีหลัง ·
tx เปิดทีหลัง ปิดก่อน · เราแค่เขียน defer
ต่อท้ายทุกครั้งที่เปิดของ ที่เหลือ Go จัดลำดับย้อนกลับให้เอง
แต่อย่าเพิ่งเหมาว่าโค้ดข้างบนจะพังถ้าปิดสลับลำดับ —
logFile กับ tx ในสนิปนั้นเป็นคนละของกัน ไม่ได้ซ้อนทับกันจริง
สลับลำดับปิดก็ยังทำงานได้ · LIFO จะสำคัญจริงตอนของซ้อนบนกันจริง ๆ เช่น
เปิดไฟล์แล้วสร้าง bufio.Writer ทับบนไฟล์ตัวนั้น ถ้าปิดไฟล์ก่อนแล้วค่อย
Flush() ข้อความที่ค้างในบัฟเฟอร์จะหายไปทั้งก้อน ไฟล์ออกมาว่างเปล่า
ที่ไม่เหมือนกัน: ถอดรองเท้าผิดลำดับในชีวิตจริง เรารู้ตัวทันทีเพราะมันฝืน
แต่ในโค้ดไม่มีอะไรมาเตือนเลยถ้าลำดับผิด · และ Go เรียงลำดับจาก
บรรทัด defer ที่ถูกรันจริง ไม่ใช่ลำดับที่เห็นในไฟล์ —
defer ที่อยู่ใน if ซึ่งไม่ได้ทำงาน จะไม่ถูกนับเข้ากองเลย
- LIFO / stack
- LIFO ย่อจาก Last In First Out = “เข้าทีหลัง ออกก่อน” · กองของที่ทำงานแบบนี้เรียกว่า stack (เหมือนจานซ้อนกันในซิงค์ ใบที่วางล่าสุดคือใบแรกที่ถูกหยิบ)
- deferred call
- ฟังก์ชันหนึ่งตัวที่ถูก
deferเอาไว้ · ถาม AI ว่า “ฟังก์ชันนี้มี deferred call กี่ตัว รันเรียงลำดับยังไง” จะได้คำตอบเป็นลำดับที่ชัดเจน - no-op (no operation)
- “เรียกแล้วไม่เกิดอะไรขึ้น” · เช่น
Rollback()หลัง commit สำเร็จไปแล้ว · ประโยคที่ได้ยินบ่อยตอนรีวิวโค้ดคือ “ตรงนี้มันเป็น no-op อยู่แล้ว ไม่ต้องไปดักไว้” - resource cleanup
- งาน “คืนของที่ยืมมา” เช่น ปิด file ปิด connection ปลดล็อก · พูดว่า “ใช้ defer เพื่อ cleanup ให้ครบทุกทางออกของฟังก์ชัน”
กฎ 3 — deferred function แก้ named return value ได้
"Deferred functions may read and assign to the returning function's named return values."
func c() (i int) { // ← ตั้งชื่อ return value ว่า i
defer func() { i++ }()
return 1
}
ฟังก์ชันนี้คืนค่าอะไร?
2
คำสั่ง return 1 ในฟังก์ชันที่มี named return value ไม่ได้จบในก้าวเดียว
แต่ทำงาน 3 ขั้นตามลำดับ:
(1) เอาค่า 1 ไปใส่ในตัวแปร i → ตอนนี้ i = 1
(2) รัน deferred function ทั้งหมด → i++ ทำงาน → i = 2
(3) ค่อยส่งค่าที่อยู่ใน i ณ ตอนนั้นออกไปจริง → คืน 2
ขั้นที่ (2) แทรกอยู่ระหว่าง "ใส่ค่า" กับ "ส่งออก" นี่แหละคือช่องที่ deferred
แอบเข้าไปแก้ได้ และต้องตั้งชื่อ return value เท่านั้นถึงจะมีตัวแปรให้แก้
ใช้ทำอะไรจริง — ห่อ error ให้มีบริบทโดยไม่ต้องแก้ทุกจุด return:
func loadConfig(path string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("loadConfig %s: %w", path, err)
}
}()
// ...มี return err อยู่ 5 จุด ทุกจุดจะถูกห่อให้อัตโนมัติ
}
%w คือ verb พิเศษที่มีเฉพาะใน fmt.Errorf
(verb = ตัวอักษรที่ตามหลัง % ซึ่งบอกว่าค่าตรงนั้นจะพิมพ์ออกมาแบบไหน)
หน้าตา output ของมันเหมือน %v ทุกอย่าง แต่มันเก็บ error ตัวเดิมติดไปด้วย
ทำให้โค้ดชั้นบนยังตรวจได้ว่า "ข้างในมัน error ตัวไหนกันแน่"
ถ้าใช้ %v ข้อความจะเหมือนกันเป๊ะแต่ error ตัวเดิมหายไป เหลือแค่ตัวหนังสือ
เวลาห่อ error ให้ใช้ %w เสมอ
คุณแพ็กของใส่กล่องที่เขียนชื่อไว้ข้างกล่อง แล้ววางไว้หน้าบ้านรอรถขนส่งมารับ
ระหว่างที่กล่องยังวางอยู่ตรงนั้น ยังมีจังหวะเปิดฝาใส่ของเพิ่มได้อีก
สิ่งที่ปลายทางได้รับ คือของตอนที่รถมารับ ไม่ใช่ตอนที่คุณเพิ่งแพ็กเสร็จ
เทียบกับโค้ดกฎ 3 (func c() (i int) ที่คืน 2):
return 1 = แพ็กของใส่กล่องชื่อ i ·
deferred function = จังหวะที่ยังเปิดฝาใส่เพิ่มได้ · การส่งค่าออกจริง ๆ = รถมารับกล่องไป
โค้ด loadConfig ที่เพิ่งดูไปข้างบนก็เรื่องเดียวกัน:
err คือกล่องที่เขียนชื่อไว้ · deferred func เลยเปิดฝาไปห่อมันเพิ่มได้
ทุกจุดที่ return โดยไม่ต้องไปแก้ทีละจุด
ที่ไม่เหมือนกัน: กล่องพัสดุจริงเปิดฝาได้เสมอ แต่ในโค้ด
กล่องต้องมีชื่อเขียนไว้ก่อนถึงจะเปิดได้ — func c() (i int)
เท่านั้นที่มีชื่อให้อ้างถึง · ถ้าเขียน func c() int เฉย ๆ ของก็ยังไปวางรอเหมือนกัน
แต่ไม่มีชื่อให้เราเขียนโค้ดไปแตะมันได้เลย
- named return value
- การตั้งชื่อค่าที่ฟังก์ชันจะคืน เช่น
(err error)· เวลาคุยกับ senior ให้พูดว่า “ตรงนี้ต้องใช้ named return value ไหม deferred func จะได้แก้ err ได้” จะสื่อกว่าพูดว่า “ใส่ชื่อตรงวงเล็บข้างหลัง” - error wrapping
- “ห่อ error ตัวเดิมไว้ข้างใน error ตัวใหม่” ทำด้วย
%w· ประโยคที่ใช้ตอนรีวิวโค้ดคือ “ตรงนี้ wrap error ด้วย %w หรือยัง” - errors.Is
- ถามว่า “ในห่อมี error ตัวนี้ อยู่ไหม” · ใช้กับ error สำเร็จรูปที่ทั้งโปรเจกต์
ใช้ตัวเดียวกัน เช่น
sql.ErrNoRows· ถ้าห่อด้วย%vแทน%wมันจะหาไม่เจอ คืนfalse— นี่คือเหตุผลที่ต้องใช้%w· ลองพิมพ์ถาม AI ว่า “ทำไม errors.Is ถึงหา error ไม่เจอ ถ้าห่อด้วย %v” แล้วอ่านคำตอบดู - errors.As
- ดึง error ในห่อออกมาเป็นชนิดที่เราประกาศเอง เพื่ออ่านข้อมูลข้างใน (เช่น field
Code) · ระวังจุดที่คนพลาดบ่อย: error ชนิดที่เราสร้างใหม่ทุกครั้ง ถ้าเอาไปเทียบด้วยerrors.Isจะได้falseตลอด เพราะมันคนละตัวกัน — เคสนี้ต้องใช้errors.As
3 · 2 กับดักที่ต้องรู้
defer ในลูปจะ กองสะสมจนจบฟังก์ชัน ไม่ใช่จบทุกรอบ
// ❌ เปิด file 1000 ตัวค้างพร้อมกัน
for _, name := range files {
f, _ := os.Open(name)
defer f.Close()
}
// ✓ ครอบด้วยฟังก์ชันย่อย → จบทุกรอบ
for _, name := range files {
func() {
f, _ := os.Open(name)
defer f.Close()
// ...
}()
}
ไม่มีเครื่องมือไหนจับกับดักนี้ให้อัตโนมัติ — compiler ไม่เตือน
go vet ก็ไม่เตือน วิธีเดียวคือใช้สายตา:
เห็น defer อยู่ในบล็อกของ for เมื่อไหร่ ให้หยุดดูทันที
ว่าลูปนี้วนกี่รอบ ถ้าจำนวนรอบไม่แน่นอน (มาจาก input/ฐานข้อมูล) ให้ครอบด้วยฟังก์ชันย่อยเสมอ
defer fmt.Println("จะไม่ถูกพิมพ์")
log.Fatal("boom") // = log.Print + os.Exit(1) → ตายทันที
เทียบกับอีกกรณีหนึ่ง: panic คือตอนที่โปรแกรมเจอความผิดพลาดร้ายแรง
จนไปต่อไม่ได้ (เช่น s[10] ทั้งที่ slice มี 3 ช่อง) แล้วมันเลิกทำงาน
ฟังก์ชันปัจจุบัน ถอยออกไปหาฟังก์ชันที่เรียกมัน แล้วถอยต่อไปเรื่อย ๆ จนสุดทาง
— ระหว่างที่ถอยออกทีละชั้นนั้น Go รัน defer ของทุกชั้นให้ครบ
ต่างจาก os.Exit/log.Fatal ที่ตัดไฟทิ้งทันทีไม่ถอยให้
นั่นแหละที่ทำให้ defer เป็นเครื่องมือคืน resource
(ของที่ยืมจากระบบมา เช่น file, connection, ล็อก) ที่ปลอดภัยที่สุด
4 · ทดสอบความจำ
1. x := 5; defer fmt.Println(x); x = 99 — พิมพ์อะไร?
กฎ 1 — argument ถ่ายรูปเก็บตอนบรรทัด defer ถ้าอยากได้ 99
ต้องเขียน defer func(){ fmt.Println(x) }()
2. เขียน defer A() แล้ว defer B() ตามลำดับ อะไรทำงานก่อน?
กฎ 2 — LIFO เหมือน stack ออกแบบมาเพื่อให้คลาย dependency ย้อนกลับได้ถูกต้อง (เปิด db → เปิด tx → ปิด tx → ปิด db)
3. โปรแกรมเจอ panic กลางทาง — defer ที่ประกาศไว้แล้วจะ…
panic คลาย stack ทีละชั้น และรัน deferred ของแต่ละชั้นครบ นี่คือเหตุผลที่
defer mu.Unlock() ปลอดภัยกว่าการเรียก mu.Unlock() เอง —
ต่อให้โค้ดตรงกลาง panic ก็ยังปลดล็อกให้
ข้อยกเว้นเดียวคือ os.Exit /
log.Fatal ที่ตายทันทีไม่คลาย stack
4. defer จะแก้ค่าที่ฟังก์ชันคืนออกไปได้ ก็ต่อเมื่อ…
กฎ 3 — ต้องเป็น named return value เช่น
func f() (err error) ถึงจะมีตัวแปรให้ deferred function ไปแก้ได้
ถ้าเขียน func f() error เฉย ๆ จะไม่มีชื่อให้อ้างถึง
5 · ลงมือทำ
ภารกิจ: ทำ utility จับเวลา
- รันโค้ดกฎ 1, 2, 3 ทั้งสามอันใน Playground ให้เห็นผลจริงด้วยตา
- แก้ตัวอย่างกฎ 1 ให้พิมพ์
1แทน0โดยห้ามย้ายบรรทัดdefer - ก่อนอื่นสังเกตอย่างเดียว:
trackTimeประกาศว่าคืนค่าชนิดfunc()— แปลว่ามันคืนฟังก์ชันออกมาเป็นค่า ไม่ได้คืนตัวเลขหรือ string ก็อปโค้ดข้างล่างไปรัน แล้วเช็คว่า output มีบรรทัดที่ขึ้นต้นด้วยwork tookตามด้วยเวลาราว ๆ 500 ms (ของผมรันได้work took 501.140125ms— ตัวเลขทศนิยมของคุณจะไม่เท่ากันเป๊ะ):func trackTime(name string) func() { start := time.Now() return func() { fmt.Printf("%s took %v\n", name, time.Since(start)) } } func work() { defer trackTime("work")() // ← สองชุด! time.Sleep(500 * time.Millisecond) } - ลบวงเล็บชุดหลังออกเหลือ
defer trackTime("work")แล้วรันใหม่ — ผลที่ควรเห็น: คอมไพล์ผ่าน แต่ไม่พิมพ์อะไรเลย (go vetก็ไม่ว่าอะไรด้วย) - ตรวจคำอธิบายของคุณกับเฉลยนี้ ต้องตรงกันทั้งสองบรรทัด:
- วงเล็บชุดแรก
trackTime("work")ทำงาน ทันทีตอนเจอบรรทัด defer (กฎ 1 — argument ถูกประเมินตรงนั้น) มันจึงจับเวลาเริ่มต้นได้ถูกจุด แล้วคืน "ก้อนฟังก์ชัน" ออกมา - วงเล็บชุดที่สอง คือตัวที่บอก
deferว่า ก้อนฟังก์ชันที่ได้มานั่นแหละคือสิ่งที่ต้องเรียกตอนจบฟังก์ชัน ถ้าไม่มีชุดนี้ สิ่งที่ถูก defer จะกลายเป็นตัวtrackTimeเอง → มันไปเริ่มจับเวลาตอนworkจบพอดี แล้วโยนก้อนฟังก์ชันทิ้ง ไม่มีใครเรียก จึงเงียบสนิท
- วงเล็บชุดแรก
- ท้าทาย: เขียนฟังก์ชันที่ใช้กฎ 3 ห่อ error —
func doWork() (err error)ที่ไม่ว่าจะ return จากจุดไหน error จะถูกเติมคำว่า"doWork: "ข้างหน้าเสมอ
ต่อไปทุกครั้งที่พิมพ์ Open, Begin, Lock
หรือ Dial มือคุณจะพิมพ์บรรทัด defer ต่อทันทีโดยไม่ต้องคิด
— นั่นคือนิสัยที่แยกโค้ดที่รั่วออกจากโค้ดที่ไม่รั่ว
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ:
Defer, Panic, and Recover
— สั้นมาก อ่าน 6 นาทีจบ ส่วนท้ายว่าด้วย recover ยังไม่ต้องอ่านลึก
แต่ให้รู้ว่ามีอยู่
💬 ติดตรงไหนถามได้เลย — ข้อ 3 ของภารกิจ (วงเล็บสองชุด) เป็นจุดที่รวมความรู้ 3 เรื่องเข้าด้วยกัน: closure + first-class function (ฟังก์ชันที่เก็บใส่ตัวแปร ส่งเป็น argument หรือคืนกลับมาได้ เหมือนค่าตัวหนึ่ง) + defer ถ้าตรงนี้ผ่าน แปลว่า Part 1 ของ Day 3 แน่นแล้ว
อ้างอิง
- Defer, Panic, and Recover — กฎ 3 ข้อ พร้อมตัวอย่าง
a(),b(),c()ที่ยกมาในบทนี้ - Go spec — Defer statements — พฤติกรรมตอน panic และการคลาย stack
- Go Code Review Comments — Don't Panic, Indent Error Flow