เรียน Go ภาษาง่าย 📘

🧩

เจาะลึก Functional Programming ใน Go ฉบับภาษาง่าย

Map/Filter/Reduce, closure, curry ด้วย generics

9 บท
14 ข้อสอบ
1. คิดแบบ functional คืออะไร?

แบบเดิม (imperative): สั่งทีละขั้น "ทำนี่ก่อน แล้วทำนั่น"

go
total := 0
for _, n := range nums {
    if n%2 == 0 {
        total += n*n
    }
}

แบบ functional: บอกว่า "เอาของมาผ่านสายพานทีละเครื่อง"

go
total := Sum(Map(Filter(nums, IsEven), Square))

ข้อดี: อ่านเป็นประโยค เทสต์ทีละเครื่องง่าย ไม่ต้องจำว่า total โดนแก้ตรงไหนบ้าง

Go ทำได้ ~70% ของ functional แท้: ไม่มี map/filter ใน std โดยตรง (ต้องเขียนเองหรือใช้ slices + generic) แต่ไอเดียใช้ได้ครบ

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


2. ฟังก์ชันเป็นของ: เก็บ ส่ง คืน ได้

ใน Go ฟังก์ชันเก็บในตัวแปรได้เหมือนเลข:

go
add := func(a, b int) int { return a + b }
fmt.Println(add(2, 3)) // 5

// ส่งฟังก์ชันเข้าฟังก์ชัน
func Apply(f func(int) int, v int) int { return f(v) }
fmt.Println(Apply(func(x int) int { return x*2 }, 5)) // 10

// คืนฟังก์ชันออกมา
func Maker() func() int {
    n := 0
    return func() int { n++; return n }
}
c := Maker()
fmt.Println(c(), c()) // 1 2

type ตั้งชื่อให้ฟังก์ชันได้ อ่านง่ายขึ้น:

go
type IntFormatter func(int) string
✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


3. Higher-order: Map Filter Reduce ฉบับ Go

เขียนเองครั้งเดียว ใช้ได้ทุก type ด้วย generics (Go 1.18+):

go
func Map[T, R any](s []T, f func(T) R) []R {
    out := make([]R, 0, len(s))
    for _, v := range s { out = append(out, f(v)) }
    return out
}
func Filter[T any](s []T, keep func(T) bool) []T {
    var out []T
    for _, v := range s { if keep(v) { out = append(out, v) } }
    return out
}
func Reduce[T, R any](s []T, init R, f func(R, T) R) R {
    acc := init
    for _, v := range s { acc = f(acc, v) }
    return acc
}

// ใช้
nums := []int{1, 2, 3, 4}
evens := Filter(nums, func(n int) bool { return n%2 == 0 }) // [2 4]
squares := Map(evens, func(n int) int { return n*n })       // [4 16]
sum := Reduce(squares, 0, func(a, b int) int { return a+b }) // 20

ทริก: ถ้าโปรเจกต์เล็กใช้ for ธรรมดาก็พอ อย่าฝืน functional จนอ่านยาก กฎ Effective Go คืออ่านง่ายก่อนเท่

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


4. Closure + Curry แบบง่ายๆ

Closure = ฟังก์ชันจำของข้างนอกได้:

go
func Counter() func() int {
    n := 0
    return func() int { n++; return n }
}

Curry = แตกฟังก์ชันหลายช่องให้เหลือช่องเดียวต่อครั้ง:

go
func Add(a int) func(int) int {
    return func(b int) int { return a+b }
}
add5 := Add(5)
fmt.Println(add5(3)) // 8

ใช้ทำ middleware / ตั้งค่า:

go
func WithPrefix(p string) func(string) string {
    return func(s string) string { return p + s }
}
hello := WithPrefix("สวัสดี ")
fmt.Println(hello("สมชาย"))
✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


5. Pure vs ไม่ Pure + Immutability ใน Go
  • Pure = ใส่เท่าเดิม ได้เท่าเดิม ไม่แอบแก้ข้างนอก เทสต์ง่าย
go
// pure
func Add(a, b int) int { return a+b }
// ไม่ pure (แอบแก้ข้างนอก)
var total int
func AddToTotal(n int) { total += n }

Go ไม่มี immutable แท้ๆ แต่เลียนแบบได้:

  • ใช้ append([]T{}, s...) ก็อปก่อนแก้ ไม่แก้ของเดิม
  • ใช้ struct ค่า (value) แทน pointer ถ้าไม่อยากให้แก้
  • ส่งของผ่าน channel แทนแชร์ตัวแปร (ตรงกับคำขวัญ Go: share memory by communicating)
✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


6. Functional + Generics + Channels (ของใหม่ 1.27)

Go 1.27 เดาชนิดให้ ไม่ต้องเขียน [T] ย้ำ แม้ส่งฟังก์ชันเข้า channel:

go
func GenericFormatter[T any](v T) string { return fmt.Sprintf("value: %v", v) }
type IntFormatter func(int) string

ch := make(chan IntFormatter, 1)
ch <- GenericFormatter // 1.27 เดา T=int ให้เอง เมื่อก่อนต้องเขียน GenericFormatter[int]
formatters := []IntFormatter{GenericFormatter}

เอา functional มาผสม pipeline:

go
func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() { defer close(out); for _, n := range nums { out <- n } }()
    return out
}
func MapChan[T, R any](in <-chan T, f func(T) R) <-chan R {
    out := make(chan R)
    go func() { defer close(out); for v := range in { out <- f(v) } }()
    return out
}
// ใช้: for v := range MapChan(gen(1,2,3,4), func(n int) int { return n*n }) { fmt.Println(v) }
✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


7. Mini Project + แบบฝึก

โจทย์: มีออเดอร์ []Order{Price, Qty} ให้กรองเฉพาะ Qty>0 → คิด Price*Qty → รวมยอด ด้วย Map/Filter/Reduce ที่เขียนเอง + เทสต์

แบบฝึก:

  1. เขียน Filter กรองเลขคี่
  2. เขียน Map แปลงชื่อเป็น "สวัสดี "+ชื่อ
  3. ทำ counter 2 ตัวแยกกันไม่แย่งกัน
  4. ทำ pipeline gen -> filter คู่ -> square ด้วย channel
  5. รัน go vet + go fix ./... ดูว่าแนะนำอะไร (1.26 มี modernizers ช่วยย่อโค้ด)

อ้างอิง: Effective Go #functions, Go 1.27 blog generic inference + channel sends

✏️ แบบทดสอบท้ายบท
แบบทดสอบท้ายบท
0/2


ภาคผนวก: ตัวอย่างเพิ่ม + Best Practices
ตัวอย่างเพิ่ม 1: Map generic ใช้ได้ทุก type
go
func Map[T any, U any](xs []T, f func(T) U) []U {
    out := make([]U, 0, len(xs))
    for _, x := range xs {
        out = append(out, f(x))
    }
    return out
}
names := Map([]int{1, 2, 3}, func(n int) string {
    return fmt.Sprint("เบอร์ ", n)
})
ตัวอย่างเพิ่ม 2: Filter กรองของ
go
func Filter[T any](xs []T, keep func(T) bool) []T {
    var out []T
    for _, x := range xs {
        if keep(x) {
            out = append(out, x)
        }
    }
    return out
}
evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { return n%2 == 0 })
ตัวอย่างเพิ่ม 3: closure counter แยกกันคนละตัว
go
func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}
a, b := counter(), counter()
fmt.Println(a(), a(), b()) // 1 2 1 (แยกกัน ไม่แย่งกัน)
Best practices เช็กลิสต์
  • pure func (input เดิมได้ output เดิม ไม่แตะของข้างนอก) เทสต์ง่ายสุด
  • อย่าแก้ slice/map ที่รับมา ถ้าจะเปลี่ยนให้ clone ก่อน (caller จะงง)
  • closure ระวัง loop var: Go 1.22+ แต่ละรอบได้ตัวแปรใหม่แล้ว ใช้ได้สบาย
  • Map/Filter ด้วย generics เขียนครั้งเดียวใช้ได้ทุก type
  • pure + immutable มากไปช้าได้ งานร้อนๆ วัดก่อนค่อยเลือก
  • ฟังก์ชันเล็กๆ ประกอบกัน (pipeline) อ่านง่ายกว่าก้อนยักษ์ก้อนเดียว
แบบฝึกหัด
  • เขียน Reduce รวมยอดราคาสินค้าในตะกร้า
  • ทำ retry(fn, 3) higher-order ที่ลองใหม่เองเมื่อ error