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ẽ:
- Tính hash của key
- Xác định bucket chứa dữ liệu
- 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


Bình luận
Đăng nhập để tham gia thảo luận.
Chưa có bình luận nào. Hãy là người đầu tiên chia sẻ góc nhìn — bình luận hiển thị sau khi được duyệt.