Cách khắc phục lỗi 'cannot take the address of' với Map trong Go

intermediate🔷 Go2026-07-24| Go (Golang) mọi phiên bản

Error Message

cannot take the address of m["key"]
#con-tro#maps#quan-ly-bo-nho#golang

Vấn đề

Đây là một tình huống phổ biến: bạn có một struct được lưu trữ trong một map và cần truyền nó vào một hàm để sửa đổi dữ liệu thông qua con trỏ. Bạn có thể thử sử dụng toán tử lấy địa chỉ (&) trực tiếp trên phần tử của map. Điều này nghe có vẻ hợp lý, nhưng trình biên dịch sẽ không cho phép.

type User struct {
    Name string
    Age  int
}

m := map[string]User{
    "id123": {Name: "Alice", Age: 30},
}

// Dòng này sẽ gây ra lỗi biên dịch
updateUser(&m["id123"])

Trình biên dịch sẽ ngăn bạn ngay lập tức với một thông báo cụ thể:

./main.go:15:13: cannot take the address of m["id123"]

Bạn cũng sẽ gặp phải rào cản tương tự khi cố gắng lấy địa chỉ của giá trị trả về từ một hàm, chẳng hạn như &getSomeValue(). Cả hai đều thất bại vì cùng một lý do cốt lõi: khả năng lấy địa chỉ (addressability).

Tại sao Go chặn điều này

Các giá trị trong map của Go không thể lấy địa chỉ (addressable) vì map là các thực thể động. Bên dưới bề mặt, Go lưu trữ dữ liệu map trong các bucket. Khi một map phát triển—cụ thể là khi nó đạt đến hệ số tải (load factor) khoảng 6.5—nó sẽ kích hoạt quá trình "rehash". Trong quá trình này, Go cấp phát một tập hợp các bucket mới và chuyển các phần tử hiện có sang các vị trí bộ nhớ mới.

Nếu Go cho phép bạn lấy địa chỉ của một giá trị trong map, bạn sẽ giữ một con trỏ tới một ô nhớ cụ thể. Ngay khi map mở rộng và dọn sạch bucket đó, con trỏ của bạn sẽ trỏ đến dữ liệu cũ, không hợp lệ. Điều này dẫn đến các lỗi không xác định cực kỳ khó truy vết. Để đảm bảo an toàn bộ nhớ, đội ngũ phát triển Go đã thiết kế các phần tử map không thể lấy địa chỉ.

Kết quả của hàm bị hạn chế vì một lý do khác. Một giá trị trả về thường là một "rvalue" tạm thời nằm trong thanh ghi CPU hoặc một vị trí ngăn xếp (stack) nhất thời. Việc lấy địa chỉ của nó rất rủi ro vì vùng lưu trữ cho giá trị đó có thể biến mất ngay khi dòng mã thực thi xong.

Cách khắc phục

Giải pháp 1: Sử dụng biến tạm

Cách khắc phục nhanh nhất là chuyển giá trị của map vào một biến cục bộ. Các biến trên stack thì ổn định và có thể lấy địa chỉ.

// 1. Trích xuất giá trị ra một biến cục bộ
user := m["id123"]

// 2. Truyền địa chỉ của biến
updateUser(&user)

// 3. Ghi các thay đổi ngược lại vào map
m["id123"] = user

Cách tiếp cận này an toàn nhưng liên quan đến việc sao chép hai lần. Nếu struct của bạn rất lớn—ví dụ, một khối dữ liệu 10KB—việc thực hiện việc này hàng nghìn lần mỗi giây có thể ảnh hưởng đến hiệu suất.

Giải pháp 2: Sử dụng Map chứa con trỏ

Nếu bạn cần sửa đổi các giá trị trong map thường xuyên, hãy thay đổi định nghĩa map của bạn để lưu trữ các con trỏ thay vì các struct thô. Đây là tiêu chuẩn trong ngành đối với các tập hợp có thể thay đổi trong Go.

// Thay đổi map[string]User thành map[string]*User
m := map[string]*User{
    "id123": &User{Name: "Alice", Age: 30},
}

// Truy cập con trỏ trực tiếp mà không cần toán tử &
val := m["id123"]
updateUser(val)

Khi map thực hiện rehash, nó sẽ di chuyển các con trỏ. Struct User thực tế vẫn nằm nguyên tại vị trí cũ trong bộ nhớ. Điều này loại bỏ nhu cầu sao chép và vượt qua hoàn toàn các vấn đề về khả năng lấy địa chỉ.

Giải pháp 3: Xử lý kết quả của hàm

Đối với các kết quả hàm như &foo.GetStatus(), cách khắc phục cũng tương tự: gán kết quả cho một biến trước để cung cấp cho nó một "ngôi nhà" vĩnh viễn trên stack.

// Thất bại: &someFunction()

// Hoạt động:
val := someFunction()
result := &val

Kiểm tra

Chạy go build. Nếu lỗi biến mất, trình biên dịch đã hài lòng. Tuy nhiên, nếu bạn chọn Giải pháp 1, hãy kiểm tra lại logic của mình. Một sai lầm thường gặp là cập nhật biến cục bộ nhưng quên gán lại nó vào map. Nếu không có bước gán cuối cùng đó, map của bạn vẫn sẽ không thay đổi.

temp := m["key"]
temp.Age = 31
// Nếu không có m["key"] = temp, map vẫn lưu trữ Age: 30
m["key"] = temp

Kết luận

- **Map dễ biến động:** Các phần tử di chuyển trong quá trình mở rộng, vì vậy địa chỉ bộ nhớ của chúng không bao giờ là vĩnh viễn.
- **An toàn là trên hết:** Các quy tắc về khả năng lấy địa chỉ của Go tồn tại để ngăn chặn các con trỏ lơ lửng (dangling pointers) và lỗi hỏng bộ nhớ.
- **Chọn loại map phù hợp:** Sử dụng `map[K]V` cho dữ liệu nhỏ, chủ yếu để đọc. Sử dụng `map[K]*V` khi bạn cần cập nhật các trường hoặc xử lý các struct lớn một cách hiệu quả.

Related Error Notes