Reference · เก็บไว้เปิดตอนเขียนโค้ด

Go Day 3 — Cheat Sheet

ครอบคลุมสไลด์ทั้ง 107 หน้า · ตัวอย่างทุกอันรันจริงบน Go 1.26 แล้ว · พิมพ์ลงกระดาษได้

สารบัญ

  1. Function ขั้นสูง
  2. Array
  3. Slice
  4. Map
  5. Struct
  6. Pointer & Memory
  7. defer
  8. Method & Receiver
  9. ตารางกับดักรวม

1 · Function ขั้นสูง

Variadic

func sum(nums ...int) int { ... }   // ข้างในนี้ nums คือ []int ธรรมดา

sum()            // 0
sum(10, 20)      // 30
sum(xs...)       // กาง slice เข้าไป — ต้องมี ... ไม่งั้น error

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 (ข้อมูลจริงอยู่ในตัวมันเอง)

3 · Slice — []T

Slice Header (24 bytes) Underlying Array ┌──────────────┐ ┌────┬────┬────┬───┬───┐ │ Data ────────┼────────────────────▶│ 10 │ 20 │ 30 │ . │ . │ │ Len = 3 │ └────┴────┴────┴───┴───┘ │ Cap = 5 │ len=3 ──┘ cap−len=2 ┘ └──────────────┘ unsafe.Sizeof(s) == 24 เสมอ (วัดแค่ header ไม่ใช่ข้อมูล)

กฎเหล็ก: 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 — จุดที่บั๊กเกิด

เขียนlencapหมายเหตุ
s[low:high]high − lowcap(s) − low⚠️ high ไม่คุม cap
s[low:high:max]high − lowmax − 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)

len=1 cap=4 len=257 cap=512 len=5 cap=8 len=513 cap=848 ← เลิกเป็น 2x ตรงนี้ len=9 cap=16 len=849 cap=1280 len=129 cap=256 len=1793 cap=2560 ≤ 256 → โต 2x เป๊ะ ๆ > 256 → ค่อย ๆ ลดเข้าหา ~1.25x สูตรจริง: newcap + (newcap + 3×256)/4 แล้วปัดตาม memory size class

รู้จำนวนล่วงหน้า → 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
Key takeaway — Slice

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

m (pointer 8 bytes) → hmap { count, B, hash0, buckets → [bmap × 2^B] } O(1) ปกติ · O(n) worst case เมื่อ collision หนัก hash0 สุ่มทุกครั้งที่รัน → กัน hash-flooding + เป็นเหตุผลที่ลำดับไม่แน่นอน

หมายเหตุ: hmap/bmap เป็น internal ของ runtime เปลี่ยนได้ทุกเวอร์ชัน (Go 1.24 เปลี่ยนไปใช้ Swiss Table แล้ว) — เข้าใจแนวคิดพอ

Nested map — ต้อง init ชั้นในก่อน

users := make(map[string]map[string]int)
users["alice"] = map[string]int{}   // ← ขาดบรรทัดนี้ = panic
users["alice"]["score"] = 90
Key takeaway — Map

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 — Struct
✓ Do✗ Don't
เก็บข้อมูลที่เกี่ยวข้องกันเป็นหน่วยเดียวpositional init {1,"a","b"}
ตั้งชื่อ field ให้สื่อชัดเจนanonymous struct โดยไม่จำเป็น
embed + composition + promotionembed ลึกเกิน 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)

✓ &x ตัวแปรปกติ ✗ &42 literal ✓ &s.field field ของ struct ✗ &foo() ผลลัพธ์ฟังก์ชัน ✓ &arr[i] element array/slice ✗ &m["k"] map value (ย้ายตอน rehash) ✓ &(*p) pointer ที่ deref ✗ &MyConst constant

ควร / ไม่ควรใช้ pointer

✓ ควรใช้✗ ไม่ควรใช้
ต้องแก้ตัวแปรต้นฉบับstruct เล็ก (≤64 bytes) — copy เร็วกว่า
ข้อมูลขนาดใหญ่ (copy แพง)ฟังก์ชันที่แค่อ่านค่า
ต้องการให้ค่าเป็น nil ได้type ที่ value คือ identity (time.Time)
slice / map / channel — เป็น reference อยู่แล้ว

Stack vs Heap

StackHeap
เร็ว (แค่เลื่อน 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 ทางการ (รันยืนยันแล้วทั้งสามข้อ):

#กฎตัวอย่างผล
1argument ประเมินตอนบรรทัด defer i:=0; defer Println(i); i++0
2LIFO หลังฟังก์ชัน 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) } }()
    ...
}
กับดัก defer

8 · Method & Receiver

func (c *Cart) TotalPrice() (*money.Money, error) { … } └───┬───┘ └────┬────┘ └───────────┬─────────┘ receiver method name signature Base type: Cart · Receiver type: *Cart
MethodFunction
ผูกกับ 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:

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()