#Map
Huỳnh Gia Mai @huynhmai1 · 1 ngày

[Beginner] Tất tần tật về Map trong Go – Cấu trúc dữ liệu không thể thiếu trong mọi ứng dụng Backend

Tuần trước mình đã làm về #Slice với #Array rồi thì hôm nay mình sẽ tiếp tục với #Map trong Go. Nếu slice là cấu trúc dữ liệu xuất hiện nhiều nhất khi làm việc với danh sách, thì map chính là công cụ được sử dụng nhiều nhất khi cần lưu trữ dữ liệu theo dạng key-value.Trong các dự án Go backend thực tế, map xuất hiện ở rất nhiều nơi:

  • Lưu cache trong memory
  • Mapping ID → Object
  • Parse JSON
  • Lưu configuration
  • Đếm số lượng phần tử
  • Group dữ liệu
  • Implement lookup table

Ví dụ quen thuộc:

users := map[int]string{
1: "Alice",
2: "Bob",
}

Thay vì duyệt qua toàn bộ danh sách để tìm user có ID = 2, map cho phép truy cập trực tiếp bằng key. Nhưng phía sau sự đơn giản đó là một cơ chế khá phức tạp liên quan đến hash table, bucket, memory allocation và concurrency. Trong bài viết này chúng ta sẽ tìm hiểu toàn bộ về map trong Go.Map là gì?Map là một kiểu dữ liệu trong Go dùng để lưu trữ dữ liệu theo cặp:key -> valueVí dụ:

ages := map[string]int{
"Alice": 20,
"Bob": 25,
}

Trong đó:

  • Key: string
  • Value: int

Có thể truy cập:

fmt.Println(ages["Alice"])
// Output: 20

Khai báo MapCách 1: Dùng make()Cách phổ biến:

users := make(map[string]string)

Map lúc này đã được tạo nhưng chưa có dữ liệu. Thêm phần tử:

users["name"] = "John"

Cách 2: Khởi tạo trực tiếp

users := map[string]string{
"name": "John",
"role": "admin",
}

Zero value của MapKhác với slice:

var users []string

slice nil vẫn append được. Nhưng:

var users map[string]string

Map nil không thể ghi dữ liệu.

users["name"] = "John" // sẽ gây panic
// panic: assignment to entry in nil map

Muốn sử dụng phải:

users = make(map[string]string)

Truy cập phần tử trong MapVí dụ:

users := map[string]string{
"name": "John",
}

fmt.Println(users["name"])
//Output: John

Nhưng nếu key không tồn tại:

fmt.Println(users["age"])
// Output:
// Go ko báo lỗi mà trả về zero value: ""
// Điều này dễ gây bug.

Kiểm tra key có tồn tại hay khôngGo cung cấp cách kiểm tra rất đặc biệt:

value, ok := users["age"]

Ví dụ:

users := map[string]string{
"name":"John",
}

name, ok := users["name"]

fmt.Println(name, ok)
// Output: John true

// Nếu key ko tồn tại
age, ok := users["age"]
fmt.Println(ok)
// Output: false

Đây gọi là:

"comma ok idiom"

Một pattern xuất hiện rất nhiều trong Go.Xóa phần tử trong MapDùng delete:

users := map[string]string{
"name":"John",
"role":"admin",
}

delete(users,"role")

Sau đó:

fmt.Println(users)
// Output: map[name:John]

// Nếu xóa key không tồn tại
delete(users,"abc")
// Sẽ ko xảy ra lỗi hay panic

Duyệt MapSử dụng range:

users := map[string]string{
"name":"John",
"role":"admin",
}

for key,value := range users {
fmt.Println(key,value)
}

// Output: name John và role admin
// Tuy nhiên ko phải lần chạy nào thứ tự cũng như trên

Map không có thứ tựBạn không thể đảm bảo output sẽ luôn luôn như sau

// Lần 1:
name John
role admin

// Lần 2:
name John
role admin

Kết quả có thể khác nhau. Go cố tình random thứ tự iteration để tránh việc developer phụ thuộc vào thứ tự của map.Map hoạt động bên trong như thế nào?Bên dưới, map trong Go được implement bằng hash table. Cơ bản map sẽ có cấu trúc như sau:

key → hash function → bucket → value

Ví dụ:

userID := 100

Go sẽ:

  1. Tính hash của key
  2. Xác định bucket chứa dữ liệu
  3. Lưu value vào bucket đó

Khi truy cập:

users[100]

Go không cần duyệt toàn bộ dữ liệu. Nó tính hash lại và đi thẳng tới bucket. Độ phức tạp trung bình: O(1)Key của Map phải là kiểu gì?Không phải mọi kiểu dữ liệu đều có thể làm key.

Key phải comparable.

Ví dụ hợp lệ:

map[string]int

map[int]string

map[[32]byte]string

map[struct{}]bool

Không hợp lệ:

map[[]int]string

Vì slice không comparable. Slice chứa pointer tới vùng nhớ bên ngoài nên không thể so sánh trực tiếp.Map với StructTrong backend, map với struct xuất hiện rất nhiều. Ví dụ:

type User struct {
ID int
Name string
}

users := map[int]User{
1:{
ID:1,
Name:"John",
},
}

Truy cập:

user := users[1]

Map lưu PointerMột cách khác:

users := map[int]*User{}

Lợi ích:

  • Không copy struct
  • Có thể thay đổi object trực tiếp
  • Tiết kiệm memory với struct lớn

Map là Reference Type?Một câu hỏi rất phổ biến:

"Map trong Go có phải reference type không?"

Câu trả lời:

Map không phải reference type theo định nghĩa của Go.
Nhưng hành vi của nó giống reference type.

Ví dụ:

a := map[string]int{
"x":1,
}

b := a

b["x"] = 100

fmt.Println(a)

// Output: map[x:100]

Tại sao? Vì khi copy map:

  • Go chỉ copy map header.
  • Hai biến cùng trỏ tới cùng underlying data.

Truyền Map vào FunctionVí dụ:

func update(data map[string]int){
data["count"] = 100
}

func main(){

numbers := map[string]int{
"count":1,
}

update(numbers)

fmt.Println(numbers)
}

// Output: map[count:100]

Khác với array. Map truyền vào function vẫn có thể thay đổi dữ liệu gốc.Map không an toàn khi dùng với GoroutineMột lỗi rất phổ biến:

counter := map[string]int{}

go func(){
counter["a"]++
}()

go func(){
counter["b"]++
}()

Panic thể xảy ra:

fatal error: concurrent map writes

Map trong Go không hỗ trợ concurrent write.Cách xử lý Concurrent MapDùng MutexVí dụ:

type SafeMap struct {
mu sync.Mutex
data map[string]int
}

Khi ghi:

mu.Lock()

data[key] = value

mu.Unlock()

Dùng sync.MapGo cung cấp:

var m sync.Map

Ví dụ:

m.Store("name","John")

value,_ := m.Load("name")

Nhưng không phải lúc nào sync.Map cũng tốt hơn map + mutex.Thông thường:

  • Logic đơn giản → map + mutex
  • Read nhiều, write ít → sync.Map

Map và MemoryMap có khả năng giữ memory ngay cả khi bạn delete phần tử.Ví dụ:

m := make(map[int]int)

for i:=0;i<100000;i++{
m[i]=i
}

for i:=0;i<100000;i++{
delete(m,i)
}

Map có thể không trả toàn bộ memory lại cho OS ngay lập tức.Nếu cần giải phóng:

m = make(map[int]int)

Preallocate MapNếu biết trước số lượng phần tử:Không nên:

users := make(map[int]string)

for i:=0;i<100000;i++{
users[i]="user"
}

Nên:

users := make(map[int]string,100000)

Go có thể chuẩn bị bucket trước.Lợi ích:

  • Ít resize
  • Ít allocation
  • Tốt hơn về performance

Map vs SliceVí dụ tìm user:Slice:

// Với slice: O(n)

for _,user := range users{
if user.ID == id {
return user
}
}

// Với map: O(1)
users[id]

Nếu cần lookup theo key thường xuyên → Map là lựa chọn tốt hơn.Những lỗi phổ biến khi dùng MapDùng map nilSai:

var m map[string]int

m["a"]=1

Đúng:

m := make(map[string]int)

Không kiểm tra key tồn tạiSai:

user := users[id]
// Có thể nhận zero value.

Đúng:

user, ok := users[id]

Assume thứ tự mapSai:

for _,v := range m {
// assume order
}

Map không đảm bảo order.Dùng map trong nhiều goroutine mà không lock

counter := map[string]int{}

go func(){
counter["a"]++
}()

go func(){
counter["b"]++
}()

Có thể gây crash.Khi nào nên dùng Map?Map phù hợp khi:

  • Cần tìm kiếm nhanh theo key
  • Dữ liệu có quan hệ mapping
  • Không quan tâm thứ tự
  • Cần grouping dữ liệu

Ví dụ:

userID -> User

email -> Account

country -> []User

Kết luậnMap là một trong những cấu trúc dữ liệu quan trọng nhất trong Go. Bề ngoài nó chỉ đơn giản:

map[key]value

Nhưng bên dưới là cả một hệ thống:

  • Hash function
  • Bucket
  • Allocation
  • Collision handling
  • Memory management

Hiểu rõ map giúp bạn:

  • Viết code nhanh hơn
  • Tránh bug về nil map
  • Tránh lỗi concurrency
  • Tối ưu performance trong backend

Nếu array là nền móng, slice là công cụ hằng ngày thì map chính là "vũ khí" giúp Go xử lý dữ liệu dạng lookup cực kỳ hiệu quả.
#Array #concurrency

Xem thêm
Thị Trường

Chia sẻ bài viết