บทเรียน 08 · Resource Cleanup

defer: เปิดแล้วปิดให้ครบทุกทางออก

~12 นาที · ต้องมี: บทเรียน 07

ฟังก์ชันหนึ่งมี return ได้ 5 จุด ถ้าต้องปิด file ทุกจุด คุณต้อง เขียน Close() 5 ครั้ง — และวันหนึ่งจะมีคนเพิ่ม return จุดที่ 6 แล้วลืมเขียน Close() ตรงนั้น file ก็รั่วทันที defer ทำให้ปัญหานี้หายไปเลย ด้วยกฎแค่ 3 ข้อ ซึ่งมีข้อหนึ่งที่หลอกคนเกือบทุกคน

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 กับดักที่ต้องรู้

กับดัก 1 — scope คือ "ฟังก์ชัน" ไม่ใช่ block

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/ฐานข้อมูล) ให้ครอบด้วยฟังก์ชันย่อยเสมอ

กับดัก 2 — os.Exit และ log.Fatal ไม่รัน defer
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. รันโค้ดกฎ 1, 2, 3 ทั้งสามอันใน Playground ให้เห็นผลจริงด้วยตา
  2. แก้ตัวอย่างกฎ 1 ให้พิมพ์ 1 แทน 0 โดยห้ามย้ายบรรทัด defer
  3. ก่อนอื่นสังเกตอย่างเดียว: 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)
    }
  4. ลบวงเล็บชุดหลังออกเหลือ defer trackTime("work") แล้วรันใหม่ — ผลที่ควรเห็น: คอมไพล์ผ่าน แต่ไม่พิมพ์อะไรเลย (go vet ก็ไม่ว่าอะไรด้วย)
  5. ตรวจคำอธิบายของคุณกับเฉลยนี้ ต้องตรงกันทั้งสองบรรทัด:
    • วงเล็บชุดแรก trackTime("work") ทำงาน ทันทีตอนเจอบรรทัด defer (กฎ 1 — argument ถูกประเมินตรงนั้น) มันจึงจับเวลาเริ่มต้นได้ถูกจุด แล้วคืน "ก้อนฟังก์ชัน" ออกมา
    • วงเล็บชุดที่สอง คือตัวที่บอก defer ว่า ก้อนฟังก์ชันที่ได้มานั่นแหละคือสิ่งที่ต้องเรียกตอนจบฟังก์ชัน ถ้าไม่มีชุดนี้ สิ่งที่ถูก defer จะกลายเป็นตัว trackTime เอง → มันไปเริ่มจับเวลาตอน work จบพอดี แล้วโยนก้อนฟังก์ชันทิ้ง ไม่มีใครเรียก จึงเงียบสนิท
  6. ท้าทาย: เขียนฟังก์ชันที่ใช้กฎ 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 แน่นแล้ว

อ้างอิง