Go Zero to Hero - Bài 20: Vòng lặp for-range để duyệt Array và Slice
Thay vì phải tạo biến đếm i, kiểm tra điều kiện i < len(slice), và thủ công gọi slice[i], cú pháp for range làm hộ bạn tất cả những việc đó ở hậu trường.
1. Cú pháp cơ bản: Sự thanh lịch của Go
Khi bạn gọi range trên một Array hoặc Slice, nó luôn trả về cho bạn 2 giá trị ở mỗi vòng lặp:
- Index: Vị trí (chỉ mục) của phần tử hiện tại.
- Value: Giá trị của phần tử đó.
package main
import "fmt"
func main() {
// Một slice chứa danh sách IP của các máy chủ trạm
stationIPs := []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}
// Cú pháp: for [index], [value] := range [danh_sách]
for index, ip := range stationIPs {
fmt.Printf("Trạm %d có IP là: %s\n", index, ip)
}
}
2. Nghệ thuật "Bơ đi mà sống" với Blank Identifier (_)
Như bạn đã biết, Go mắc hội chứng "ám ảnh cưỡng chế" cực nặng với các biến rác: Khai báo mà không dùng thì không cho biên dịch!
Vậy nếu bạn chỉ muốn lấy giá trị (value) mà không quan tâm đến vị trí (index) thì sao? Không thể khai báo biến index rồi để đó được. Đây là lúc Dấu gạch dưới (Blank Identifier _) phát huy tác dụng.
// 1. Chỉ lấy Value (Bỏ qua Index)
for _, ip := range stationIPs {
fmt.Println("Đang ping tới:", ip)
}
// 2. Chỉ lấy Index (Lược bỏ hoàn toàn Value)
// Rất hữu ích khi bạn chỉ cần lấy vị trí để thao tác trực tiếp lên mảng gốc
for index := range stationIPs {
fmt.Println("Đang xử lý phần tử ở vị trí:", index)
}
3. Cạm bẫy "Copy Value": Bệnh viện trả về của các Gopher newbie
Đây là phần quan trọng nhất của bài viết này. Hãy đọc thật kỹ nếu bạn không muốn hệ thống backend của mình cập nhật sai dữ liệu vào Database!
Hãy xem xét bài toán: Bạn có một danh sách vé đang ở trạng thái PENDING. Bạn muốn duyệt qua danh sách và đổi tất cả thành PAID.
// CODE LỖI LOGIC!
func main() {
tickets := []string{"PENDING", "PENDING", "PENDING"}
for _, t := range tickets {
t = "PAID" // Cố gắng cập nhật trạng thái
}
fmt.Println(tickets) // KẾT QUẢ: [PENDING PENDING PENDING]. Ơ kìa???
}
Tại sao mảng gốc không hề bị thay đổi?
Bởi vì biến t (value) trong cú pháp for _, t := range chỉ là một BẢN SAO (Copy) của phần tử thực sự trong mảng.
Ở mỗi vòng lặp, Go tạo ra một biến cục bộ tên là t, chép giá trị "PENDING" vào đó. Bạn đổi t thành "PAID", tức là bạn chỉ đổi cái bản sao tạm thời kia, còn mảng gốc thì vẫn "bình chân như vại".
Giải pháp: Thao tác qua Index
Để sửa đổi dữ liệu của mảng gốc, bạn BẮT BUỘC phải dùng Index để trỏ trực tiếp vào ô nhớ của mảng đó.
// CODE CHUẨN GO
func main() {
tickets := []string{"PENDING", "PENDING", "PENDING"}
for i := range tickets {
tickets[i] = "PAID" // Trỏ thẳng vào mảng gốc và ghi đè
}
fmt.Println(tickets) // KẾT QUẢ: [PAID PAID PAID]. Chuẩn bài!
}
4. Tối ưu hiệu năng bộ nhớ (Performance Optimization)
Hãy tưởng tượng, thay vì mảng chứa các chuỗi string ngắn ngủn, bạn có một mảng chứa hàng vạn cấu trúc dữ liệu (Struct) siêu to khổng lồ (chứa thông tin user, lịch sử giao dịch, logs...).
Nếu bạn dùng cú pháp for _, item := range hugeSlice, ở MỖI vòng lặp, Go sẽ phải copy toàn bộ dữ liệu của cái Struct khổng lồ đó vào biến item. Điều này vắt kiệt CPU và làm RAM phình to nhanh chóng.
Best Practice cho hệ thống hiệu năng cao:
- Duyệt bằng Index:
for i := range hugeSlicevà lấy dữ liệu bằnghugeSlice[i]. Cách này 0% copy. - Sử dụng Slice of Pointers: Khai báo danh sách dưới dạng mảng con trỏ (ví dụ:
[]*Transaction). Khi đó, vòng lặpfor rangechỉ copy cái địa chỉ bộ nhớ (chiếm đúng 8 bytes), tốc độ sẽ nhanh như điện xẹt dù dữ liệu bên trong có lớn đến mức nào.
Tổng kết
Công lực của for range được tóm gọn qua 3 điểm cốt tử:
- Tiện lợi: Tự động đếm, tự động cấp phát index và value, code ngắn gọn dễ đọc.
- Khắt khe: Nhớ dùng
_(Blank identifier) để bỏ qua biến không sử dụng. - An toàn: Biến value trả về chỉ là bản sao. Tuyệt đối không dùng nó để gán lại (mutate) dữ liệu gốc. Muốn sửa dữ liệu, hãy dùng index.
All rights reserved