Cách viết chuẩn: map với giá trị rỗng
Go không có từ khóa set và thư viện chuẩn cũng không có set. Một map có khóa là các phần tử và giá trị không mang gì là đủ dùng.
struct{} là kiểu struct rỗng và struct{}{} là giá trị duy nhất của nó. Nó chiếm 0 byte, nên map chỉ lưu khóa và không gì khác.
Mọi quy tắc của map đều áp dụng: khóa phải so sánh được, thứ tự duyệt là ngẫu nhiên, ghi vào map nil sẽ gây panic, và ghi đồng thời cần khóa (lock). Trang map giải thích từng điểm.
map[T]struct{} hay map[T]bool
Cách viết phổ biến còn lại là map[T]bool. Nó dễ đọc hơn vì khóa không tồn tại trả về false:
seen := map[string]bool{}
seen["a"] = true
if seen["a"] { ... }
So sánh hai cách:
map[T]struct{} | map[T]bool | |
|---|---|---|
| Kích thước giá trị | 0 byte | 1 byte (cộng căn chỉnh bộ nhớ) |
| Kiểm tra thành viên | _, ok := s[k] | s[k] |
| Mơ hồ | không | s[k] = false là trạng thái thứ ba |
Hàng thứ ba mới là lý do thật khiến nhiều codebase chuộng struct rỗng: với bool, sớm muộn sẽ có người viết s[k] = false và len(s) không còn là số phần tử nữa. Với set nhỏ, chênh lệch bộ nhớ không đáng kể.
Xóa phần tử trùng lặp khỏi slice
Công dụng phổ biến nhất của set là loại bỏ trùng lặp. Đoạn code này giữ lần xuất hiện đầu tiên của mỗi giá trị và giữ nguyên thứ tự:
Đầu ra:
[b a c]
[3 1 2]
[1 2 3]
Hợp, giao, hiệu
Các phép toán trên tập hợp chỉ là vài vòng lặp. Khi kiểm tra thành viên trong set còn lại, hãy duyệt set nhỏ hơn, vì mỗi lần tra cứu trung bình tốn thời gian hằng số.
sorted chỉ có mặt để đầu ra ổn định. In một set bằng cách duyệt range sẽ cho thứ tự khác nhau sau mỗi lần chạy. maps.Keys và slices.Sorted cần Go 1.23.
Một kiểu đặt tên như type set map[string]struct{} vẫn là map: bạn truy cập theo khóa, range và delete trên nó y như cũ, và có thể gắn phương thức cho nó.
Một kiểu Set generic nhỏ
Với generics (Go 1.18), một kiểu duy nhất dùng được cho mọi kiểu phần tử so sánh được. Như thế này là đủ cho hầu hết chương trình:
Bọc map trong một struct giúp giấu đi phần struct{}{} rườm rà và bảo đảm map luôn được hàm khởi tạo tạo ra, nhờ đó loại bỏ panic do map nil. Sorted không thể là phương thức: phương thức không được khai báo tham số kiểu riêng hay siết chặt ràng buộc comparable của kiểu, mà sắp xếp lại cần cmp.Ordered. Trang generics giải thích về ràng buộc.
Nếu cần một set đầy đủ tính năng (phiên bản an toàn luồng, nhiều phép toán), có các package bên thứ ba như github.com/deckarep/golang-set. Với phần lớn code, lập trình viên Go dùng cách viết bằng map hoặc một kiểu khoảng 30 dòng như trên.
Set chứa struct
Kiểu nào so sánh được cũng làm phần tử được, kể cả struct có các trường so sánh được. Nhờ vậy kiểm tra "mình đã gặp cặp này chưa" trở nên đơn giản:
type edge struct{ from, to string }
visited := map[edge]struct{}{}
visited[edge{"a", "b"}] = struct{}{}
Slice và map không thể là phần tử của set. Để theo dõi các slice không trùng nhau, trước hết hãy chuyển mỗi slice thành một khóa so sánh được, ví dụ một mảng có kích thước cố định, hoặc một chuỗi tạo bằng fmt.Sprint.
Lỗi thường gặp
- Quên khởi tạo.
var s map[string]struct{}là nil; lần thêm đầu tiên sẽ panic. - In set và mong đầu ra ổn định. Hãy sắp xếp các phần tử trước.
- Dùng
map[T]boolvà lưufalse. Khi đólenkhông còn đếm đúng số phần tử. Hãy dùngdeleteđể xóa.
Câu hỏi thường gặp
Go có kiểu set không?
Không. Thư viện chuẩn không có set. Cách viết chuẩn là một map mà giá trị không mang thông tin gì: map[string]struct{}. Thêm phần tử là s[k] = struct{}{}, kiểm tra thành viên là _, ok := s[k], xóa là delete(s, k), và kích thước là len(s).
Nên dùng map[T]bool hay map[T]struct{} để làm set trong Go?
map[T]struct{} thể hiện rõ ý định và giá trị của nó chiếm 0 byte. map[T]bool đọc tự nhiên hơn (if seen[x]) vì khóa không tồn tại trả về false. Cả hai đều đúng; chênh lệch bộ nhớ chỉ đáng kể với set rất lớn. Hãy chọn một cách và dùng nhất quán.
Làm sao xóa phần tử trùng lặp khỏi slice trong Go?
Để giữ lần xuất hiện đầu tiên theo đúng thứ tự, hãy lặp và ghi lại các giá trị đã gặp trong một map[T]struct{}, chỉ append những giá trị chưa gặp. Nếu thứ tự không quan trọng, hãy sắp xếp rồi bỏ các phần tử trùng đứng cạnh nhau: slices.Sort(s); s = slices.Compact(s).