Skip to content

แมปใน Go

โดยทั่วไปแล้ว การดำเนินการโครงสร้างข้อมูลแมปมักมีสองแบบ คือ ตารางแฮช (hash table) และต้นไม้ค้นหา (search tree) ความแตกต่างคือแบบแรกไม่มีลำดับ แบบหลังมีลำดับ ใน Go การดำเนินการของ map อิงตามถังแฮช (ซึ่งก็เป็นตารางแฮชชนิดหนึ่ง) ดังนั้นจึงไม่มีลำดับ บทนี้จะไม่กล่าวถึงหลักการดำเนินการมากเกินไป ซึ่งเกินขอบเขตของพื้นฐาน ต่อไปจะ进行深入分析

TIP

หากต้องการเข้าใจหลักการของ map สามารถไปที่ การดำเนินการ map

การเริ่มต้น

ใน Go คีย์ของ map ต้องเปรียบเทียบได้ เช่น string, int เปรียบเทียบได้ ส่วน []int เปรียบเทียบไม่ได้ จึงไม่สามารถเป็นคีย์ของ map ได้ มีสองวิธีในการเริ่มต้น map วิธีแรกคือค่าตัวอักษร รูปแบบดังนี้

go
map[keyType]valueType{}

ยกตัวอย่างบางตัวอย่าง

go
mp := map[int]string{
   0: "a",
   1: "a",
   2: "a",
   3: "a",
   4: "a",
}

mp := map[string]int{
   "a": 0,
   "b": 22,
   "c": 33,
}

วิธีที่สองคือใช้ฟังก์ชันในตัว make สำหรับ map รับพารามิเตอร์สองตัว คือ ประเภทและความจุเริ่มต้น ตัวอย่างดังนี้

go
mp := make(map[string]int, 8)

mp := make(map[string][]int, 10)

map เป็นประเภทอ้างอิง ค่าศูนย์หรือ map ที่ยังไม่เริ่มต้นสามารถเข้าถึงได้ แต่ไม่สามารถเก็บองค์ประกอบได้ จึงต้องจัดสรรหน่วยความจำให้มัน

go
func main() {
   var mp map[string]int
   mp["a"] = 1
   fmt.Println(mp)
}
panic: assignment to entry in nil map

TIP

เมื่อเริ่มต้น map ควรจัดสรรความจุที่เหมาะสมให้มากที่สุด เพื่อลดจำนวนครั้งในการขยายขนาด

การเข้าถึง

วิธีการเข้าถึง map เหมือนกับการเข้าถึงอาร์เรย์ผ่านดัชนี

go
func main() {
  mp := map[string]int{
    "a": 0,
    "b": 1,
    "c": 2,
    "d": 3,
  }
  fmt.Println(mp["a"])
  fmt.Println(mp["b"])
  fmt.Println(mp["d"])
  fmt.Println(mp["f"])
}
0
1
3
0

จากโค้ดจะเห็นว่าแม้ว่าใน map ไม่มีคู่คีย์-ค่า "f" แต่ก็ยังมีค่าส่งคืนอยู่ สำหรับคีย์ที่ไม่มีใน map ค่าส่งคืนคือค่าศูนย์ของประเภทนั้น และที่จริงแล้วมีค่าส่งคืนสองค่าเมื่อเข้าถึง map ค่าส่งคืนแรกคือค่าของประเภทนั้น ค่าส่งคืนที่สองคือค่าบูลีน แสดงว่าคีย์มีอยู่หรือไม่ เช่น

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   if val, exist := mp["f"]; exist {
      fmt.Println(val)
   } else {
      fmt.Println("key ไม่มีอยู่")
   }
}

หาความยาวของ map

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(len(mp))
}

การเก็บค่า

วิธีการเก็บค่าของ map ก็คล้ายกับการเก็บค่าของอาร์เรย์ เช่น

go
func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   fmt.Println(mp)
}

เมื่อเก็บค่า การใช้คีย์ที่มีอยู่จะทับค่าเดิม

go
func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   if _, exist := mp["b"]; exist {
      mp["b"] = 3
   }
   fmt.Println(mp)
}

แต่ก็มีกรณีพิเศษ นั่นคือเมื่อคีย์เป็น math.NaN()

go
func main() {
  mp := make(map[float64]string, 10)
  mp[math.NaN()] = "a"
  mp[math.NaN()] = "b"
  mp[math.NaN()] = "c"
  _, exist := mp[math.NaN()]
  fmt.Println(exist)
  fmt.Println(mp)
}
false
map[NaN:c NaN:a NaN:b]

จากผลลัพธ์จะเห็นว่าคีย์ที่เหมือนกันไม่ได้ทับกัน กลับกันยังมีอยู่หลายตัวได้ และไม่สามารถตัดสินว่ามีอยู่หรือไม่ จึงไม่สามารถดึงค่าได้ตามปกติ เนื่องจาก NaN เป็นนิยามตามมาตรฐาน IEE754 การดำเนินการของมันเสร็จสิ้นโดยคำสั่งแอสเซมบลีระดับล่าง UCOMISD ซึ่งเป็นคำสั่งเปรียบเทียบจำนวนจริงความแม่นยำคู่แบบไม่มีลำดับ คำสั่งนี้จะพิจารณากรณีของ NaN ดังนั้นผลลัพธ์คือตัวเลขใดๆ ก็ไม่เท่ากับ NaN, NaN ก็ไม่เท่ากับตัวเองด้วย ซึ่งทำให้ค่าแฮช每次都ต่างกัน เกี่ยวกับ这一点ชุมชนเคยอภิปรายกันอย่างดุเดือด แต่ทางการคิดว่าไม่จำเป็นต้องแก้ไข ดังนั้นควรหลีกเลี่ยงการใช้ NaN เป็นคีย์ของ map ให้มากที่สุด

การลบ

go
func delete(m map[Type]Type1, key Type)

การลบคู่คีย์-ค่าต้องใช้ฟังก์ชันในตัว delete เช่น

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(mp)
   delete(mp, "a")
   fmt.Println(mp)
}
map[a:0 b:1 c:2 d:3]
map[b:1 c:2 d:3]

สิ่งที่ควรระวังคือ หากค่าเป็น NaN แม้แต่การลบคู่คีย์-ค่านั้นก็ทำไม่ได้

go
func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   fmt.Println(mp)
   delete(mp, math.NaN())
   fmt.Println(mp)
}
map[NaN:c NaN:a NaN:b]
map[NaN:c NaN:a NaN:b]

การวนซ้ำ

ใช้ for range สามารถวนซ้ำ map ได้ เช่น

go
func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
c 2
d 3
a 0
b 1

จะเห็นว่าผลลัพธ์ไม่มีลำดับ ซึ่งยืนยันว่า map เก็บข้อมูลแบบไม่มีลำดับ ที่น่ากล่าวถึงคือ แม้ NaN จะไม่สามารถดึงค่าได้ตามปกติ แต่สามารถเข้าถึงได้ผ่านการวนซ้ำ เช่น

go
func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
NaN a
NaN c
NaN b

การล้าง

ก่อน go1.21 หากต้องการล้าง map ได้เพียง delete แต่ละคีย์ของ map เท่านั้น

go
func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  for k, _ := range m {
    delete(m, k)
  }
  fmt.Println(m)
}

แต่ go1.21 อัปเดตฟังก์ชัน clear แล้ว ไม่ต้องดำเนินการแบบก่อนหน้าอีกต่อไป เพียงใช้ clear หนึ่งครั้งก็สามารถล้างได้

go
func main() {
  m := map[string]int{
    "a": 1,
    "b": 2,
  }
  clear(m)
  fmt.Println(m)
}

เอาต์พุต

map[]

Set

Set เป็นชุดที่ไม่มีลำดับและไม่ประกอบด้วยองค์ประกอบซ้ำ Go ไม่มีโครงสร้างข้อมูลที่คล้ายกันให้ แต่คีย์ของ map นั้นไม่มีลำดับและไม่ซ้ำกัน ดังนั้นสามารถใช้ map แทน set ได้

go
func main() {
  set := make(map[int]struct{}, 10)
  for i := 0; i < 10; i++ {
    set[rand.Intn(100)] = struct{}{}
  }
  fmt.Println(set)
}
map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]

TIP

โครงสร้าง体がว่างเปล่าไม่กินหน่วยความจำ

ข้อควรระวัง

map ไม่ใช่โครงสร้างข้อมูลที่ปลอดภัยต่อการทำงานพร้อมกัน ทีม Go คิดว่าในสถานการณ์ส่วนใหญ่ การใช้ map ไม่เกี่ยวข้องกับสถานการณ์ความพร้อมสูง การเพิ่มมูเท็กซ์จะลดประสิทธิภาพอย่างมาก ภายใน map มีกลไกตรวจสอบการอ่านและเขียน หากขัดแย้งจะ触发 fatal error เช่น สถานการณ์ต่อไปนี้มีความเป็นไปได้สูงที่จะ触发 fatal

go
func main() {

   group.Add(10)
   // map
   mp := make(map[string]int, 10)
   for i := 0; i < 10; i++ {
      go func() {
         // การเขียน
         for i := 0; i < 100; i++ {
            mp["helloworld"] = 1
         }
         // การอ่าน
         for i := 0; i < 10; i++ {
            fmt.Println(mp["helloworld"])
         }
         group.Done()
      }()
   }
   group.Wait()
}
fatal error: concurrent map writes

ในสถานการณ์เช่นนี้ ต้องใช้ sync.Map แทน

Golang by www.golangdev.cn edit