Maps
การสร้าง map
หัวข้อที่มีชื่อว่า “การสร้าง map”Map คือ hash table ที่ map key ชนิด K ไปยัง value ชนิด V สร้างด้วย make หรือ composite literal:
// make — preferred when you add entries incrementallyscores := make(map[string]int)
// composite literal — preferred when entries are known upfrontscores := map[string]int{ "Alice": 95, "Bob": 87,}Key type ต้องเป็น comparable (รองรับ ==) type ที่ใช้เป็น key ได้คือ string, int, ตัวเลขทุกชนิด, boolean, pointer, array, channel, interface, และ struct ที่ field ทุกตัวเป็น comparable interface key compile ผ่านได้เสมอ แต่จะ panic ตอน runtime ถ้า dynamic type ที่มันเก็บอยู่กลายเป็น incomparable (เช่น slice); pointer key เปรียบเทียบกันที่ address ไม่ใช่ค่าที่มันชี้ไป ส่วน slice, map, และ function ไม่สามารถใช้เป็น key ได้
Set, get, delete
หัวข้อที่มีชื่อว่า “Set, get, delete”scores["Charlie"] = 72 // setfmt.Println(scores["Alice"]) // get → 95delete(scores, "Bob") // delete (no-op if key absent)Zero value สำหรับ key ที่ไม่มีอยู่
หัวข้อที่มีชื่อว่า “Zero value สำหรับ key ที่ไม่มีอยู่”การอ่าน key ที่ไม่มีอยู่ ไม่ panic เลย — จะคืน zero value ของ value type:
fmt.Println(scores["Dave"]) // 0 — not a panicแม้จะสะดวก แต่ก็ซ่อนบั๊กได้: คุณไม่รู้ว่า key map ไปที่ 0 หรือไม่เคยถูก set ใช้ comma-ok idiom เพื่อแยกแยะ
Comma-ok idiom
หัวข้อที่มีชื่อว่า “Comma-ok idiom”การ lookup map แบบสอง result คืนค่าและ boolean ที่บอกว่า key มีอยู่หรือไม่:
v, ok := scores["Dave"]if !ok { fmt.Println("Dave not found")}Nil map: อ่านได้ แต่เขียนไม่ได้
หัวข้อที่มีชื่อว่า “Nil map: อ่านได้ แต่เขียนไม่ได้”Zero value ของ map type คือ nil — ไม่ใช่ map ว่างที่พร้อมใช้งาน nil map ทำตัวเหมือน map ว่างสำหรับ operation อ่าน ทุกแบบ แต่จะ panic ตอนเขียน:
var m map[string]intfmt.Println(m["a"], len(m)) // 0 0 — reading a nil map is completely safem["a"] = 1 // panic: assignment to entry in nil mapverify บน 1.26.5 แล้ว panic คือ:
panic: assignment to entry in nil mapนี่คือหนึ่งใน bug ที่ AI เขียน Go แล้วพลาดบ่อยที่สุด — struct field ที่ declare เป็น map[string]int โดยไม่ initialize อะไรเลย ดูเหมือน zero value ปกติ และ compile ผ่านสบาย ๆ จนกว่าจะเขียนครั้งแรก initialize map ที่ตั้งใจจะเขียนเสมอ — make(map[string]int) หรือ composite literal ไม่ว่าจะตอน declare หรือใน constructor
Map เป็น reference type
หัวข้อที่มีชื่อว่า “Map เป็น reference type”การ assign map จะ copy แค่ header ไม่ใช่ข้อมูล ตัวแปรทั้งสองชี้ไปยัง hash table เดียวกัน:
a := map[string]int{"x": 1}b := ab["x"] = 99fmt.Println(a["x"]) // 99 — a and b share the same mapMap เป็น function argument
หัวข้อที่มีชื่อว่า “Map เป็น function argument”เพราะ map value จริง ๆ ก็คือ pointer ไปยัง hash table ของ runtime การส่ง map เข้าฟังก์ชันคือการส่ง pointer นั้นแบบ by value — ฟังก์ชันได้ copy ของ header เป็นของตัวเอง แต่ header ทั้งสองยังชี้ไปยัง table เดียวกัน ไม่มีทางป้องกันไม่ให้ฟังก์ชันแก้ map ที่เราส่งเข้าไป และก็ไม่จำเป็นต้อง return มันกลับด้วย — caller เห็นทุกการเปลี่ยนแปลงอยู่แล้ว:
func setAll(m map[string]int, v int) { for k := range m { m[k] = v }}
scores := map[string]int{"a": 1, "b": 2}setAll(scores, 9)fmt.Println(scores["a"], scores["b"]) // 9 9 — the caller's map was mutated directlyConcurrent map write คือ crash ที่กู้คืนไม่ได้
หัวข้อที่มีชื่อว่า “Concurrent map write คือ crash ที่กู้คืนไม่ได้”Map ของ Go ไม่ ปลอดภัยสำหรับการเข้าถึงพร้อมกัน goroutine สองตัวที่เขียน map เดียวกันพร้อมกันจะไม่ race กันแบบเงียบ ๆ — runtime ตรวจจับได้และ crash ทั้งโปรเซสทันที และ crash แบบนี้ recover() จับไม่ได้:
m := make(map[int]int)var wg sync.WaitGroupfor i := 0; i < 20; i++ { wg.Add(1) go func(n int) { defer wg.Done() for j := 0; j < 10000; j++ { m[n] = j } }(i)}wg.Wait()verify บน 1.26.5 (go run) แล้ว จบด้วยข้อความนี้เสมอ:
fatal error: concurrent map writes
goroutine 27 [running]:internal/runtime/maps.fatal(...)...fatal error (ไม่ใช่ panic:) คือสัญญาณของ Go ว่า “นี่ไม่ใช่ application error ที่กู้คืนได้ นี่คือการละเมิดความสมบูรณ์ของ runtime” — อยู่ในหมวดเดียวกับการถูก kill เพราะ out-of-memory หรือ stack overflow ถ้าหลาย goroutine ต้องอ่านและเขียน map เดียวกัน ให้ป้องกันทุกการเข้าถึงด้วย sync.RWMutex (RLock/RUnlock สำหรับอ่าน, Lock/Unlock สำหรับเขียน) หรือใช้ sync.Map เมื่อ pattern การเข้าถึงเป็นแบบ key แยกกันต่อ goroutine เป็นส่วนใหญ่ หรือมี key จำนวนน้อยที่ถูกแย่งใช้หนัก ๆ sync.RWMutex คลุม map ธรรมดามักเป็นตัวเลือกที่ดีกว่าโดย default — sync.Map ถูกออกแบบมาสำหรับ workload ที่แคบกว่าและเสีย type safety ไป (API ของมันรับและคืนค่าเป็น any)
Map value ไม่ addressable
หัวข้อที่มีชื่อว่า “Map value ไม่ addressable”เอา address ของ map element ไม่ได้ และเรียก method แบบ pointer-receiver หรือ assign field ของ struct ที่เก็บตรง ๆ ใน map ก็ไม่ได้:
type Point struct{ X, Y int }m := map[string]Point{"origin": {0, 0}}m["origin"].X = 5 // compile errorverify บน 1.26.5 (go build) แล้ว:
./main.go:7:2: cannot assign to struct field m["origin"].X in map& ก็เหมือนกัน:
mm := map[string]int{"a": 1}p := &mm["a"] // compile error./main.go:5:8: invalid operation: cannot take address of m["a"] (map index expression of type int)เหตุผลเป็นเรื่องโครงสร้าง ไม่ใช่กฎที่ตั้งขึ้นมาลอย ๆ: map สามารถ rehash แล้วย้ายทุก entry ไป bucket layout ใหม่ได้ทุกเมื่อ (ตอน grow หรือด้วย Swiss-table implementation ที่ใช้ตั้งแต่ Go 1.24) pointer เข้าไปใน bucket เก่าจะชี้ไปยัง memory ที่ค้างหรือถูกใช้ซ้ำแบบเงียบ ๆ หลัง rehash ดังนั้น compiler จึงปฏิเสธที่จะสร้าง pointer แบบนั้นตั้งแต่แรก แก้ด้วย pattern copy-modify-store หรือเปลี่ยนไปเก็บ pointer แทน:
p := m["origin"] // load a copyp.X = 5m["origin"] = p // store it back
// or: map[string]*Point avoids the copy round-trip entirelym2 := map[string]*Point{"origin": {0, 0}}m2["origin"].X = 5 // fine — *Point is addressable through the pointer already stored in the mapการ mutate map ระหว่าง range
หัวข้อที่มีชื่อว่า “การ mutate map ระหว่าง range”การลบ key ปัจจุบันระหว่าง range เป็นเรื่องปลอดภัยและถูกกำหนดไว้ชัดเจน — key ที่ถูกลบจะไม่ถูกส่งออกมาใน iteration ถัดไปเลย แต่การเพิ่ม key ใหม่ระหว่าง range เป็นอีกเรื่อง — spec ของภาษาปล่อยให้ ไม่ระบุ (unspecified) ว่า key ที่เพิ่งเพิ่มเข้าไปจะถูก visit ใน loop เดียวกันนั้นหรือไม่ อย่าเขียน loop ที่ความถูกต้องขึ้นอยู่กับการเห็น (หรือไม่เห็น) key ที่เพิ่ง insert เข้าไปเอง
Composite key
หัวข้อที่มีชื่อว่า “Composite key”Type ที่ comparable ทุกตัวใช้เป็น map key ได้ รวมถึง struct ด้วย นี่เป็นวิธี idiomatic ทั่วไปในการสร้าง lookup แบบหลายส่วน (พิกัด 2D, คู่ (userID, resourceID)) โดยไม่ต้องต่อ string เข้าด้วยกัน:
type Coord struct{ X, Y int }
grid := map[Coord]string{ {0, 0}: "origin", {1, 1}: "diag",}fmt.Println(grid[Coord{0, 0}]) // originลำดับ iteration แบบสุ่ม
หัวข้อที่มีชื่อว่า “ลำดับ iteration แบบสุ่ม”Spec ของภาษาไม่ได้กำหนดลำดับ iteration ไว้ และ runtime ก็จงใจสุ่มจุดเริ่มต้นของทุก range loop — สอง loop ในการรันเดียวกันยังให้ลำดับต่างกันได้ หากต้องการ print entry ของ map แบบ deterministic ให้ sort key ก่อน:
import "sort"
keys := make([]string, 0, len(scores))for k := range scores { keys = append(keys, k)}sort.Strings(keys)for _, k := range keys { fmt.Printf("%s: %d\n", k, scores[k])}ตั้งแต่ Go 1.23 package maps และ slices ทำเรื่องนี้ได้ในบรรทัดเดียว — maps.Keys คืน iterator และ slices.Sorted เก็บผลแล้ว sort ให้:
import ("maps"; "slices")
for _, k := range slices.Sorted(maps.Keys(scores)) { fmt.Printf("%s: %d\n", k, scores[k])}helper สมัยใหม่อีกสองตัว: clear(m) (Go 1.21) ลบทุก entry และ maps.Clone/maps.Copy ใช้แทน loop copy ที่เขียนเอง
กำหนดขนาด map ล่วงหน้า
หัวข้อที่มีชื่อว่า “กำหนดขนาด map ล่วงหน้า”make(map[K]V, hint) pre-size พื้นที่เก็บข้อมูลภายในของ map ไว้ล่วงหน้าสำหรับประมาณ hint entry ช่วยหลีกเลี่ยงการโตแบบค่อยเป็นค่อยไป (และการ rehash) ที่เกิดขึ้นเมื่อ map ที่สร้างด้วย make(map[K]V) เต็มขึ้นเรื่อย ๆ hint เป็นแค่คำแนะนำ ไม่ใช่ขีดจำกัดตายตัว — map ยังโตเกินมันได้ถ้าจำเป็น:
big := make(map[int]bool, 100)fmt.Println(len(big)) // 0 — hint pre-sizes storage; it does not create entriesตั้งแต่ Go 1.24 การ implement map ของ runtime เป็นแบบ Swiss-table (open addressing ที่ใช้ metadata แบบ SIMD-friendly) แทน bucket-and-overflow-chain layout แบบเดิม — เร็วขึ้นและใช้ memory น้อยลง แต่ contract ระดับภาษาที่เรียนมาในบทนี้ (ไม่มีลำดับ, ไม่ addressable, ไม่ปลอดภัยสำหรับ concurrency) ไม่เปลี่ยนแปลงเลย
package main
import ( "fmt" "sort")
func main() { // make a map and add entries scores := make(map[string]int) scores["Alice"] = 95 scores["Bob"] = 87 scores["Charlie"] = 72
// Comma-ok idiom v, ok := scores["Bob"] fmt.Println(v, ok) // 87 true
v2, ok2 := scores["Dave"] fmt.Println(v2, ok2) // 0 false
// Delete an entry delete(scores, "Charlie")
// Sort keys for deterministic output keys := make([]string, 0, len(scores)) for k := range scores { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Printf("%s: %d\n", k, scores[k]) }} Playground ตัวที่สองเน้นหัวข้อเชิงลึกด้านบน: map เป็น function argument, pattern copy-modify-store สำหรับ map value, การลบระหว่าง range แบบปลอดภัย, และการกำหนดขนาดด้วย hint
package main
import "fmt"
type Point struct{ X, Y int }
func setAll(m map[string]int, v int) { for k := range m { m[k] = v }}
func main() { // Maps as function arguments: the callee mutates the caller's map directly m := map[string]int{"a": 1, "b": 2} setAll(m, 9) fmt.Println(m["a"], m["b"])
// Map values are not addressable: load, modify, store back points := map[string]Point{"origin": {X: 0, Y: 0}} p := points["origin"] p.X = 5 points["origin"] = p fmt.Println(points["origin"])
// Deleting the CURRENT key during range is safe and specified scores := map[string]int{"a": 1, "b": 2, "c": 3} for k := range scores { if k == "b" { delete(scores, "b") } } fmt.Println(len(scores))
// make with a size hint pre-sizes storage; it does not create entries big := make(map[int]bool, 100) fmt.Println(len(big))} Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR H["hash(key)"] --> B0["bucket 0"] H --> B1["bucket 1"] H --> B2["bucket 2 (probed on collision)"] B0 --> E0["key/value slots"] B1 --> E1["key/value slots"] B2 --> E2["key/value slots"]
Map คือ hash table: hash(key) เลือก bucket และ entry อยู่ใน slot หนึ่งของ bucket นั้น (หรือ bucket ข้างเคียง ถ้า bucket แรกเต็มแล้ว — เรียกว่า “probing”) ภาพเดียวนี้อธิบายกฎสามข้อในบทนี้พร้อมกัน: lookup/insert/delete เป็น O(1) โดยเฉลี่ยเพราะการ hash กระโดดตรงไปยัง bucket เลย; ลำดับ iteration สุ่มเพราะไม่มีเหตุผลที่ต้อง visit bucket ตามลำดับใดลำดับหนึ่ง; และ value ที่เก็บใน bucket slot ไม่ addressable เพราะการ grow/rehash สามารถย้ายมันไปยัง bucket คนละตัวได้ทุกเมื่อ
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| built-in hash map | O(1) average lookup, ใช้งานง่าย | ไม่ thread-safe — ต้องใช้ mutex หรือ sync.Map |
| zero value for missing key | ไม่ panic เมื่ออ่าน key ที่ไม่มี | ซ่อน bug ถ้าลืมใช้ comma-ok idiom |
| dynamic sizing | map ขยายอัตโนมัติ | map ไม่ shrink อัตโนมัติ (Swiss table ตั้งแต่ 1.24 คืนบางส่วนเมื่อ grow); สร้างใหม่ถ้าต้องการปล่อย memory |
| randomized iteration | ป้องกัน code ที่พึ่งพา order โดยไม่ตั้งใจ | ต้อง sort key ถ้าต้องการ deterministic output |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- map safe สำหรับ concurrent access — map ไม่ใช่ — goroutine หลายตัวอ่าน/เขียนพร้อมกันทำให้เกิด race condition และ crash
- iteration order ของ map เป็น insertion order — Go map iterate แบบ random order โดยเจตนา ทุก run อาจต่างกัน
delete()ลด memory ทันที — Go map ไม่คืน memory ที่ใช้กับ bucket หลัง delete — สร้าง map ใหม่ถ้าต้องการ shrinklen(m)slow เหมือน iterate —len(map)เป็น O(1) Go track count ไว้ใน map header
💡 ตัวอย่างจากของจริง
http.Headerคือmap[string][]stringธรรมดา — request แต่ละตัวมี Header ของตัวเอง จึงไม่ต้อง lock
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Map เป็นจุดที่โค้ดที่ generate มา compile ผ่านฉลุยแต่ยังผิดได้ง่ายที่สุดจุดหนึ่ง เช็คสิ่งเหล่านี้:
- struct field ชนิด map เคยถูกเขียนโดยไม่เช็คว่า initialize แล้วหรือยังหรือเปล่า (
makeใน constructor หรือ check-and-init ตอนใช้ครั้งแรก)? field ชนิด map ที่เป็น zero value คือnilและการเขียนครั้งแรกจะ panic - มีโค้ดที่พยายาม assign เข้า field หรือ index ของ map value ตรง ๆ หรือเปล่า (
m[k].Field = x,m[k][i] = x)? ทั้งคู่เป็น compile error — map value ไม่ addressable - มี goroutine เขียน map ที่แชร์กันโดยไม่มี
sync.RWMutexหรือsync.Mapที่ไหนเลยหรือเปล่า? โค้ดแบบนี้ compile ผ่านและมักจะ “ใช้งานได้” ตอน test แล้วค่อย crash ทั้งโปรเซสตอน production ภายใต้ concurrency จริง - loop เพิ่ม key ใหม่เข้า map ที่กำลัง range อยู่ โดยคาดหวังผลลัพธ์ที่ deterministic เฉพาะเจาะจงหรือเปล่า? spec ปล่อยเรื่องนี้ไว้ unspecified — โค้ดอาจทำงานถูกวันนี้แล้วพังใน Go version อื่นหรือ GOMAXPROCS อื่น
- map ถูกส่งเข้าฟังก์ชันที่ผู้เขียนเชื่อว่าจะได้ “copy” หรือเปล่า (มี comment แบบ
// safe, m is a local copy)? copy ที่ได้มีแค่ header — callee ยังแก้ทุก entry ได้อยู่ดี
🐛 หา bug ในโค้ดนี้
type Config struct { Counts map[string]int}
func NewConfig() *Config { return &Config{}}
func main() { c := NewConfig() c.Counts["hits"]++ fmt.Println(c.Counts)}เฉลย
NewConfig คืน *Config ที่ field Counts ไม่เคยถูก initialize เลย — zero value ของ map[string]int คือ nil ไม่ใช่ map ว่าง c.Counts["hits"]++ คือการอ่านแล้วเขียน ฝั่งอ่านไม่มีปัญหา (คืน 0 สำหรับ nil map) แต่ฝั่งเขียน panic verify บน 1.26.5 แล้ว: panic: assignment to entry in nil map แก้โดยให้ NewConfig initialize field นี้: return &Config{Counts: make(map[string]int)}