โหมดมืด
บทที่ 3 — Functions
หลังจบบท คุณจะ:
- เขียน function ตั้งแต่พื้นฐาน (basic) ถึงขั้นสูง (advanced)
- ใช้ multiple return (คืนค่าหลายค่า), named return (คืนค่าแบบตั้งชื่อ)
- เข้าใจ variadic (รับ argument ไม่จำกัดจำนวน) + closure (ฟังก์ชันที่จำตัวแปรรอบตัวได้) + function as value (มองฟังก์ชันเป็นค่า)
- รู้จัก method (เมธอด — ดูพอเข้าใจรอบนี้ รายละเอียดเต็มอยู่บท 5)
1. Function Basic
ฟังก์ชันคือหน่วยพื้นฐานที่รวมโค้ดไว้ใช้ซ้ำ Go ใช้คีย์เวิร์ด func ตามด้วยชื่อ, พารามิเตอร์ (ระบุ type ไว้หลังชื่อ), และ type ที่คืนค่า — ถ้าพารามิเตอร์หลายตัว type เดียวกัน ย่อรวมได้ และถ้าไม่คืนค่าก็ไม่ต้องระบุ return type:
go
func add(a int, b int) int {
return a + b
}
// Call
sum := add(3, 5) // 8text
ส่วนประกอบ:
func name(params) returnType { body }Same-Type Parameters
go
// Shorthand
func add(a, b int) int {
return a + b
}
// Same as
func add(a int, b int) int {
return a + b
}No Return
go
func greet(name string) {
fmt.Println("Hello,", name)
}→ ไม่ระบุ returnType
2. Multiple Return Values
⭐ Feature เด่นของ Go — function return หลายค่าได้ (ต่างจาก C#: C# ก็คืนหลายค่าได้เหมือนกันด้วย ValueTuple เช่น (int, string) หรือ out parameter — แต่ใน Go multiple return คู่กับ error คืออิดิออม (idiom) หลักของทั้งภาษา ไม่ใช่แค่ทางเลือกแบบ C#)
go
import "errors" // ต้อง import เพื่อใช้ errors.New
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Call
result, err := divide(10, 3)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)Common Pattern: Value + Error
go
file, err := os.Open("data.txt")
if err != nil {
return err
}
user, err := repo.FindById(id)
if err != nil {
return err
}→ Go = "error เป็น return value" — ไม่ใช่ exception (กลไกโยน-ดักข้อผิดพลาดของภาษาอื่น เช่น try-catch ของ Java/Python/C#)
→ สำหรับ dev .NET: นี่คือการปรับ mindset ที่ใหญ่สุดตอนย้ายมา Go — เลิกใช้ throw/try-catch สำหรับ flow ปกติ ทุกฟังก์ชันที่พลาดได้จะคืน error เป็นค่าสุดท้ายเสมอ ต้องเช็ค if err != nil เองทุกจุดที่เรียก ไม่มีใครดักให้อัตโนมัติเหมือน exception
Ignore with _
go
_, err := fn() // ignore value
result, _ := fn() // ignore error (ระวัง!)3. Named Return Values
go
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("division by zero")
return // "naked return" (รีเทิร์นเปล่า — `return` ไม่ระบุค่า Go ใช้ค่าใน named return variable ให้)
}
result = a / b
return // = return result, err
}Pros / Cons
✅ Documentation — ชื่อบอก meaning
✅ Auto zero value
❌ "Naked return" อ่านยากใน function ยาว — confusing
❌ Easy to forget assigning before return
Recommendation: ใช้กับ small function เท่านั้น — ส่วนใหญ่ explicit return ดีกว่า
4. Variadic Function
รับ argument จำนวนไม่จำกัด:
go
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Call
sum() // 0
sum(1) // 1
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
// Pass slice
nums := []int{1, 2, 3}
sum(nums...) // spread operator (ตัวกระจาย — ใส่ `...` ต่อท้าย slice เพื่อ "แตก" สมาชิกออกเป็น argument หลายตัวให้ variadic function รับ)Built-in Examples
go
fmt.Println("a", "b", "c")
append(slice, 1, 2, 3)
strings.Join([]string{"a", "b"}, ",")Mixed Parameters
go
// any = interface{} (อินเทอร์เฟซว่าง — ชนิดที่รับค่าอะไรก็ได้ คล้าย Object ใน Java)
// ตั้งแต่ Go 1.18 นิยมเขียน any แทน interface{} — บท 6 อธิบายเต็ม
func logf(prefix string, args ...any) {
fmt.Print(prefix, ": ")
fmt.Println(args...)
}
logf("INFO", "User", 42, "logged in")→ Variadic ต้องเป็น parameter สุดท้าย
5. ฟังก์ชันเป็นค่า (Function as Value)
ใน Go ฟังก์ชันเป็น "first-class value" (ค่าระดับเดียวกับข้อมูลทั่วไป เช่น int หรือ string) หมายความว่ามันถูกปฏิบัติเหมือนค่าทั่วไป:
- เก็บใส่ตัวแปรได้
- ส่งเป็น argument ให้ฟังก์ชันอื่นได้
- ให้ฟังก์ชันคืนค่าเป็นฟังก์ชันได้
- ใส่ใน slice/map ได้
ความสามารถนี้คือรากฐานของการเขียนโค้ดแบบ functional และ callback:
go
// ตัวอย่างด้านล่างเป็นการอธิบาย concept เท่านั้น ไม่ใช่ไฟล์เดียวรันได้ทั้งบล็อก
// apply และ multiplier เป็น "func ระดับ package" (ประกาศนอก main — ห้ามซ้อน func มีชื่อไว้ใน func อื่น
// Go อนุญาตแค่ func ไม่มีชื่อ/anonymous ซ้อนได้เท่านั้น) ส่วนที่เหลือ (add, op, result, double, triple)
// ใช้ `:=` ได้เพราะอยู่ใน func main() เท่านั้น (ตามกฎบท 1 ว่า `:=` ใช้ได้เฉพาะในฟังก์ชัน)
// --- ระดับ package (นอก main) ---
func apply(fn func(int, int) int, a, b int) int {
return fn(a, b)
}
func multiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
// --- ใน func main() ---
// Assign
add := func(a, b int) int { return a + b } // นิยาม add ก่อน
var op func(int, int) int = add
op = func(a, b int) int { return a - b } // anonymous function
result := op(10, 3) // 7
// Pass as argument
apply(add, 3, 5) // 8
apply(func(a, b int) int { return a * b }, 3, 5) // 15
// Return from function
double := multiplier(2)
double(5) // 10
double(10) // 20
triple := multiplier(3)
triple(5) // 156. Closure — Capture Variable
Closure (โคลเชอร์) = anonymous function (ฟังก์ชันไม่มีชื่อ) ที่ "จำ" ตัวแปรนอก scope (ขอบเขตของตัวมัน) ได้ — เหมือน lambda ของ C# ที่ capture ตัวแปรรอบข้างได้เช่นกัน (แต่ระวัง loop variable capture ด้านล่าง: Go < 1.22 พังแบบเดียวกับที่ C# foreach เคยพังก่อนแก้ในเวอร์ชันเก่า):
go
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
c1 := counter()
c1() // 1
c1() // 2
c1() // 3
c2 := counter() // separate count
c2() // 1
c2() // 2count อยู่ใน counter() แต่ inner function "จำ" และ modify ได้ — แม้ outer return ไปแล้ว
Use Case
go
// Filter
func makeFilter(predicate func(int) bool) func([]int) []int {
return func(nums []int) []int {
var result []int
for _, n := range nums {
if predicate(n) {
result = append(result, n)
}
}
return result
}
}
positive := makeFilter(func(n int) bool { return n > 0 })
even := makeFilter(func(n int) bool { return n%2 == 0 })
positive([]int{-1, 2, -3, 4}) // [2 4]
even([]int{1, 2, 3, 4, 5}) // [2 4]Closure Gotcha — Loop Variable
go
// Go < 1.22 problem (Go 1.22+ fixed)
funcs := []func(){}
for i := 0; i < 3; i++ {
funcs = append(funcs, func() { fmt.Println(i) })
}
// Go < 1.22: prints 3 3 3 (all share same i)
// Go >= 1.22: prints 0 1 2 (per-iteration i)7. Anonymous Function
"Anonymous function" คือฟังก์ชันที่ไม่มีชื่อ — เขียนตรงจุดที่ใช้เลย จะเรียกทันที (IIFE), เก็บใส่ตัวแปร, หรือส่งเป็น argument ก็ได้ พบบ่อยมากเวลาใช้กับ goroutine, defer และ callback:
go
// Immediately Invoked
func() {
fmt.Println("anonymous")
}() // () ← call immediately
// With argument
func(name string) {
fmt.Println("Hi", name)
}("Anna")
// Assign + call later
greet := func(name string) {
fmt.Println("Hi", name)
}
greet("Anna")
greet("Ben")ใช้กับ:
- Goroutine launch
- defer
- Callback
8. Recursive Function
ฟังก์ชัน recursive (เรียกซ้ำ/เรียกตัวเอง) คือฟังก์ชันที่เรียกตัวเอง — เหมาะกับปัญหาที่แตกเป็นปัญหาย่อยแบบเดียวกัน เช่น factorial (แฟกทอเรียล n!).
ข้อควรระวังใน Go คือ ถ้า recursion ลึกมากอาจเกิด stack overflow (กองซ้อนการเรียกฟังก์ชันล้น จนโปรแกรมพัง) เหตุผลคือ Go ไม่มี tail-call optimization (การปรับโค้ดให้เรียกซ้ำท้ายฟังก์ชันไม่กิน stack เพิ่ม) — ถ้าเรียกตัวเองลึกมากทุก frame จะถูกเก็บสะสมไว้จริง ๆ
อย่างไรก็ตาม Go ช่วยผ่อนไว้บางส่วน: stack ของแต่ละ goroutine เริ่มเล็กแล้วขยายได้อัตโนมัติ (dynamic stack growth) ทำให้ recursion ระดับลึกปานกลางปลอดภัยตามปกติ แต่ถ้าลึกมาก ๆ จริง (ระดับหลักล้าน frame) ก็ยังอาจ stack overflow ได้อยู่ดี ดังนั้นงานที่ต้องวนซ้ำลึกมาก บางครั้งเขียนแบบ loop จะปลอดภัยกว่า:
go
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
factorial(5) // 120⚠️ ระวัง stack overflow ที่ recursion ลึกมาก — Go ไม่มี tail-call optimization
go
// Better: iterative
func factorial(n int) int {
result := 1
for i := 2; i <= n; i++ {
result *= i
}
return result
}9. Pass by Value (Always)
Go = always pass by value — แต่บางอย่างที่ "value" คือ reference อยู่แล้ว
⚠️ ระวัง! false friend กับ C#: ใน C# ส่ง class object เข้าฟังก์ชันแล้วแก้ field ข้างในได้เลย (เพราะ class = reference type) แต่ Go struct ส่งเข้าฟังก์ชันแล้วได้แค่ "สำเนา" แก้เท่าไหร่ก็ไม่ติดกลับไปที่ตัวเดิม — พูดง่าย ๆ Go struct ≈ C# struct ไม่ใช่ C# class ถ้าอยากแก้ของจริงต้องส่ง pointer (*User) แบบด้านล่าง
Primitive — Copy
go
func increment(n int) {
n++ // local copy
}
x := 5
increment(x)
fmt.Println(x) // 5 (unchanged)Struct — Copy
go
type User struct {
Name string
Age int
}
func updateUser(u User) {
u.Name = "Modified" // local copy
}
user := User{Name: "Anna"}
updateUser(user)
fmt.Println(user.Name) // Anna (unchanged)Pointer — "By reference" (effectively)
go
func updateUser(u *User) {
u.Name = "Modified"
}
user := User{Name: "Anna"}
updateUser(&user)
fmt.Println(user.Name) // ModifiedSlice, Map, Channel — "By reference" (technically by value, but value = pointer)
go
func modifySlice(s []int) {
s[0] = 999 // modify underlying array (อาเรย์ภายใน — เดี๋ยวบท 4 อธิบายเต็ม)
}
nums := []int{1, 2, 3}
modifySlice(nums)
fmt.Println(nums) // [999 2 3]→ Slice ภายในคือกลุ่ม 3 ข้อมูล: pointer ที่ชี้ไป array จริง + len (จำนวนที่ใช้อยู่) + cap (จำนวนที่จองไว้). ตอนส่งเข้าฟังก์ชัน Go ก๊อปเฉพาะ 3 ข้อมูลนี้ แต่ pointer ยังชี้ array ก้อนเดิม.
→ รายละเอียดเต็มของ slice + backing array (อาเรย์ภายในของ slice) เดี๋ยวบท 4 อธิบาย — ตอนนี้แค่รู้ว่า "slice ส่งเข้าฟังก์ชันแล้วแก้ค่าจริงได้" ก็พอ
10. Defer + Function — Common Pattern
แพตเทิร์นที่ใช้บ่อยที่สุดของ defer คือเก็บกวาดทรัพยากรหลังเปิด เช่นเปิดไฟล์แล้ว defer file.Close() ทันที — มั่นใจว่าปิดเสมอไม่ว่าฟังก์ชันจะ return ทางไหน:
go
func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close() // cleanup, no matter how function exits
return doWork(file)
}Defer in Loop (อันตราย)
go
// ❌ Resource leak — defer accumulate (และอย่าใช้ _ ละเลย error ในโค้ดจริง)
for _, path := range files {
file, _ := os.Open(path) // ⚠️ ตัวอย่างละ error ไว้เพื่อโฟกัสบั๊ก defer
defer file.Close() // ทุกไฟล์ defer — close ตอน function จบ
// ...
}
// 1000 files → 1000 open handles!
// ✅ Use function (พร้อม error handling เต็ม)
for _, path := range files {
func() {
file, err := os.Open(path)
if err != nil {
log.Printf("open %s: %v", path, err)
return
}
defer file.Close()
// ...
}()
}11. Method (Preview)
นอกจากฟังก์ชันลอย ๆ Go ยังมี "method" คือฟังก์ชันที่ผูกกับ type หนึ่ง (เขียน receiver ไว้หน้าชื่อ เช่น func (r Rectangle) Area()) ทำให้เรียกแบบ r.Area() ได้ — ตรงนี้แค่เกริ่นให้เห็นภาพ รายละเอียดเต็มอยู่บทที่ 5:
Method = function ที่ผูกกับ type:
go
type Rectangle struct {
Width, Height float64
}
// Method on Rectangle
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Call
r := Rectangle{Width: 3, Height: 4}
r.Area() // 12→ รายละเอียด: บทที่ 5 (Structs + Methods)
12. Function Type
เราตั้งชื่อให้ "รูปแบบของฟังก์ชัน" (signature) ได้ด้วย type เช่น type BinaryOp func(int, int) int — ช่วยให้โค้ดอ่านง่ายและสื่อเจตนา ฟังก์ชันใดก็ตามที่มี signature ตรงกันก็ส่งเข้าไปใช้ได้ เจอบ่อยใน strategy pattern, callback และ HTTP handler:
go
// Define a function type
type BinaryOp func(int, int) int
// Use as parameter type
func calculate(a, b int, op BinaryOp) int {
return op(a, b)
}
// Functions ที่ match signature ก็ใช้ได้
add := func(a, b int) int { return a + b }
mul := func(a, b int) int { return a * b }
calculate(3, 5, add) // 8
calculate(3, 5, mul) // 15ใช้สำหรับ:
- Strategy pattern
- Callback
- HTTP handler
go
// http.ResponseWriter + *http.Request มาจาก package net/http ของ stdlib
// ใช้รับ-ตอบ HTTP request — บท 11 (Standard Library) และบท 13 อธิบายเต็ม
type Handler func(http.ResponseWriter, *http.Request)
func middleware(next Handler) Handler {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request: %s", r.URL.Path)
next(w, r)
}
}💡 ในงานจริง stdlib มี
http.HandlerFunc(ที่ signature เดียวกันนี้) ให้แล้ว — ตัวอย่างด้านบนเขียนHandlerเองเพื่อสอนเรื่อง "function type" เท่านั้น
13. Recover from Panic
panic คือการที่โปรแกรมเจอสถานการณ์ร้ายแรงแล้วหยุดทำงาน ส่วน recover (เรียกใน defer) ช่วย "ดักจับ" panic นั้นไม่ให้โปรแกรมล่มทั้งตัว — เป็นระบบคล้าย exception ของภาษาอื่น แต่ Go แนะนำให้ใช้น้อย ใช้ error สำหรับ business logic ปกติ เก็บ panic ไว้สำหรับกรณีที่กู้ไม่ได้จริง ๆ:
go
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil // panic if b == 0
}
result, err := safeDivide(10, 0)
// result = 0, err = "recovered: integer divide by zero"⚠️ panic + recover คือระบบจัดการ error ร้ายแรงของ Go (คล้าย try-catch ของภาษาอื่น) — แต่ Go แนะนำให้ใช้น้อย ใช้ error ปกติแทน
→ ใช้ error สำหรับ business logic, panic สำหรับ unrecoverable (กรณีกู้ไม่ได้จริง ๆ)
14. init() Function
go
package mypackage
import "fmt"
var config Config
func init() {
config = loadConfig()
fmt.Println("Package initialized")
}init() ถูก auto-call ก่อน main() — ก่อน package ใด ๆ จะใช้ (ต่างจาก C#: คล้าย static constructor ที่รันครั้งเดียวก่อนใช้ class — แต่ Go มีได้หลายตัวต่อ package และรันตามลำดับ import)
- มีได้หลาย
init()ใน package เดียว - ใช้สำหรับ register driver (ลงทะเบียน driver — เช่น
database/sqldriver ตอน startup), validate config (ตรวจสอบไฟล์ config ก่อนเริ่มทำงาน)
⚠️ ใช้น้อย — ส่วนใหญ่ explicit constructor / dependency injection ดีกว่า
15. Documentation Comments
Go ให้ความสำคัญกับเอกสารในโค้ด — คอมเมนต์ที่เขียนเหนือ function/package (ขึ้นต้นด้วยชื่อสิ่งนั้น) จะถูกดึงเป็นเอกสารอัตโนมัติด้วย go doc และแสดงบน pkg.go.dev โดยไม่ต้องเขียนเอกสารแยก:
go
// Package math provides basic math functions.
package math
// Add returns the sum of a and b.
//
// Example:
//
// sum := Add(3, 5) // 8
func Add(a, b int) int {
return a + b
}bash
go doc math.Add→ Comment ก่อน function = doc comment (gofmt มาตรฐาน)
16. ตัวอย่างเต็ม — เครื่องคิดเลข (Function as Value)
รวมแนวคิดทั้งบท — function type, function as value, closure, map เก็บฟังก์ชัน — ในโปรแกรมที่รันได้และอ่านง่าย:
go
package main
import (
"errors"
"fmt"
)
// ชนิดของ operation (function type)
type MathOp func(float64, float64) (float64, error)
// ลงทะเบียน operations ใน map
func makeOperations() map[string]MathOp {
return map[string]MathOp{
"+": func(a, b float64) (float64, error) { return a + b, nil },
"-": func(a, b float64) (float64, error) { return a - b, nil },
"*": func(a, b float64) (float64, error) { return a * b, nil },
"/": func(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("หารด้วยศูนย์ไม่ได้")
}
return a / b, nil
},
}
}
// calculator รับ op เป็น function — ใช้ function as argument
func calculate(a, b float64, op MathOp) (float64, error) {
return op(a, b)
}
func main() {
ops := makeOperations()
pairs := [][3]float64{
{10, 3, 0}, // จะเลือก op ตามลูป
}
_ = pairs // ตัวอย่างเรียกตรง ๆ
for symbol, op := range ops {
result, err := calculate(10, 3, op)
if err != nil {
fmt.Printf("10 %s 3 = error: %v\n", symbol, err)
} else {
fmt.Printf("10 %s 3 = %.4g\n", symbol, result)
}
}
// closure: สร้าง "multiplier" ที่จำ factor ไว้
double := func(x float64) float64 { return x * 2 }
triple := func(x float64) float64 { return x * 3 }
fmt.Println("double(5) =", double(5))
fmt.Println("triple(5) =", triple(5))
}→ Demonstrate: function type, function as value, closure, map เก็บฟังก์ชัน, error handling
⚠️ ตัวอย่างขั้นสูง — Mini HTTP Router (ข้ามได้ในรอบแรก)
⚠️ ตัวอย่างนี้ กระโดดไปใช้ของบทถัดไป:
struct+method(บท 5) และnet/http(บท 11/13). ถ้ายังไม่คุ้น ข้ามไปก่อนได้เลย — ค่อยกลับมาอ่านหลังเรียนบท 5
go
package main
import (
"fmt"
"log"
"net/http"
)
type Handler func(http.ResponseWriter, *http.Request)
type Router struct {
routes map[string]Handler
}
func NewRouter() *Router {
return &Router{routes: make(map[string]Handler)}
}
func (r *Router) Handle(path string, handler Handler) {
r.routes[path] = handler
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
handler, ok := r.routes[req.URL.Path]
if !ok {
http.NotFound(w, req)
return
}
handler(w, req)
}
// Middleware
func withLogging(next Handler) Handler {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("%s %s\n", r.Method, r.URL.Path)
next(w, r)
}
}
func main() {
r := NewRouter()
r.Handle("/", withLogging(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Welcome!")
}))
r.Handle("/hello", withLogging(func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "stranger"
}
fmt.Fprintf(w, "Hello, %s!\n", name)
}))
// โค้ดจริงต้องเช็ค error ของ ListenAndServe ด้วย — port ชนกันจะเงียบ
log.Fatal(http.ListenAndServe(":8080", r))
}→ Demonstrate: function as value, closure, method, type definition
17. Common Patterns
Option Pattern (Functional Options)
💡 ตัวอย่างนี้ใช้
time.Duration(ชนิดเวลาของ Go —30 * time.Second= 30 วินาที) ดูเต็ม ๆ บท 11
go
type Server struct {
addr string
timeout time.Duration
tls bool
}
type Option func(*Server)
func WithAddr(addr string) Option {
return func(s *Server) { s.addr = addr }
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithTLS() Option {
return func(s *Server) { s.tls = true }
}
func NewServer(opts ...Option) *Server {
s := &Server{
addr: ":8080", // defaults
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(
WithAddr(":9000"),
WithTimeout(60*time.Second),
WithTLS(),
)→ Pattern หลักของ Go สำหรับ configurable constructor — ดีกว่า builder pattern ของ Java
Functional Helpers (use closure)
go
// Map
func Map(s []int, fn func(int) int) []int {
result := make([]int, len(s))
for i, v := range s {
result[i] = fn(v)
}
return result
}
// Filter
func Filter(s []int, fn func(int) bool) []int {
var result []int
for _, v := range s {
if fn(v) {
result = append(result, v)
}
}
return result
}
doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
positive := Filter([]int{-1, 2, -3, 4}, func(n int) bool { return n > 0 })→ Go 1.21+ มี slices package ใน stdlib ที่ให้ helper พื้นฐาน (Contains, Sort, Index, Clone, ฯลฯ) — แต่ Map/Filter/Reduce แบบ functional ยังไม่อยู่ใน stdlib (ทีม Go มีข้อเสนอ — proposal — จะเพิ่มมานานแต่ยังไม่ผ่าน) ถ้าจำเป็นให้เขียนเอง หรือใช้ไลบรารีนอก เช่น samber/lo
📦 การใช้ไลบรารีนอก (เช่น
samber/lo) ต้องผ่านgo getที่ใช้ Go Modules — รันคำสั่งgo get github.com/samber/loแล้ว import ในโค้ด ถ้ายังไม่ได้อ่าน บท 9 — Packages + Modules ข้ามไปก่อน เขียน loop เองพอ
18. ⚠️ Common Pitfalls
ตารางนี้รวมข้อผิดพลาดเรื่องฟังก์ชันที่มือใหม่มักพลาด คู่กับวิธีที่ถูกต้อง — โดยเฉพาะ defer ใน loop ยาว, การสับสน & กับ * และความเข้าใจผิดว่า slice ถูกก๊อปเมื่อส่งเข้าฟังก์ชัน:
| ❌ | ✅ |
|---|---|
| Forget return (ลืม return) | compile error |
Ignore error with _ (กลืน error) | handle properly (จัดการให้เรียบร้อย) |
defer in long loop (defer ใน loop ยาว) | inner function or explicit close (ใช้ฟังก์ชันย่อยห่อ หรือปิดเอง) |
Multiple _ in return — confusing (มี _ หลายตัว อ่านยาก) | use named return (ใช้ named return) |
Mix up & (address) and * (deref) | & get pointer, * get value |
| Modify slice in function thinking it copy (คิดว่า slice ถูกก๊อป) | slice = reference under header (ภายในแชร์ array เดิม) |
| Long function (ฟังก์ชันยาวเกินไป) | extract helper functions (แยกฟังก์ชันย่อย) |
| Too many parameters (พารามิเตอร์เยอะเกิน) | use struct or option pattern (รวมเป็น struct หรือใช้ option pattern) |
19. Best Practices
สรุปแนวทางเขียนฟังก์ชันที่ดีใน Go — ให้ฟังก์ชันสั้น ทำหน้าที่เดียว คืน error อย่างชัดเจน ใช้ closure เมื่อต้องการเก็บ state และใช้ functional options เมื่อ constructor มีตัวเลือกเยอะ:
text
✅ Short function (< 50 line ideal)
✅ Single responsibility
✅ Return error explicitly
✅ Use closure for stateful behavior
✅ Function type for callback
✅ Functional options for configurable constructor
✅ Defer for cleanup
✅ Document exported function (PascalCase)20. Checkpoint
🛠️ Checkpoint 3.1 — Math Library (ไลบรารีคณิตศาสตร์)
เขียน function:
Add(a, b),Sub,Mul,Div(คืนค่า value + error เมื่อหารด้วย 0)Sum(nums ...int) intAvg(nums ...float64) (float64, error)(หาค่าเฉลี่ย)- ทดสอบทุก function
🛠️ Checkpoint 3.2 — Counter Closure (ตัวนับด้วย closure)
ทำ:
NewCounter()คืนค่าเป็น function ที่นับ +1 ทุกครั้งที่เรียกNewLimitedCounter(max)หยุดเมื่อถึง max แล้วคืน error- ทดสอบว่าตัวนับแต่ละตัวแยก state (สถานะ/ค่าที่จำไว้) กันได้
🛠️ Checkpoint 3.3 — Functional Helpers (ฟังก์ชันช่วยสไตล์ functional)
เขียน:
Map([]int, func(int) int) []int(แปลงทุกตัว)Filter([]int, func(int) bool) []int(กรองเอาเฉพาะที่ผ่านเงื่อนไข)Reduce([]int, func(acc, n int) int, initial int) int(ยุบรวมเป็นค่าเดียว)- ลองใช้ทดสอบ: คูณสอง, เอาเฉพาะเลขคู่, หาผลรวม
🛠️ Checkpoint 3.4 — Option Pattern (รูปแบบส่งตัวเลือกผ่านฟังก์ชัน)
สร้าง Logger struct:
- WithLevel(level) — ตั้งระดับ log
- WithOutput(writer) — ตั้งปลายทางที่จะเขียน
- WithPrefix(prefix) — ตั้งคำนำหน้าข้อความ
- Constructor (ฟังก์ชันสร้าง object):
NewLogger(opts ...Option) *Logger
🛠️ Checkpoint 3.5 — Recover (กู้จาก panic)
เขียน function ที่ panic + recover:
- คืน error แทนที่จะ panic
- ทดสอบว่าผู้เรียก (caller) ได้รับ error ตามปกติ
21. สรุปบท
✅ Function: func name(params) returnType { body }
✅ Multiple return — Go feature เด่น: return value, error
✅ Named return — readable แต่ระวังใน function ยาว
✅ Naked return — return ไม่ระบุค่า (ใช้ค่าใน named return variable) ระวังใน function ยาว
✅ Variadic fn(args ...int) — รับ argument ไม่จำกัด
✅ First-class function — assign, pass, return ได้
✅ Closure — function จำตัวแปรนอก scope
✅ Pass by value always — แต่ slice/map = reference under
✅ Defer สำหรับ cleanup — LIFO, ระวัง defer ใน loop
✅ Functional Options — pattern สำหรับ configurable constructor
✅ Document exported function — gofmt + go doc