Functions
Syntax พื้นฐานของ Function
หัวข้อที่มีชื่อว่า “Syntax พื้นฐานของ Function”Function ใน Go ประกาศด้วย keyword func ตามด้วยชื่อ, parameters, และ return types:
func add(a int, b int) int { return a + b}
// Consecutive parameters of the same type can share a type annotation.// This is a second, equivalent spelling of the same function — pick one style per function.func add(a, b int) int { return a + b}argument ทุกตัวใน Go ถูกส่งแบบ by value — เป็น copy — ไม่มีข้อยกเว้น เห็นได้ชัดสำหรับ int หรือ struct แต่ก็จริงกับ slice ด้วยเช่นกัน: argument ที่เป็น slice copy แค่ header สามคำ (pointer, length, capacity) ไม่ copy backing array นี่คือเหตุผลที่การเขียนลง s[i] ภายใน function จะเห็นผลที่ caller แต่การ append ภายใน function บางครั้งกลับไม่เห็นผล — คำอธิบายเต็มพร้อม diagram ของ header อยู่ที่บท Slices
Multiple Return Values
หัวข้อที่มีชื่อว่า “Multiple Return Values”Go functions สามารถ return หลายค่าได้ ที่เป็น pattern มาตรฐานสำหรับ error handling:
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil}
result, err := divide(10, 3)if err != nil { log.Fatal(err)}fmt.Println(result)Pattern (value, error) ถือเป็น idiomatic Go — แทบทุก function ที่อาจล้มเหลวจะ return error เป็น value สุดท้าย
Named Return Values
หัวข้อที่มีชื่อว่า “Named Return Values”Return values สามารถตั้งชื่อได้ ทำให้สามารถใช้ bare return ได้:
func bounds(nums []int) (lo, hi int) { lo, hi = nums[0], nums[0] for _, v := range nums[1:] { if v < lo { lo = v } if v > hi { hi = v } } return // bare return: returns current lo, hi}ใช้ named returns เพื่อความชัดเจนใน function สั้น ๆ หลีกเลี่ยงใน function ยาวเพราะทำให้อ่านสับสน การตั้งชื่อ named result เป็น min/max ก็ compile ผ่านเช่นกัน — แต่ Go 1.21 เพิ่ม built-in min/max เข้ามาแล้ว local name แบบนี้จะ shadow built-in ไปตลอด function นั้น ถือเป็น smell ที่ควรเลี่ยง
Variadic Functions
หัวข้อที่มีชื่อว่า “Variadic Functions”ใช้ ...T เพื่อรับ arguments จำนวนไม่จำกัดของ type เดียวกัน:
func sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total}
sum(1, 2, 3) // individual argssum(1, 2, 3, 4, 5) // any number of args workssum([]int{1,2,3}...) // spread a slice with ...การ spread slice ที่เป็น nil ก็ใช้ได้เหมือนกันและไม่ใช่กรณีพิเศษ: var none []int; sum(none...) ส่ง slice ความยาวศูนย์เข้าไป loop วนศูนย์รอบ แล้ว return 0 slice ที่เป็น nil จะพฤติกรรมเหมือน slice ความยาวศูนย์ทุกที่ที่มันถูกอ่าน
defer เลื่อนการเรียก function ออกไปจนกว่า function ที่ครอบอยู่จะ return เหมาะสำหรับ cleanup:
func readFile(name string) error { f, err := os.Open(name) if err != nil { return err } defer f.Close() // runs when readFile returns
// ... work with f ... return nil}หลาย defer ใน function เดียวจะ execute แบบ LIFO (last in, first out)
flowchart TB
subgraph Push["Pushed in this order"]
direction LR
P1["defer close(0)"] --> P2["defer close(1)"] --> P3["defer close(2)"]
end
subgraph Pop["Run in this order at return (LIFO)"]
direction LR
R1["close(2)"] --> R2["close(1)"] --> R3["close(0)"]
end Arguments ของ deferred call ถูก evaluate ทันที ณ จุดที่ defer statement ถูก execute ไม่ใช่ตอน call จริง:
i := 0defer fmt.Println(i) // captures i's value (0) right nowi = 100// when the function returns, this prints "0", not "100"defer และ Named Result
หัวข้อที่มีชื่อว่า “defer และ Named Result”deferred closure อ่านและแม้แต่ เขียนทับ named return value ได้ เพราะ named result ก็แค่ตัวแปรที่อยู่ใน scope ตลอดทั้ง function body — รวมถึง deferred call ด้วย นี่คือวิธีมาตรฐานในการแนบ context หรือแปลง error ก่อนที่มันจะออกจาก function:
func doWork() (err error) { defer func() { if err != nil { err = fmt.Errorf("doWork: %w", err) } }() return errors.New("boom")}doWork() return errors.New("boom") แต่พอถึงตอนที่ caller เห็นค่า deferred function ได้ทำงานและ wrap มันไปแล้ว — การเรียกจะ print doWork: boom วิธีนี้ใช้ได้เฉพาะกับ named result เท่านั้น: deferred function ที่ closure บน return err ธรรมดาจะเปลี่ยนสิ่งที่ caller ได้รับไม่ได้ เพราะไม่มีตัวแปรร่วมให้มันแก้ไข
defer ใน Loop
หัวข้อที่มีชื่อว่า “defer ใน Loop”defer ทำงานตอน function return ไม่ใช่ตอนจบ loop iteration ที่มันถูกเขียน การ defer ใน for loop เป็นสาเหตุคลาสสิกของ resource leak — file, connection, หรือ lock ทุกตัวจะสะสมไว้จนกว่าทั้ง function จะจบ ไม่ใช่ทุกครั้งที่วน loop:
func leaky(n int) { for i := 0; i < n; i++ { r := openResource(i) defer r.Close() // accumulates n deferred calls, all pending until leaky returns } // if n is large, this line runs with n resources still open}ถ้า leaky เองเป็น function ที่รันนาน — สมมติว่าประมวลผลไฟล์นับพันไฟล์ใน loop — file descriptor ทุกตัวจะเปิดค้างไว้จนกว่าจะถึงจุดจบสุด และ process อาจ file descriptor หมดก่อนที่ leaky จะ return ด้วยซ้ำ วิธีแก้คือให้แต่ละ iteration มี scope ของตัวเอง ปกติผ่าน helper function เพื่อให้ defer ทำงานทุก iteration:
func tidy(n int) { for i := 0; i < n; i++ { func() { r := openResource(i) defer r.Close() // runs at the end of this anonymous function, every iteration // ... use r ... }() }}bug ที่เกี่ยวข้องกันคือ defer f.Close() ทิ้ง error ที่ Close return อย่างเงียบ ๆ สำหรับไฟล์ที่แค่อ่าน มักไม่มีปัญหา แต่สำหรับไฟล์ที่คุณ เขียน ลงไป Close อาจเป็นจุดที่ flush buffered data จริง ๆ — และ flush ที่ล้มเหลวก็คือ error จริงที่คุณเพิ่งทิ้งไป:
func saveBuggy(f *os.File, data []byte) error { defer f.Close() // any error from Close is silently discarded _, err := f.Write(data) return err}
func saveFixed(f *os.File, data []byte) (err error) { defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = cerr // only report Close's error if Write didn't already fail } }() _, err = f.Write(data) return err}Closures และ First-Class Functions
หัวข้อที่มีชื่อว่า “Closures และ First-Class Functions”Functions ใน Go เป็น first-class values — สามารถ assign ให้ตัวแปร, ส่งเป็น argument, หรือ return จาก function ได้:
// Function as a value in a variabledouble := func(n int) int { return n * 2 }fmt.Println(double(5)) // 10
// Function as a parameterapply := func(f func(int) int, x int) int { return f(x) }fmt.Println(apply(double, 3)) // 6Closure คือ function ที่ “จำ” ตัวแปรจาก enclosing scope:
func counter() func() int { n := 0 return func() int { n++ // captures n from counter return n }}
c := counter()fmt.Println(c()) // 1fmt.Println(c()) // 2 — state is preserved inside the closurefmt.Println(c()) // 3แต่ละครั้งที่เรียก counter() จะได้ n อิสระจากกัน counter สองตัวจึงไม่รบกวนกัน
Closure บน Loop Variable: ก่อนและหลัง Go 1.22
หัวข้อที่มีชื่อว่า “Closure บน Loop Variable: ก่อนและหลัง Go 1.22”Closure ที่สร้างข้างใน for loop จะ capture ตัวแปร loop เอง ไม่ใช่ snapshot ของค่ามันตอนนั้น ก่อน Go 1.22 ทุก iteration ของ for แชร์ตัวแปร ตัวเดียว ทำให้ closure ทุกตัวเห็นค่าสุดท้ายที่ตัวแปรนั้นถืออยู่ตอนที่มันถูกเรียกจริง:
var fns []func()for _, v := range []int{0, 1, 2} { fns = append(fns, func() { fmt.Println(v) })}for _, f := range fns { f()}Compile ด้วย go 1.21 ใน go.mod โปรแกรมนี้ print 2 2 2 บน Go 1.26.5 — closure ทุกตัวแชร์ v ตัวเดียวกัน ซึ่งค่าสุดท้ายหลัง loop คือ 2 วิธีแก้แบบดั้งเดิมที่ยังพบได้บ่อยใน code ที่เขียนก่อน 1.22 (และใน code ที่ AI ที่ train จาก code แบบนั้น generate ออกมา) คือ copy ตัวแปรใหม่ข้างใน loop body: v := v ก่อนใช้ v ใน closure
Go 1.22 เปลี่ยน semantics นี้: แต่ละ iteration ได้ v ของตัวเอง ทำให้ source บรรทัดเดียวกันนี้ — เปลี่ยนแค่บรรทัด go ใน go.mod เป็น 1.22 — print 0 1 2:
go 1.21 in go.mod → 2 2 2
go 1.22 in go.mod → 0 1 2ทั้งสองผลลัพธ์ด้านบนเป็น output จริงจากโปรแกรมตัวเดียวกันนี้บน Go 1.26.5 เปลี่ยนแค่บรรทัด go ใน go.mod ระหว่างการรัน playground แบบ interactive ด้านล่างใช้ toolchain สมัยใหม่เสมอ จึงจะแสดง behavior แบบหลัง 1.22 เท่านั้น go directive คือสิ่งที่ gate การเปลี่ยนแปลงนี้ต่อ module และไม่สามารถสลับได้จากภายในโปรแกรมเดียว การ copy v := v ไม่จำเป็นอีกต่อไปเมื่อ go.mod ของ module ระบุ go 1.22 ขึ้นไป — แต่คุณจะยังเห็นมันใน code เก่า และการจำรูปแบบนี้ได้จะบอกว่าผู้เขียนกำลังป้องกัน behavior เก่าอยู่
Function Value, Method Value, และ nil
หัวข้อที่มีชื่อว่า “Function Value, Method Value, และ nil”function type ก็เป็นแค่ type อีกตัวหนึ่ง — คุณประกาศตัวแปรของมันได้, เก็บใน struct field ได้, หรือส่งเป็น value ได้ zero value ของมันคือ nil และ function value เทียบได้กับ nil เท่านั้น (การเทียบ function value ที่ไม่ใช่ nil สองตัวด้วย == เป็น compile error):
type IntFunc func(int) int
var f IntFuncfmt.Println(f == nil) // true
f(3) // panics: runtime error: invalid memory address or nil pointer dereferenceนี่คือ bug รูปแบบเดียวกับ nil pointer dereference และมักพบบ่อยที่สุดกับ callback field ที่เป็น optional บน struct (OnError func(error)) ที่ caller ลืมกำหนดค่า ควร nil-check function value ก่อนเรียกใช้เสมอ เว้นแต่ contract ของ type จะรับประกันว่ามันถูกกำหนดค่าไว้แล้วเสมอ
package main
import ( "errors" "fmt")
// Multiple return valuesfunc divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil}
// Variadicfunc sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total}
// Closurefunc counter() func() int { n := 0 return func() int { n++ return n }}
// defer demofunc deferDemo() { fmt.Println("deferDemo start") defer fmt.Println("deferred 1 (runs last)") defer fmt.Println("deferred 2 (runs second)") defer fmt.Println("deferred 3 (runs first)") fmt.Println("deferDemo end")}
func main() { // Multiple returns result, err := divide(10, 3) if err != nil { fmt.Println("error:", err) } else { fmt.Printf("10 / 3 = %.4f\n", result) }
_, err = divide(5, 0) if err != nil { fmt.Println("divide by zero:", err) }
// Variadic fmt.Println("sum(1..5) =", sum(1, 2, 3, 4, 5))
// Closure counter c := counter() fmt.Println("counter:", c(), c(), c())
// defer LIFO deferDemo()} Playground ตัวที่สองครอบคลุมทุกอย่างที่เพิ่มใหม่ในหน้านี้: named result ที่ถูก defer แก้ไข, defer ใน loop, error จาก Close ที่ถูกกลืน พร้อมวิธีแก้, และ nil function value
package main
import ( "errors" "fmt")
// doWork shows a named result modified by a deferred func — a common way to// annotate an error right before it leaves the function.func doWork() (err error) { defer func() { if err != nil { err = fmt.Errorf("doWork: %w", err) } }() return errors.New("boom")}
type resource struct{ id int }
func openResource(id int) *resource { fmt.Println("open", id) return &resource{id}}
func (r *resource) Close() { fmt.Println("close", r.id) }
// leaky defers Close inside a loop — every resource stays open until leaky// itself returns, not at the end of each iteration.func leaky(n int) { for i := 0; i < n; i++ { r := openResource(i) defer r.Close() // accumulates n deferred calls } fmt.Println("all opened, none closed yet")}
type mockFile struct{ closeErr error }
func (f *mockFile) Write(p []byte) (int, error) { return len(p), nil }func (f *mockFile) Close() error { return f.closeErr }
// saveBuggy discards whatever error Close returns.func saveBuggy(f *mockFile, data []byte) error { defer f.Close() // any error from Close is silently discarded _, err := f.Write(data) return err}
// saveFixed captures Close's error into the named result if nothing else failed.func saveFixed(f *mockFile, data []byte) (err error) { defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = cerr } }() _, err = f.Write(data) return err}
type IntFunc func(int) int
func sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total}
func main() { // Named result modified by defer. fmt.Println(doWork())
// defer inside a loop: resources pile up until the function returns. leaky(3)
// defer f.Close() silently swallowing a write/flush error, and the fix. f := &mockFile{closeErr: errors.New("disk full")} fmt.Println("saveBuggy:", saveBuggy(f, []byte("data"))) fmt.Println("saveFixed:", saveFixed(f, []byte("data")))
// A nil function value compares equal to nil, and calling it panics. var nilFn IntFunc fmt.Println("nil func == nil:", nilFn == nil)
// A nil slice spread into a variadic parameter behaves like an empty one. var nilSlice []int fmt.Println("sum(nilSlice...):", sum(nilSlice...))} ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| multiple return values | explicit error handling ไม่มี hidden exception | caller ต้องตรวจทุก error — verbose กว่า try/catch |
| named return values | documentation ใน signature, bare return ได้, defer แก้ผลลัพธ์ได้ | bare return ใน function ยาวทำให้อ่านสับสน |
defer | cleanup code อยู่ใกล้กับ resource acquisition | arguments evaluate ทันที, LIFO order อาจ surprise, และทำงานตอน function exit ไม่ใช่ตอนจบ loop iteration |
| closures | encapsulate state ได้กระชับ | captured variable มักถูก allocate บน heap (escape analysis) |
| pass-by-value ทุกที่ | ไม่มี aliasing แอบแฝงผ่าน function boundary โดย default | ส่ง struct ใหญ่แบบ value จะ copy ทั้งหมด; slice หรือ map argument ยังแก้ไขสิ่งที่มันชี้ไปได้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
deferช้า” —deferมี overhead น้อยมาก (nanosecond range) ใช้ได้สบายใน production code สำหรับ cleanup - “multiple return values ทำให้ performance แย่ลง” — Go return multiple values ผ่าน register หรือ stack โดยตรง ไม่มี heap allocation; แทบไม่มีผลต่อ performance
- “closure ทุกตัว escape ไปที่ heap” — compiler ทำ Escape Analysis และอาจ allocate closure บน stack ถ้าไม่ออกนอก scope
- “function ใน Go ไม่ใช่ first-class” — function ใน Go เป็น first-class value สามารถ assign, ส่งเป็น argument, หรือ return จาก function อื่นได้
- “
deferใน loop cleanup ทุก iteration” — ไม่จริง deferred call ทุกตัวค้างไว้จนกว่า function ที่ครอบอยู่ จะ return ไม่ใช่ตอนจบ loop
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ function ที่ generate มาใช้ defer, closure, หรือ function value ให้ตรวจสิ่งเหล่านี้ก่อนเชื่อโค้ด:
deferอยู่ข้างในforloop ที่วนได้มากกว่าสองสามรอบหรือเปล่า? deferred call ของแต่ละ iteration จะค้างไว้จนกว่า function จะ return ไม่ใช่ตอนจบ loopdefer f.Close()ตามหลังWrite/Flushแล้วทิ้ง error ที่Closereturn หรือเปล่า? ดูว่ามี named result ให้ deferred function แนบ error นั้นเข้าไปได้ไหม- closure capture loop variable หรือเปล่า และถ้าใช่
go.modของ module ระบุgo 1.22ขึ้นไปหรือยัง? ต่ำกว่านั้น closure ทุกตัวที่สร้างใน loop จะแชร์ตัวแปรเดียวกัน - function-typed value (field หรือ parameter) ถูกเรียกโดยไม่เช็ก
nilก่อนหรือเปล่า? - โค้ดเทียบ function value สองตัวตรง ๆ ด้วย
==(ไม่ใช่เทียบกับnil) หรือเปล่า? นั่นคือ compile error — function type เทียบได้แค่กับ nil เท่านั้น
🐛 หา bug ในโค้ดนี้
func leaky(n int) { for i := 0; i < n; i++ { r := openResource(i) defer r.Close() } fmt.Println("all opened, none closed yet")}เฉลย
defer กำหนดให้ r.Close() ทำงานตอน leaky return — ไม่ใช่ตอนจบแต่ละ loop iteration เมื่อ n มีค่ามาก บรรทัดนี้จะรันตอนที่ resource ทั้ง n ตัวยังเปิดอยู่ และใน program ที่รันนานอาจทำให้ file descriptor หรือ connection หมดก่อนที่ leaky จะ return ด้วยซ้ำ แก้ได้โดยให้แต่ละ iteration มี scope ของตัวเอง เพื่อให้ deferred call ทำงานทุก iteration ปกติผ่าน helper function เล็ก ๆ: func() { r := openResource(i); defer r.Close(); /* use r */ }() เรียกครั้งเดียวต่อ loop iteration