ข้ามไปยังเนื้อหา

Maps

Map คือ hash table ที่ map key ชนิด K ไปยัง value ชนิด V สร้างด้วย make หรือ composite literal:

// make — preferred when you add entries incrementally
scores := make(map[string]int)
// composite literal — preferred when entries are known upfront
scores := 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 ได้

scores["Charlie"] = 72 // set
fmt.Println(scores["Alice"]) // get → 95
delete(scores, "Bob") // delete (no-op if key absent)

การอ่าน key ที่ไม่มีอยู่ ไม่ panic เลย — จะคืน zero value ของ value type:

fmt.Println(scores["Dave"]) // 0 — not a panic

แม้จะสะดวก แต่ก็ซ่อนบั๊กได้: คุณไม่รู้ว่า key map ไปที่ 0 หรือไม่เคยถูก set ใช้ comma-ok idiom เพื่อแยกแยะ

การ lookup map แบบสอง result คืนค่าและ boolean ที่บอกว่า key มีอยู่หรือไม่:

v, ok := scores["Dave"]
if !ok {
fmt.Println("Dave not found")
}

Zero value ของ map type คือ nil — ไม่ใช่ map ว่างที่พร้อมใช้งาน nil map ทำตัวเหมือน map ว่างสำหรับ operation อ่าน ทุกแบบ แต่จะ panic ตอนเขียน:

var m map[string]int
fmt.Println(m["a"], len(m)) // 0 0 — reading a nil map is completely safe
m["a"] = 1 // panic: assignment to entry in nil map

verify บน 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

การ assign map จะ copy แค่ header ไม่ใช่ข้อมูล ตัวแปรทั้งสองชี้ไปยัง hash table เดียวกัน:

a := map[string]int{"x": 1}
b := a
b["x"] = 99
fmt.Println(a["x"]) // 99 — a and b share the same map

เพราะ 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 directly

Map ของ Go ไม่ ปลอดภัยสำหรับการเข้าถึงพร้อมกัน goroutine สองตัวที่เขียน map เดียวกันพร้อมกันจะไม่ race กันแบบเงียบ ๆ — runtime ตรวจจับได้และ crash ทั้งโปรเซสทันที และ crash แบบนี้ recover() จับไม่ได้:

m := make(map[int]int)
var wg sync.WaitGroup
for 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)

เอา 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 error

verify บน 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 copy
p.X = 5
m["origin"] = p // store it back
// or: map[string]*Point avoids the copy round-trip entirely
m2 := map[string]*Point{"origin": {0, 0}}
m2["origin"].X = 5 // fine — *Point is addressable through the pointer already stored in the map

การลบ key ปัจจุบันระหว่าง range เป็นเรื่องปลอดภัยและถูกกำหนดไว้ชัดเจน — key ที่ถูกลบจะไม่ถูกส่งออกมาใน iteration ถัดไปเลย แต่การเพิ่ม key ใหม่ระหว่าง range เป็นอีกเรื่อง — spec ของภาษาปล่อยให้ ไม่ระบุ (unspecified) ว่า key ที่เพิ่งเพิ่มเข้าไปจะถูก visit ใน loop เดียวกันนั้นหรือไม่ อย่าเขียน loop ที่ความถูกต้องขึ้นอยู่กับการเห็น (หรือไม่เห็น) key ที่เพิ่ง insert เข้าไปเอง

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

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 ที่เขียนเอง

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))
}
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"]
Go map hash key ไปยัง bucket; key ที่ชนกันจะ probe ไป bucket ถัดไป

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 mapO(1) average lookup, ใช้งานง่ายไม่ thread-safe — ต้องใช้ mutex หรือ sync.Map
zero value for missing keyไม่ panic เมื่ออ่าน key ที่ไม่มีซ่อน bug ถ้าลืมใช้ comma-ok idiom
dynamic sizingmap ขยายอัตโนมัติ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 ใหม่ถ้าต้องการ shrink
  • len(m) slow เหมือน iterate — len(map) เป็น O(1) Go track count ไว้ใน map header

💡 ตัวอย่างจากของจริง

http.Header คือ map[string][]string ธรรมดา — request แต่ละตัวมี Header ของตัวเอง จึงไม่ต้อง lock

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

คุณอ่าน scores["Dave"] แต่ไม่เคยเพิ่ม "Dave" เข้าไป Go คืนค่าอะไร?
ค่า return ที่สองใน v, ok := scores["Alice"] บอกอะไร?
โค้ดนี้ print อะไร? var m map[string]int fmt.Println(m["a"]) m["a"] = 1
goroutine ยี่สิบตัวเรียก m[n] = j บน map[int]int เดียวกันโดยไม่มี locking เลย เกิดอะไรขึ้นจริงตอนรัน?
มี type Point struct{ X, Y int } และ m := map[string]Point{"o": {0, 0}} ทำไม m["o"].X = 5 ถึง compile ไม่ผ่าน?