range là dạng của vòng lặp for dùng để đi qua một tập hợp. Mỗi lần lặp nó cho bạn một hoặc hai giá trị, và đó là những giá trị gì thì tùy vào kiểu mà bạn range:
| Toán hạng | Giá trị thứ nhất | Giá trị thứ hai | Ghi chú |
|---|---|---|---|
slice []T, mảng [n]T | chỉ số int | phần tử T, một bản sao | |
con trỏ tới mảng *[n]T | chỉ số | phần tử | |
| chuỗi | chỉ số byte int | rune | giải mã UTF-8 |
map map[K]V | khóa K | giá trị V | thứ tự ngẫu nhiên |
channel chan T | phần tử T | không có | cho đến khi channel bị đóng |
số nguyên n | từ 0 đến n trừ 1 | không có | Go 1.22 |
func(yield func() bool) | không có | không có | Go 1.23 |
func(yield func(V) bool) | V | không có | Go 1.23 |
func(yield func(K, V) bool) | K | V | Go 1.23 |
Chọn những biến bạn nhận
for i, v := range s {} // both
for i := range s {} // index (or key) only
for _, v := range s {} // value only
for range s {} // neither: just repeat len(s) times
Khai báo một biến mà không dùng là lỗi biên dịch, nên hãy bỏ hoặc thay bằng _ những biến bạn không cần. for range 3 {} là cách ngắn nhất để lặp lại một việc ba lần.
Giá trị là một bản sao
Ở mỗi lần lặp, phần tử được sao chép vào biến giá trị. Thay đổi biến đó không làm thay đổi tập hợp:
Sao chép cũng tốn thời gian khi phần tử là struct lớn. Với slice chứa struct lớn, for i := range s kết hợp s[i] sẽ tránh được việc sao chép.
Chuỗi trả về rune
Range trên chuỗi sẽ giải mã UTF-8. Giá trị thứ nhất là vị trí byte nơi mỗi ký tự bắt đầu, nên có thể nhảy cóc số:
Để đi qua từng byte thay vì từng ký tự, hãy dùng vòng lặp theo chỉ số, for i := 0; i < len(s); i++, hoặc range trên []byte(s). Xem trang rune và byte để biết chi tiết về UTF-8.
Map: thứ tự ngẫu nhiên
Thứ tự duyệt map không được quy định, và runtime cố ý xáo trộn nó để code không thể dựa vào một thứ tự nào. Muốn đầu ra cố định, hãy sắp xếp các khóa:
Xóa phần tử trong lúc range là an toàn, và một phần tử bị xóa khi chưa được duyệt tới sẽ không xuất hiện. Thêm phần tử thì được phép nhưng khó đoán: một khóa mới có thể xuất hiện hoặc không ở phần sau của cùng vòng lặp. fmt.Println in map với các khóa đã sắp xếp, đó là lý do dòng cuối luôn ổn định.
Channel: cho đến khi bị đóng
Range trên channel nhận giá trị cho đến khi channel bị đóng và đã được lấy hết. Nếu không ai đóng nó, vòng lặp bị chặn mãi mãi (và nếu mọi goroutine đều bị chặn, runtime dừng lại với fatal error: all goroutines are asleep - deadlock!):
Bên gửi đóng channel, không bao giờ là bên nhận. Xem trang channel để biết về channel có bộ đệm và select.
Số nguyên (Go 1.22)
range n đếm từ 0 lên đến n, không bao gồm n. Kiểu của biến vòng lặp là kiểu của n:
for i := range 3 {} // i is int: 0, 1, 2
for i := range uint8(3) {} // i is uint8
for range 0 {} // runs zero times
Với n âm, vòng lặp cũng chạy không lần nào. n được tính một lần, trước khi vòng lặp bắt đầu.
range tính toán những gì, và khi nào
Biểu thức sau range được tính một lần, trước lần lặp đầu tiên. Với slice, range sau đó dùng phần header của slice đó, nên độ dài của nó cố định:
Giá trị 99 được append vào không bao giờ được duyệt tới, vì độ dài vòng lặp đã cố định ở 3. Nhưng nums[2] = 30 thì được thấy, vì lúc đó nums vẫn dùng chung mảng nền với slice đang được range. Đổi chỗ hai dòng đó thì giá trị 30 cũng biến mất: append vượt quá dung lượng 3, cấp phát một mảng mới, và lần ghi rơi vào mảng mới đó. Range trên một giá trị mảng sẽ sao chép toàn bộ mảng trước, nên thay đổi ở arr[2] không được thấy; hãy range trên &arr hoặc arr[:] để tránh việc sao chép.
Range trên hàm (Go 1.23)
Từ Go 1.23, range còn nhận cả hàm iterator. Một iterator nhận callback yield, gọi nó một lần cho mỗi giá trị, và dừng khi yield trả về false (điều này xảy ra khi thân vòng lặp chạy break hoặc return):
iter.Seq[V] và iter.Seq2[K, V] là tên chuẩn cho hai dạng iterator. Thư viện chuẩn trả về chúng từ slices.All, slices.Values, slices.Backward, maps.Keys, maps.Values, maps.All, và trong Go 1.24 từ strings.SplitSeq, strings.Lines và bytes.SplitSeq. Iterator phải tôn trọng giá trị trả về của yield: gọi yield thêm lần nữa sau khi nó đã trả về false sẽ gây panic.
Biến vòng lặp riêng cho từng lần lặp (Go 1.22)
Từ Go 1.22, các biến do for ... range khai báo là biến mới ở mỗi lần lặp. Bắt v trong một closure hay goroutine sẽ bắt giá trị của lần lặp đó, chứ không phải một biến dùng chung mà cuối cùng giữ phần tử cuối cùng. Trước Go 1.22 bạn cần viết v := v bên trong vòng lặp; dòng đó giờ là thừa. Quy tắc này được quyết định bởi phiên bản go trong go.mod, nên một module cũ vẫn giữ hành vi cũ.
Lỗi thường gặp
- Lấy địa chỉ của biến giá trị. Trước Go 1.22,
ptrs = append(ptrs, &v)lưu cùng một địa chỉ ở mọi lần lặp. Từ 1.22 nó lưu các địa chỉ khác nhau, nhưng mỗi địa chỉ trỏ tới một bản sao, không phải phần tử trong slice. Hãy dùng&items[i]để trỏ vào slice. - Trông chờ thứ tự của map. Đầu ra trông như đã sắp xếp trong một test nhỏ sẽ không giữ được thứ tự đó mãi.
- Range trên channel mà không ai đóng. Vòng lặp, và cả goroutine đang chạy nó, sẽ chờ mãi mãi.
- Range trên một mảng lớn theo giá trị. Toàn bộ mảng bị sao chép trước. Hãy range trên một slice của nó.
Câu hỏi thường gặp
range trả về gì trong Go?
Tùy vào toán hạng. Với slice hoặc mảng: chỉ số và phần tử. Với chuỗi: chỉ số byte và rune. Với map: khóa và giá trị. Với channel: từng giá trị nhận được. Với số nguyên n (Go 1.22): từ 0 đến n trừ 1. Với hàm iterator (Go 1.23): những gì hàm đó yield ra. Bạn có thể bỏ biến thứ hai, hoặc bỏ qua biến thứ nhất bằng _.
Vì sao sửa giá trị trong range không làm thay đổi slice?
Biến giá trị là một bản sao của phần tử. for _, v := range items { v.Price = 0 } chỉ thay đổi bản sao. Hãy dùng chỉ số: for i := range items { items[i].Price = 0 }, hoặc range trên một slice chứa con trỏ.
Có thể range trên một số nguyên trong Go không?
Có, từ Go 1.22: for i := range 5 { ... } chạy với i từ 0 đến 4. for range 5 { ... } lặp năm lần mà không cần biến. Tệp go.mod của module phải ghi go 1.22 hoặc mới hơn.
Xóa phần tử khỏi map trong lúc range có an toàn không?
Có. Xóa một phần tử chưa được duyệt tới thì nó sẽ không xuất hiện nữa, và xóa phần tử hiện tại cũng không sao. Thêm phần tử trong lúc lặp cũng được phép, nhưng phần tử mới có thể được duyệt hoặc không, nên đừng dựa vào điều đó.