สารบัญ
1 · Function ขั้นสูง
Variadic
func sum(nums ...int) int { ... } // ข้างในนี้ nums คือ []int ธรรมดา
sum() // 0
sum(10, 20) // 30
sum(xs...) // กาง slice เข้าไป — ต้องมี ... ไม่งั้น error
- ต้องเป็น parameter ตัวสุดท้าย และมีได้ ตัวเดียว
- ห้ามใช้กับ return type
Function เป็น value ชนิดหนึ่ง
op := add // เก็บใส่ตัวแปร — type คือ func(int, int) int
op = multiply // สลับได้ ถ้า signature เหมือนกัน (ชื่อไม่เกี่ยว)
func compute(op func(float64, float64) float64) float64 // รับเข้า
func compute() func(float64, float64) float64 // คืนออก
Anonymous / IIFE / Closure
greet := func(n string) { ... } // anonymous — เก็บใส่ตัวแปร
func(n string) { ... }("Gopher") // IIFE — ประกาศแล้วเรียกทันที
func newCounter() func() int {
count := 0 // escape ไป heap
return func() int { count++; return count }
}
c := newCounter(); c(); c() // 1 แล้ว 2
d := newCounter(); d() // 1 — คนละตัว! เรียกใหม่ = count ใหม่
closure จับ "ตัวแปร" ไม่ใช่ "ค่า" — จำประโยคนี้ไว้ จะได้ใช้ตอน defer
2 · Array — [N]T
var arr [5]int = [5]int{10, 20, 30, 40, 50}
unsafe.Sizeof(arr) // 40 = 5 × 8 bytes (ข้อมูลจริงอยู่ในตัวมันเอง)
- ขนาดเป็นส่วนหนึ่งของ type —
[3]intกับ[5]intคนละ type assign กันไม่ได้ - value type → assign / ส่งเข้าฟังก์ชัน = copy ทั้งก้อน
- เทียบ
==ได้ (ต่างจาก slice) - งานจริงแทบไม่ใช้ตรง ๆ แต่ต้องเข้าใจเพราะ slice สร้างทับมัน
3 · Slice — []T
กฎเหล็ก: 0 <= len <= cap เสมอ
สร้าง
s := []int{10, 20, 30} // len=3 cap=3
s := make([]int, 5, 10) // make([]T, len, cap)
s := make([]int, 5) // cap = len = 5
var s []int // nil slice: len=0 cap=0 data=nil
Subslice — จุดที่บั๊กเกิด
| เขียน | len | cap | หมายเหตุ |
|---|---|---|---|
s[low:high] | high − low | cap(s) − low | ⚠️ high ไม่คุม cap |
s[low:high:max] | high − low | max − low | ✓ คุม cap ได้เอง |
s := []int{10, 20, 30, 40, 50}
c := s[1:3] // len=2 cap=4 → ยังเหลือที่
c = append(c, 999)
// s → [10 20 30 999 50] 💥 40 โดนทับ
c := s[1:3:3] // len=2 cap=2 → เต็มพอดี
c = append(c, 999)
// s → [10 20 30 40 50] ✓ ปลอดภัย (append จองบ้านใหม่)
CRUD
s = append(s, 40) // Create
v := s[1] // Read
s[1] = 99 // Update
s = slices.Delete(s, 1, 2) // Delete (Go 1.21+) — แนะนำ
s = append(s[:i], s[i+1:]...) // Delete เขียนเอง (แก้ array เดิมในที่!)
copy(dst, src) // คัดลอก = min(len(dst), len(src)) ตัว
Growth strategy (วัดจริงบน Go 1.26)
รู้จำนวนล่วงหน้า → make([]T, 0, n) จองทีเดียว เร็วกว่ามาก
nil vs empty
var nilS []int; emptyS := []int{}
nilS == nil // true
emptyS == nil // false
len(nilS) // 0 — ใช้ได้
for range nilS {} // ปลอดภัย ไม่ panic
nilS = append(nilS, 1) // ✓ ได้!
nilS[0] // 💥 panic: index out of range
0 <= len <= capเสมอ · เข้าถึง index ที่ไม่มี → panic- เทียบ
==ไม่ได้ (ยกเว้นกับnil) → ใช้slices.Equal() - เป็น reference type → ระวังการแชร์ memory ทุกครั้งที่ subslice
- subslice ข้อมูลนิดเดียวจาก array ยักษ์ = GC เก็บ array ทั้งก้อนไม่ได้ → ใช้
copy() - ห้ามลบสมาชิกระหว่าง
for range(range ใช้ snapshot ของ header ตอนเริ่ม)
4 · Map — map[K]V
m := make(map[string]int) // ✓ ใช้ได้เลย
var m map[string]int // nil map — เขียนแล้ว panic!
m := map[string]int{"a": 1} // literal
m["k"] = 10 // Create / Update — syntax เดียวกัน
v := m["k"] // Read — key ไม่มีจะคืน zero value ไม่ใช่ error
delete(m, "k") // Delete — ลบ key ที่ไม่มีก็ไม่ error
Comma-ok — ใช้ทุกครั้งที่ "ไม่มี" ≠ "เป็นศูนย์"
v, ok := m["k"]
if v, ok := m["k"]; ok { ... }
if _, ok := m["k"]; !ok { ... }
ภายใน — hash map
Nested map — ต้อง init ชั้นในก่อน
users := make(map[string]map[string]int)
users["alice"] = map[string]int{} // ← ขาดบรรทัดนี้ = panic
users["alice"]["score"] = 90
- ไม่มีลำดับ —
for rangeออกมาไม่เรียง และสุ่มใหม่ทุกรอบ (Go จงใจ)
ต้องการเรียง → เก็บ key ใส่ slice แล้วslices.Sort() - nil map เขียน = panic (แต่อ่านคืน zero value ได้) — ต่างจาก nil slice
- non-addressable →
m["a"].Age = 31compile ไม่ผ่าน
cannot assign to struct field m["a"].Age in map
แก้: ใช้map[K]*V· หรืออ่าน–แก้–ยัดกลับ okแยก "ไม่มี key" ออกจาก "มีแต่เป็น zero value"
5 · Struct
type User struct { Name string; Age int }
var u1 User // zero value ทุก field
u2 := User{Name: "Bob", Age: 30} // ✓ named — ใช้แบบนี้
u3 := User{"Bob", 30} // ⚠️ positional — เพิ่ม field ทีหลังแล้วพัง
fmt.Printf("%+v\n", u2) // {Name:Bob Age:30} — %+v โชว์ชื่อ field
ทำไม Go ไม่มี class
| ปัญหาของ inheritance | คำอธิบาย |
|---|---|
| Diamond Problem | สืบมาสองทางที่มีต้นตอเดียวกัน → เรียก method ของใคร |
| Tight Coupling | แก้พ่อทีลูกพังหมด |
| Deep Hierarchy | สืบ 7 ชั้น อ่านโค้ดต้องไล่ขึ้นไปหาต้นตอ |
| Is-A Problem | บังคับความสัมพันธ์ "เป็น" ทั้งที่จริงแค่ "มี" |
Go แทนด้วย Composition + Promotion + Has-A
Embedding & Promotion
type Animal struct{ Name string }
func (a Animal) Eat() { ... }
type Dog struct {
Animal // embed — ไม่มีชื่อ field
Breed string
}
d := Dog{Animal: Animal{Name: "บัดดี้"}, Breed: "Golden"}
d.Name // promoted field (= d.Animal.Name)
d.Eat() // promoted method (= d.Animal.Eat())
ชื่อชนกัน → ชั้นตื้นกว่าชนะ (shadowing ไม่ใช่ override — ตัวเดิมยังอยู่):
func (c Cat) Eat() { ... } // Cat มี Eat เอง + embed Animal ที่มี Eat
c.Eat() // ของ Cat
c.Animal.Eat() // ของ Animal — ยังเรียกได้ผ่านทางเต็ม
Anonymous struct
p := struct{ X, Y int }{X: 1, Y: 2} // ใช้ครั้งเดียวแล้วทิ้ง
เหมาะกับ table-driven test / decode JSON เฉพาะกิจ — ใช้ซ้ำเมื่อไหร่ให้ตั้ง type ทันที
| ✓ Do | ✗ Don't |
|---|---|
| เก็บข้อมูลที่เกี่ยวข้องกันเป็นหน่วยเดียว | positional init {1,"a","b"} |
| ตั้งชื่อ field ให้สื่อชัดเจน | anonymous struct โดยไม่จำเป็น |
| embed + composition + promotion | embed ลึกเกิน 3 ชั้น |
6 · Pointer & Memory
x := 10
p := &x // & = address of
fmt.Println(*p) // * = dereference → 10
*p = 20 // เขียนผ่านที่อยู่ → x เป็น 20
* มี 2 ความหมาย: อยู่ใน type (var p *int) = "เป็น pointer" · อยู่ใน นิพจน์ (*p) = "ตามไปเอาค่า"
อะไร & ได้ / ไม่ได้ (addressable)
- zero value =
nil→ deref nil = panic - ขนาด 8 bytes เสมอ (64-bit) ไม่ว่าจะชี้ไป struct ใหญ่แค่ไหน
- ทุกอย่างใน Go เป็น pass by value 100% — pointer แค่ copy "ที่อยู่" แทน "ของ"
c.countเมื่อcเป็น pointer → Go แปลง(*c).countให้อัตโนมัติ
ควร / ไม่ควรใช้ pointer
| ✓ ควรใช้ | ✗ ไม่ควรใช้ |
|---|---|
| ต้องแก้ตัวแปรต้นฉบับ | struct เล็ก (≤64 bytes) — copy เร็วกว่า |
| ข้อมูลขนาดใหญ่ (copy แพง) | ฟังก์ชันที่แค่อ่านค่า |
ต้องการให้ค่าเป็น nil ได้ | type ที่ value คือ identity (time.Time) |
| slice / map / channel — เป็น reference อยู่แล้ว |
Stack vs Heap
| Stack | Heap |
|---|---|
| เร็ว (แค่เลื่อน pointer) · พื้นที่น้อย · คืนอัตโนมัติเมื่อฟังก์ชันจบ | ยืดหยุ่น · ต้องมี GC · อยู่จนกว่าจะไม่มีใครชี้ |
Escape analysis — compiler ตัดสินใจให้เอง ว่าตัวแปรหลุดออกนอก scope ไหม
func newUser() *User {
u := User{Name: "Alice"} // local
return &u // ที่อยู่หลุดออกไป → escape to heap
}
go build -gcflags="-m" ./... # ให้ compiler บอกว่าตัวไหน escape
Garbage Collector — กฎเดียว: "ไม่มีการถูกชี้จากที่ไหนอีก" → ถูกนำออก
7 · defer
กฎ 3 ข้อจาก blog ทางการ (รันยืนยันแล้วทั้งสามข้อ):
| # | กฎ | ตัวอย่าง | ผล |
|---|---|---|---|
| 1 | argument ประเมินตอนบรรทัด defer | i:=0; defer Println(i); i++ | 0 |
| 2 | LIFO หลังฟังก์ชัน return | for i:=0;i<4;i++ { defer Print(i) } | 3210 |
| 3 | แก้ named return value ได้ | func c() (i int) { defer func(){i++}(); return 1 } | 2 |
// อยากได้ค่า ณ ตอนประกาศ → ส่งเป็น argument
defer fmt.Println(i)
// อยากได้ค่าล่าสุด → ห่อ closure
defer func() { fmt.Println(i) }()
Pattern ที่ใช้จริง:
defer f.Close() // ทันทีหลัง Open
defer mu.Unlock() // ทันทีหลัง Lock
defer tx.Rollback() // ทันทีหลัง Begin — commit สำเร็จแล้วไม่มีผล
defer trackTime("op")() // จับเวลา — สังเกตวงเล็บ 2 ชุด
// ห่อ error ทุกจุด return ด้วยกฎ 3
func doWork() (err error) {
defer func() { if err != nil { err = fmt.Errorf("doWork: %w", err) } }()
...
}
- scope คือฟังก์ชัน ไม่ใช่ block —
deferในลูปกองสะสมจนจบฟังก์ชัน → ครอบด้วย IIFE os.Exit()/log.Fatal()ไม่รัน defer (แต่panicรันครบ)
8 · Method & Receiver
| Method | Function | |
|---|---|---|
| ผูกกับ struct | เสมอ | อิสระ |
| เรียกด้วยชื่อตรง ๆ | ไม่ได้ (ต้อง obj.M()) | ได้ |
| ใช้กับ | พฤติกรรมของ type นั้น | logic กลาง เช่น utility, use case |
Go ไม่มี method overloading — ตั้งชื่อซ้ำใน type เดียวกันไม่ได้
Value vs Pointer receiver
func (u User) Greet() string { ... } // ได้ copy — read only / getter
func (a *Account) Deposit(x float64) { a.Balance += x } // แก้ของจริง
acc := Account{...}
acc.Deposit(500) // Go แปลงเป็น (&acc).Deposit(500) ให้อัตโนมัติ
เกณฑ์จาก Go Code Review Comments:
- ต้องแก้ค่า receiver → ต้องเป็น pointer
- มี
sync.Mutexข้างใน → ต้องเป็น pointer (copy แล้ว lock ไม่ทำงาน) - struct/array ใหญ่ → pointer
- receiver เป็น map / func / chan → อย่าใช้ pointer
- struct เล็กที่เป็น value โดยธรรมชาติ (แบบ
time.Time) → value - "Don't mix receiver types" · "When in doubt, use a pointer receiver."
Encapsulation — ตัวใหญ่/ตัวเล็ก
| ขึ้นต้น | ความหมาย |
|---|---|
Name ตัวใหญ่ | exported (public) — package อื่นเห็น |
balance ตัวเล็ก | unexported (private) — เฉพาะใน package เดียวกัน |
ขอบเขตคือ package ไม่ใช่ struct — ไฟล์อื่นใน package เดียวกันเข้าถึง balance ได้
Method chaining
func (sb *StringBuilder) Append(s string) *StringBuilder {
sb.buf = append(sb.buf, s...)
return sb // ← คืนตัวเอง = ต่อกันได้
}
result := (&StringBuilder{}).Append("Hello").Append(", World").String()
ข้อเสีย: จัดการ error ยาก — งานที่มี error ให้ใช้แบบธรรมดา
9 · ตารางกับดักรวม — อ่านก่อนส่ง PR
| โค้ดที่เห็น | สิ่งที่คุณคิด | สิ่งที่เกิดจริง |
|---|---|---|
c := s[1:3]; append(c, x) |
c เป็นของตัวเอง | 💥 เขียนทับ s[3] |
for _, u := range users { u.N = "x" } |
แก้ users ได้ | แก้ copy → ไม่เปลี่ยน · ใช้ users[i] |
m["a"].Age = 31 |
แก้ค่าใน map | 💥 compile error (non-addressable) |
var m map[string]int; m["a"]=1 |
สร้าง map ให้เอง | 💥 panic: nil map |
i:=0; defer Println(i); i++ |
พิมพ์ 1 | พิมพ์ 0 (argument แช่แข็ง) |
for … { defer f.Close() } |
ปิดทุกรอบ | กองจนจบฟังก์ชัน → file leak |
log.Fatal() ที่มี defer ค้าง |
cleanup ให้ | ไม่รัน defer เลย |
func (a Account) Deposit() |
ยอดเงินเพิ่ม | แก้ copy → ต้องใช้ *Account |
ลบสมาชิกใน for range |
ลบครบ | ข้ามตัว/ผลเพี้ยน (snapshot ของ header) |
small := huge[:10] เก็บนาน |
ใช้แค่ 10 ตัว | GC เก็บ huge ทั้งก้อนไม่ได้ → ใช้ copy() |