1 การแนะนำเกี่ยวกับ Map

ใน Go language, map คือประเภทข้อมูลพิเศษที่สามารถเก็บคู่ค่าของคีย์และค่าที่มีประเภทต่าง ๆ นี่คล้ายกับ dictionary ใน Python หรือ HashMap ใน Java ใน Go, map คือประเภทที่สร้างมาให้ในตัวและใช้ hash table เพื่อให้มีลักษณะการค้นหาข้อมูล, การอัพเดต, และการลบข้อมูลอย่างรวดเร็ว

คุณสมบัติ

  • ประเภทการอ้างอิง: map เป็นประเภทการอ้างอิง, ซึ่งหมายความว่าหลังจากสร้างมันจริง ๆ แล้วมันจะได้รับพอยน์เตอร์ไปยังโครงสร้างข้อมูลฐาน
  • การขยายได้: คล้ายกับ slices, พื้นที่ของ map ไม่ได้เป็นคงที่และขยายโดยอัตโนมัติเมื่อข้อมูลเพิ่มขึ้น
  • ความเป็นเอกลักษณ์ของคีย์: แต่ละคีย์ใน map เป็นเอกลักษณ์, และหากใช้คีย์เดียวกันเพื่อเก็บค่า ค่าใหม่จะทับค่าเดิม
  • คอลเลคชันที่ไม่เรียงลำดับ: สมาชิกใน map ไม่ได้เรียงลำดับดังนั้นลำดับของคู่ค่าคีย์-ค่าอาจแตกต่างกันทุกครั้งที่ผ่าน map

การใช้งาน

  • สถิติ: นับองค์ประกอบที่ไม่ซ้ำๆ แบบรวดเร็วโดยใช้ความเป็นเอกลักษณ์ของคีย์
  • แคช: กลไกคู่ค่าคีย์เหมาะสำหรับการนำมาใช้เพื่อแคช
  • สระว่างการเชื่อมต่อกับฐานข้อมูล: จัดการชุดของทรัพยากร เช่น การเชื่อมต่อฐานข้อมูล ทำให้ทรัพยากรสามารถถูกแบ่งปันและเข้าถึงโดยลูกค้าหลาย ๆ
  • การจัดเก็บรายการกำหนดค่า: ใช้เพื่อเก็บพารามิเตอร์จากแฟ้มกำหนดค่า

2 การสร้าง Map

2.1 การสร้างด้วยฟังก์ชัน make

วิธีที่สม่ำเสมอที่สุดในการสร้าง map คือโดยใช้ฟังก์ชัน make ด้วยรูปแบบต่อไปนี้:

make(map[keyType]valueType)

ที่นี่ keyType คือประเภทของคีย์ และ valueType คือประเภทของค่า นี่คือตัวอย่างการใช้:

// สร้าง map พร้อมกับประเภทของคีย์เป็น string และประเภทของค่าเป็น integer
m := make(map[string]int)

ในตัวอย่างนี้, เราสร้าง map ว่างที่ใช้เพื่อเก็บคู่ค่าของคีย์เป็น string และค่าเป็น integer.

2.2 การสร้างด้วย Syntax ลอริทัล

นอกจากการใช้ make, เรายังสามารถสร้างและกำหนดค่าให้ map โดยใช้ syntax ลอริทัล ซึ่งประกาศชุดคู่ค่าของคีย์-ค่าพร้อมกัน:

m := map[string]int{
    "apple": 5,
    "pear":  6,
    "banana": 3,
}

สิ่งนี้ไม่ได้สร้างเพียงแต่ map แต่ยังกำหนดค่าให้กับ map ด้วย

2.3 ข้อคิดสำหรับการกำหนดค่าให้ Map

เมื่อใช้ map, สำคัญที่ต้องทราบคือค่าศูนย์ของ map ที่ไม่ได้กำหนดค่าเริ่มต้นคือ nil, และคุณไม่สามารถเก็บคู่ค่าใดๆ ในมันโดยตรงในจุดนี้ หรือจะทำให้เกิดภาวะระบบถ่วงนี้ คุณต้องใช้ make เพื่อกำหนดค่าเริ่มต้นโดยตรง:

var m map[string]int
if m == nil {
    m = make(map[string]int)
}
// ตอนนี้เป็นปลอดภัยที่จะใช้ m

คือค่าที่เป็นสำคัญมากมายคือมีsyntax พิเศษสำหรับการตรวจสอบว่าคีย์มีอยู่ใน map หรือไม่:

value, ok := m["key"]
if !ok {
    // "key" ไม่ได้อยู่ใน map
}

ที่นี่, value คือค่าที่เกี่ยวข้องกับคีย์ที่กำหนด, และ ok คือค่า boolean ที่จะเป็น true ถ้าคีย์มีอยู่ใน map และ false ถ้าไม่มี.

3.2 การตรวจสอบความมีอยู่ของคีย์

บางครั้งเราอาจต้องการทราบเพียงแค่ว่าคีย์มีอยู่ในแผนที่หรือไม่โดยไม่สนใจค่าที่สอดคล้องกัน ในกรณีนี้ คุณสามารถใช้ค่าคืนของการเข้าถึงแผนที่ที่สอง ค่าคืนค่าตรรกะนี้จะบอกเราว่าคีย์มีอยู่ในแผนที่หรือไม่

func main() {
    scores := map[string]int{
        "Alice": 92,
        "Bob": 85,
    }

    // ตรวจสอบว่าคีย์ "Bob" มีอยู่หรือไม่
    score, exists := scores["Bob"]
    if exists {
        fmt.Println("คะแนนของ Bob:", score)
    } else {
        fmt.Println("ไม่พบคะแนนของ Bob")
    }

    // ตรวจสอบว่าคีย์ "Charlie" มีอยู่หรือไม่
    _, exists = scores["Charlie"]
    if exists {
        fmt.Println("พบคะแนนของ Charlie")
    } else {
        fmt.Println("ไม่พบคะแนนของ Charlie")
    }
}

ในตัวอย่างนี้ เราใช้อัตราการเงื่อนไขเพื่อตรวจสอบค่าตรรกะเพื่อกำหนดว่าคีย์มีอยู่หรือไม่

3.3 เพิ่มและอัปเดตองค์เมนต์

การเพิ่มองค์เมนต์ใหม่ในแผนที่และการอัปเดตองค์เมนต์ที่มีอยู่ทั้งหมดใช้อัตราการสร้างเดียวกัน หากคีย์มีอยู่แล้วค่าเดิมจะถูกแทนที่ด้วยค่าใหม่ หากคีย์ไม่มีอยู่ คีย์-ค่าคู่ใหม่จะถูกเพิ่ม

func main() {
    // กำหนดแผนที่เปล่า
    scores := make(map[string]int)

    // เพิ่มองค์เมนต์
    scores["Alice"] = 92
    scores["Bob"] = 85

    // อัปเดตองค์เมนต์
    scores["Alice"] = 96  // อัปเดตคีย์ที่มีอยู่
    
    // พิมพ์แผนที่
    fmt.Println(scores)   // ผลลัพธ์: map[Alice:96 Bob:85]
}

การเพิ่มและการอัปเดตเป็นการดำเนินการที่กระชับและสามารถทำได้ด้วยการกำหนดค่าอย่างง่าย

3.4 การลบองค์เมนต์

การลบองค์เมนต์จากแผนที่สามารถทำได้โดยใช้ฟังก์ชันที่มีอยู่ชื่อ delete ต่อไปนี้เป็นตัวอย่างการดำเนินการลบ:

func main() {
    scores := map[string]int{
        "Alice": 92,
        "Bob": 85,
        "Charlie": 78,
    }

    // ลบองค์เมนต์
    delete(scores, "Charlie")

    // พิมพ์แผนที่เพื่อให้แน่ใจว่า Charlie ถูกลบ
    fmt.Println(scores)  // ผลลัพธ์: map[Alice:92 Bob:85]
}

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

4 การวิ่งดูแผนที่

ในภาษา Go คุณสามารถใช้คำสั่ง for range เพื่อวิ่งดูโครงสร้างข้อมูลแผนที่และเข้าถึงแต่ละคีย์-ค่าในคอนเทนเนอร์ การดำเนินการวิ่งหมุนประเภทนี้เป็นการดำเนินการพื้นฐานที่รองรับโดยโครงสร้างข้อมูลแผนที่

4.1 การใช้ for range เพื่อวนรอบแผนที่

คำสั่ง for range สามารถใช้งานได้โดยตรงบนแผนที่เพื่อเรียกดูแต่ละคีย์-ค่าในแผนที่ ต่อไปนี้คือตัวอย่างพื้นฐานของการใช้ for range เพื่อวนรอบแผนที่:

package main

import "fmt"

func main() {
    myMap := map[string]int{"Alice": 23, "Bob": 25, "Charlie": 28}

    for key, value := range myMap {
        fmt.Printf("คีย์: %s, ค่า: %d\n", key, value)
    }
}

ในตัวอย่างนี้ ตัวแปร key ถูกกำหนดค่าเป็นคีย์ของการวิ่งรอบปัจจุบัน และตัวแปร value ถูกกำหนดค่าเป็นค่าที่เกี่ยวข้องกับคีย์นั้น

4.2 ข้อคิดสำหรับลำดับการวนรอบ

สำคัญที่จะทราบว่าเมื่อวนรอบบนแผนที่ ลำดับการวนรอบไม่มีการรับประกันว่าจะเหมือนทุกครั้ง แม้แต่ถ้าเนื้อหาของแผนที่ไม่ได้เปลี่ยนแปลง สาเหตุคือกระบวนการของการวนรอบบนแผนที่ใน Go ถูกออกแบบให้เป็นการสุ่มเพื่อป้องกันโปรแกรมจากการพึงพอใจที่วนรอบที่แน่นอน ซึ่งทำให้ระบบความแข็งแกร่งของโค้ดดียิ่งขึ้น

เช่น การรันโค้ดต่อไปนี้สองครั้งทำให้ได้ผลลัพธ์ที่แตกต่างกัน:

package main

import "fmt"

func main() {
    myMap := map[string]int{"Alice": 23, "Bob": 25, "Charlie": 28}

    fmt.Println("การวนรอบครั้งแรก:")
    for key, value := range myMap {
        fmt.Printf("คีย์: %s, ค่า: %d\n", key, value)
    }

    fmt.Println("\nการวนรอบครั้งที่สอง:")
    for key, value := range myMap {
        fmt.Printf("คีย์: %s, ค่า: %d\n", key, value)
    }
}

5 หัวข้อขั้นสูงเกี่ยวกับ Maps

ต่อมาเราจะลึกซึ้งเข้าไปในหัวข้อหลายอย่างที่เกี่ยวข้องกับแผนที่ ซึ่งสามารถช่วยให้คุณเข้าใจและใช้ประโยชน์จากแผนที่ได้มากขึ้น

5.1 คุณลักษณะของหน่วยความจำและประสิทธิภาพของแผนที่

ใน Go language แผนที่เป็นชนิดข้อมูลที่มีความยืดหยุ่นและมีพลวัตเฉพาะที่เกี่ยวกับการใช้พื้นที่หน่วยความจำและประสิทธิภาพ ตัวอย่างเช่น ขนาดของแผนที่สามารถเติบโตได้โดยอัตโนมัติ และเมื่อจำนวนสิ่งที่เก็บเกินแล้ว แผนที่จะทำการจัดสร้างพื้นที่จัดเก็บขนาดใหม่เพื่อรองรับความต้องการที่เพิ่มขึ้น

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

myMap := make(map[string]int, 100)

นี้สามารถลดความหนักของการขยายแผนที่ในระหว่างการทำงานได้

5.2 คุณลักษณะของประเภทการอ้างอิงของแผนที่

แผนที่เป็นประเภทการอ้างอิง ซึ่งหมายความว่าเมื่อคุณกำหนดแผนที่ให้กับตัวแปรใหม่ ตัวแปรใหม่จะมีการอ้างอิงไปที่โครงสร้างข้อมูลเดียวกันกับแผนที่เดิม นี้หมายความว่าหากคุณทำการเปลี่ยนแปลงแผนที่ผ่านตัวแปรใหม่ การเปลี่ยนแปลงเหล่านี้จะสะท้อนในตัวแปรแผนที่เดิม

ตัวอย่าง:

package main

import "fmt"

func main() {
    originalMap := map[string]int{"Alice": 23, "Bob": 25}
    newMap := originalMap

    newMap["Charlie"] = 28

    fmt.Println(originalMap) // ผลลัพธ์จะแสดงคู่มีคีย์-ค่า "Charlie": 28 ที่เพิ่มใหม่
}

เมื่อส่งแผนที่เป็นพารามิเตอร์ในการเรียกใช้ฟังก์ชัน ก็สำคัญที่โปรดทราบถึงพฤติกรรมประเภทการอ้างอิง ณ จุดนี้ สิ่งที่ถูกส่งไปคือการอ้างอิงไปยังแผนที่ ไม่ใช่สำเนา

5.3 ความปลอดภัยของความเป็นเส้นเขตและ sync.Map

เมื่อใช้แผนที่ในโอกาสซ้อนทางเทรด จำเป็นต้องให้ความสนใจเป็นพิเศษต่อปัญหาความปลอดภัยของเส้นเขต ในสถานการณ์ที่เป็นสาย ประเภทของมาพื้นที่ใน Go อาจเป็นตำแหน่งสูงสูในอัตราการไล่สาย หากไม่มีการทำซิงโครไนซ์ที่เหมาะสม

ไลบรารีมาตรฐานของ Go ให้ sync.Map ซึ่งเป็นประเภทแผนที่ที่ปลอดภัยสำหรับสภาพแวดล้อมที่เป็นสาย sync.Map นี้มีเมทอดพื้นฐาน เช่น Load, Store, LoadOrStore, Delete, และ Range สำหรับดำเนินการบนแผนที่

ต่อไปคือตัวอย่างการใช้ sync.Map:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mySyncMap sync.Map

    // เก็บคู่คีย์-ค่า
    mySyncMap.Store("Alice", 23)
    mySyncMap.Store("Bob", 25)

    // ดึงข้อมูลและพิมพ์ออกคู่คีย์-ค่า
    if value, ok := mySyncMap.Load("Alice"); ok {
        fmt.Printf("คีย์: Alice, ค่า: %d\n", value)
    }

    // ใช้เมทอด Range เพื่อวนลูปผ่าน sync.Map
    mySyncMap.Range(func(key, value interface{}) bool {
        fmt.Printf("คีย์: %v, ค่า: %v\n", key, value)
        return true // ดำเนินการต่อไปในการวนลูป
    })
}

การใช้ sync.Map แทนแผนที่ปกติสามารถหลีกเลี่ยงปัญหาการแข่งขันของสายเมื่อทำการแก้ไขแผนที่ในสภาพแวดล้อมที่เป็นสาย ซึ่งจะทำให้มั่นใจได้ในเรื่องความปลอดภัยของเส้นเขต