Menu

Map trong Golang: tạo, kiểm tra khóa, duyệt, sắp xếp và xóa

Map trong Go lưu các cặp khóa và giá trị, tra cứu rất nhanh. Tìm hiểu cách tạo map, kiểm tra khóa có tồn tại hay không bằng comma-ok, xóa, duyệt (theo thứ tự ngẫu nhiên), sắp xếp khóa, lưu struct, và tránh panic do map nil hay ghi đồng thời.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Tạo và dùng map

Kiểu map được viết là map[KeyType]ValueType. Tạo map bằng literal hoặc bằng make, rồi đọc, ghi và xóa theo khóa.

Đầu ra:

31
3
map[bob:26 cy:40]
2 0

Ví dụ trên có hai điểm tiện lợi. Đọc một khóa không tồn tại trả về giá trị zero của kiểu giá trị (counts['z'] là 0), nên đếm bằng m[k]++ chạy được mà không cần chuẩn bị gì. Và fmt in map với các khóa đã sắp xếp, rất tiện khi gỡ lỗi nhưng không nói lên gì về thứ tự duyệt.

make(map[K]V, n) nhận thêm một gợi ý kích thước (không bắt buộc). Nó cấp phát trước chỗ cho khoảng n phần tử; khác với slice, map không có dung lượng (capacity) để bạn đọc lại.

Kiểm tra khóa có tồn tại: comma-ok

Vì khóa không tồn tại được đọc ra là giá trị zero, m[k] == 0 không phân biệt được "không có" với "đang lưu 0". Hãy dùng dạng trả về hai giá trị:

Cấu trúc if v, ok := m[k]; ok { ... } giữ v và ok trong phạm vi của câu lệnh if. Đây là một trong những dòng code phổ biến nhất trong Go.

Xóa phần tử

delete(m, key) xóa phần tử tương ứng. Xóa một khóa không tồn tại sẽ không làm gì cả, xóa trên map nil cũng vậy. Để làm rỗng cả map, Go 1.21 bổ sung clear(m), hàm này giữ lại vùng nhớ đã cấp phát để map được dùng lại.

Xóa phần tử trong lúc range trên chính map đó là được phép và an toàn. Phần tử bị xóa trước khi vòng lặp đi tới sẽ không xuất hiện nữa.

Duyệt map: thứ tự là ngẫu nhiên

for k, v := range m đi qua mỗi phần tử đúng một lần, theo thứ tự không xác định. Runtime cố ý chọn ngẫu nhiên điểm bắt đầu, nên hai vòng lặp trên cùng một map trong cùng chương trình thường cho thứ tự khác nhau. Hãy chạy đoạn này vài lần:

Mọi đoạn code có đầu ra phụ thuộc vào thứ tự của map đều là bug đang chờ một lần chạy khác. Test so sánh kết quả in ra khi duyệt map là ví dụ kinh điển.

Sắp xếp khóa

Để duyệt map theo thứ tự khóa, hãy lấy các khóa, sắp xếp chúng rồi truy cập map theo từng khóa. Go 1.23 biến việc này thành một dòng nhờ các iterator của package maps và slices:

Ở Go 1.22 trở về trước, thư viện chuẩn chưa có maps.Keys. Cách làm tương đương là một vòng lặp:

keys := make([]string, 0, len(m))
for k := range m {
	keys = append(keys, k)
}
sort.Strings(keys)

Các hàm hỗ trợ khác trong maps: maps.Values, maps.Clone (bản sao nông), maps.Equal, maps.Copy(dst, src) và maps.DeleteFunc.

Kiểu khóa hợp lệ

Khóa phải so sánh được bằng ==: số, chuỗi, boolean, con trỏ, channel, mảng gồm các kiểu so sánh được, struct có mọi trường đều so sánh được, và giá trị interface. Slice, map và hàm không thể làm khóa.

map[[]int]bool{}       // compile error: invalid map key type []int
map[[2]int]bool{}      // fine: arrays are comparable
map[struct{ X, Y int }]string{} // fine: a struct key for a grid position

Dùng struct làm khóa là cách viết chuẩn của Go khi cần khóa theo nhiều giá trị cùng lúc, thay vì nối chuỗi.

Khóa kiểu interface vẫn biên dịch được kể cả khi kiểu động không so sánh được, rồi panic lúc chạy: lưu một []int vào map[any]int sẽ thất bại với runtime error: hash of unhashable type []int.

Khóa số thực vẫn dùng được, nhưng NaN không bằng chính nó, nên một khóa NaN có thể được thêm vào nhiều lần mà không bao giờ đọc lại được. Hãy tránh dùng khóa số thực.

Map chứa struct

Map có thể chứa struct, nhưng bạn không thể gán cho một trường của struct đang nằm trong map, vì giá trị trong map không lấy địa chỉ được.

Chọn lưu giá trị khi phần tử nhỏ và được thay thế nguyên khối. Chọn lưu con trỏ khi bạn cập nhật trường thường xuyên hoặc dùng chung một bản ghi từ nhiều nơi. Với con trỏ, khóa không tồn tại trả về nil, nên ptrs["nope"].Score sẽ panic.

Map chứa slice cũng vậy khi append: groups[k] = append(groups[k], v) không cần khởi tạo, vì khóa không tồn tại cho ra một slice nil và append xử lý được nil.

Map hoạt động giống tham chiếu

Một giá trị map trỏ tới dữ liệu dùng chung. Gán map hoặc truyền map vào hàm không sao chép các phần tử: cả hai biến cùng nhìn thấy một map.

Đó là lý do một hàm có thể điền dữ liệu vào map mà không cần trả map về, khác với một slice mà hàm append vào.

Panic khi dùng map nil

Giá trị zero của map là nil. Map nil đọc giống như map rỗng, nhưng ghi vào thì panic.

Đầu ra:

0 0
recovered: assignment to entry in nil map

Trường hợp map nằm trong struct mới là trường hợp hay gây lỗi trong thực tế. Hãy khởi tạo các trường map trong hàm khởi tạo (func NewCache() *Cache { return &Cache{data: map[string]string{}} }) hoặc khởi tạo trễ ngay trước lần ghi đầu tiên.

Truy cập đồng thời

Map không an toàn khi dùng đồng thời. Nếu một goroutine ghi trong khi goroutine khác đọc hoặc ghi, runtime có thể dừng chương trình với fatal error: concurrent map writes (hoặc concurrent map read and map write). Đây là fatal error, không phải panic, nên recover không bắt được.

Hãy bảo vệ map bằng một mutex:

Chương trình này luôn in ra 50 50. Dùng sync.RWMutex khi số lần đọc nhiều hơn hẳn số lần ghi. sync.Map chỉ dành cho hai trường hợp hẹp (khóa được ghi một lần và đọc nhiều lần, hoặc các goroutine làm việc trên những khóa tách biệt); với mọi trường hợp còn lại, một mutex cùng một map thường đơn giản hơn và thường nhanh hơn. Xem thêm ở mutex.

Tra cứu nhanh

Thao tácCode
Tạom := map[string]int{} hoặc make(map[string]int)
Thêm hoặc cập nhậtm[k] = v
Đọc (zero nếu không có)v := m[k]
Kiểm tra tồn tạiv, ok := m[k]
Xóadelete(m, k)
Xóa tất cảclear(m) (Go 1.21)
Kích thướclen(m)
Khóa đã sắp xếpslices.Sorted(maps.Keys(m)) (Go 1.23)
Sao chépmaps.Clone(m)
So sánhmaps.Equal(a, b)

Một map có giá trị struct{} cũng chính là kiểu tập hợp (set) của Go; xem set.

Lỗi thường gặp

  • Ghi vào map nil. Luôn tạo map bằng make, kể cả các trường map trong struct.
  • Dựa vào thứ tự duyệt. Hãy sắp xếp các khóa.
  • Dùng m[k] != 0 để kiểm tra tồn tại. Hãy dùng comma-ok.
  • Sửa trường của struct qua m[k].Field. Sao chép ra rồi ghi lại, hoặc lưu con trỏ.
  • Dùng chung map giữa các goroutine mà không khóa. Lỗi sập chương trình không thể recover.

Câu hỏi thường gặp

Làm sao kiểm tra một khóa có tồn tại trong map Go không?

Dùng dạng tra cứu trả về hai giá trị: v, ok := m[key]. ok là true khi khóa có trong map và false khi không có, lúc đó v mang giá trị zero. Chỉ đọc m[key] thì không phân biệt được khóa không tồn tại với khóa đang lưu giá trị zero.

Vì sao thứ tự duyệt map trong Go lại ngẫu nhiên?

Ngôn ngữ không quy định thứ tự, và runtime cố ý bắt đầu mỗi lần range ở một vị trí ngẫu nhiên để chương trình không thể dựa vào một thứ tự nào. Muốn duyệt theo thứ tự khóa, hãy gom các khóa lại và sắp xếp: for _, k := range slices.Sorted(maps.Keys(m)) (Go 1.23).

Làm sao lấy tất cả khóa của một map trong Go?

Từ Go 1.23, maps.Keys(m) trả về một iterator; chuyển nó thành slice bằng slices.Collect(maps.Keys(m)), hoặc thành slice đã sắp xếp bằng slices.Sorted(maps.Keys(m)). Trước bản 1.23, dùng vòng lặp for k := range m và append từng khóa vào một slice.

Vì sao ghi vào map lại panic với lỗi "assignment to entry in nil map"?

Biến map đã được khai báo nhưng chưa bao giờ được tạo: var m map[string]int là nil. Đọc map nil trả về giá trị zero, nhưng ghi vào thì panic. Hãy tạo map trước bằng m = make(map[string]int) hoặc literal m := map[string]int{}. Một trường map bên trong struct cũng cần khởi tạo như vậy.

Map trong Go có an toàn khi dùng đồng thời không?

Không. Nhiều lần ghi đồng thời, hoặc một lần ghi chạy cùng lúc với các lần đọc, có thể làm chương trình sập với fatal error: concurrent map writes, và recover không bắt được lỗi này. Hãy bảo vệ map bằng sync.Mutex hoặc sync.RWMutex, hoặc dùng sync.Map cho đúng những trường hợp nó được thiết kế ra.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu