ข้ามไปยังเนื้อหา

Functions

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

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 สุดท้าย

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 ที่ควรเลี่ยง

ใช้ ...T เพื่อรับ arguments จำนวนไม่จำกัดของ type เดียวกัน:

func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // individual args
sum(1, 2, 3, 4, 5) // any number of args works
sum([]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
defer สามตัวรันย้อนลำดับกับตอนที่ถูก push เข้าไป

Arguments ของ deferred call ถูก evaluate ทันที ณ จุดที่ defer statement ถูก execute ไม่ใช่ตอน call จริง:

i := 0
defer fmt.Println(i) // captures i's value (0) right now
i = 100
// when the function returns, this prints "0", not "100"

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 ทำงานตอน 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
}

Functions ใน Go เป็น first-class values — สามารถ assign ให้ตัวแปร, ส่งเป็น argument, หรือ return จาก function ได้:

// Function as a value in a variable
double := func(n int) int { return n * 2 }
fmt.Println(double(5)) // 10
// Function as a parameter
apply := func(f func(int) int, x int) int { return f(x) }
fmt.Println(apply(double, 3)) // 6

Closure คือ function ที่ “จำ” ตัวแปรจาก enclosing scope:

func counter() func() int {
n := 0
return func() int {
n++ // captures n from counter
return n
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2 — state is preserved inside the closure
fmt.Println(c()) // 3

แต่ละครั้งที่เรียก counter() จะได้ n อิสระจากกัน counter สองตัวจึงไม่รบกวนกัน

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 type ก็เป็นแค่ type อีกตัวหนึ่ง — คุณประกาศตัวแปรของมันได้, เก็บใน struct field ได้, หรือส่งเป็น value ได้ zero value ของมันคือ nil และ function value เทียบได้กับ nil เท่านั้น (การเทียบ function value ที่ไม่ใช่ nil สองตัวด้วย == เป็น compile error):

type IntFunc func(int) int
var f IntFunc
fmt.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 values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Variadic
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Closure
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
// defer demo
func 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 valuesexplicit error handling ไม่มี hidden exceptioncaller ต้องตรวจทุก error — verbose กว่า try/catch
named return valuesdocumentation ใน signature, bare return ได้, defer แก้ผลลัพธ์ได้bare return ใน function ยาวทำให้อ่านสับสน
defercleanup code อยู่ใกล้กับ resource acquisitionarguments evaluate ทันที, LIFO order อาจ surprise, และทำงานตอน function exit ไม่ใช่ตอนจบ loop iteration
closuresencapsulate 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

เมื่อ function ที่ generate มาใช้ defer, closure, หรือ function value ให้ตรวจสิ่งเหล่านี้ก่อนเชื่อโค้ด:

  • defer อยู่ข้างใน for loop ที่วนได้มากกว่าสองสามรอบหรือเปล่า? deferred call ของแต่ละ iteration จะค้างไว้จนกว่า function จะ return ไม่ใช่ตอนจบ loop
  • defer f.Close() ตามหลัง Write/Flush แล้วทิ้ง error ที่ Close return หรือเปล่า? ดูว่ามี 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

ใน Go idiomatic pattern ใดที่ function ใช้เมื่ออาจล้มเหลว?
ถ้าประกาศ defer สามครั้งในลำดับ A, B, C จะ execute ในลำดับใด?
Closure ใน Go จำตัวแปรจาก enclosing scope อย่างไร?
จะส่ง slice เข้า variadic function เป็น arguments แยกกันได้อย่างไร?
fmt.Println(doWork()) print อะไร?
func doWork() (err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("doWork: %w", err)
        }
    }()
    return errors.New("boom")
}
ตอนที่ print "done opening" มี resource กี่ตัวที่ยังเปิดอยู่?
for i := 0; i < 3; i++ {
    r := openResource(i)
    defer r.Close()
}
fmt.Println("done opening")