Go có ba cấu trúc điều khiển: if, switch, for. Ít hơn hầu hết ngôn ngữ khác, và mỗi cái có vài điểm riêng đáng biết.
Phần cuối bài là một thay đổi ngôn ngữ hiếm gặp: Go 1.22 đổi ý nghĩa của mã đã tồn tại, và tôi đo được nó trên ba phiên bản.
if có câu lệnh khởi tạo
if v, ok := m["khoa"]; ok {
fmt.Println(v)
}
Biến khai trong if chỉ sống trong if và else của nó. Đây là mẫu bạn sẽ gõ hàng ngày, nhất là với xử lý lỗi:
if err := lam(); err != nil {
return err
}
err không rò ra ngoài, nên bạn dùng lại tên đó ở dòng sau mà không sợ nhầm.
Hai điểm khác Java: không có ngoặc quanh điều kiện, và điều kiện phải là bool. Không có if x với x là số hay con trỏ — phải viết if x != 0, if p != nil.
switch: mặc định không rơi xuống
switch d := 2; d {
case 1, 2, 3:
fmt.Println("khớp")
fallthrough // phải viết TƯỜNG MINH mới rơi xuống
case 99:
fmt.Println("rơi xuống đây")
}
Ba khác biệt so với Java:
Không cần break. Mỗi case tự dừng. Muốn rơi xuống thì viết fallthrough — và nó rơi vô điều kiện, không kiểm giá trị của case tiếp theo.
Một case nhận nhiều giá trị, ngăn bởi dấu phẩy.
case không cần là hằng. Đây là điểm mạnh nhất: bỏ hẳn biểu thức sau switch thì mỗi case là một điều kiện bool:
switch {
case x < 3:
fmt.Println("nhỏ")
case x < 10:
fmt.Println("vừa")
default:
fmt.Println("lớn")
}
Đây là cách viết chuỗi if-else if cho gọn, và là dạng switch tôi dùng nhiều nhất trong Go.
Còn một dạng thứ ba — switch trên kiểu — sẽ nói ở bài 14.
for: một từ khoá cho tất cả
Go không có while, do-while, hay foreach. Chỉ for, với bốn dạng:
for i := 0; i < 3; i++ { } // ba phần, như Java
for j < 3 { j++ } // như while
for { } // vô hạn
for i, v := range slice { } // duyệt
for i := range 3 { } // duyệt số nguyên — Go 1.22+
Dạng cuối mới có từ Go 1.22 và rất tiện: for i := range 3 lặp với i = 0, 1, 2.
range hoạt động trên slice, mảng, chuỗi, map, channel. Với map, thứ tự duyệt là ngẫu nhiên có chủ đích — Go cố tình xáo trộn để bạn không viết mã dựa vào thứ tự. Cần thứ tự thì lấy khoá ra, sắp xếp, rồi duyệt.
Bỏ giá trị không cần bằng _:
for _, v := range slice { } // chỉ cần giá trị
for i := range slice { } // chỉ cần chỉ số
Thay đổi lớn nhất: biến vòng lặp ở Go 1.22
var fs []func()
for i := 0; i < 3; i++ {
fs = append(fs, func() { fmt.Printf("%d ", i) })
}
for _, f := range fs { f() }
Đoạn này in ra gì? Tôi chạy trên ba phiên bản:
Go 1.21 -> 3 3 3
Go 1.22 -> 0 1 2
Go 1.23 -> 0 1 2
Cùng một mã, hai kết quả khác nhau.
Trước Go 1.22, biến vòng lặp i được tạo một lần cho cả vòng lặp, nên cả ba closure cùng bắt một biến — và khi chúng chạy, biến đó đã là 3.
Từ Go 1.22, mỗi vòng lặp có biến riêng, nên mỗi closure bắt một giá trị khác nhau.
Đây là lỗi hay gặp tới mức nhóm Go quyết định phá vỡ tương thích ngược để sửa — chuyện gần như chưa từng xảy ra với Go. Cách họ làm cho an toàn: hành vi mới chỉ áp dụng khi go.mod khai go 1.22 trở lên. Module cũ giữ nguyên ngữ nghĩa cũ.
Hệ quả thực tế cho bạn:
Nếu go.mod khai dưới 1.22, mã của bạn vẫn chạy theo kiểu cũ — kể cả khi bạn build bằng Go 1.23. Đây là chỗ dễ nhầm khi đọc mã: hành vi phụ thuộc vào go.mod, không phụ thuộc vào trình biên dịch.
Mã cũ có i := i — dòng "sao chép biến vòng lặp" mà mọi hướng dẫn Go trước 2024 đều dạy — giờ thành thừa. Vô hại, nhưng có thể xoá.
Khi nâng go.mod lên 1.22, hãy chạy lại test. Rất hiếm nhưng có mã dựa vào hành vi cũ.
break và continue có nhãn
ngoai:
for i := range 3 {
for j := range 3 {
if j == 1 { continue ngoai }
if i == 2 { break ngoai }
}
}
Nhãn cho phép thoát khỏi vòng lặp ngoài. Go không có goto tuỳ tiện — thực ra nó có goto nhưng bị giới hạn chặt và gần như không ai dùng.
break cũng dùng được trong switch để thoát khỏi switch, và đây là chỗ dễ nhầm: break trong switch nằm trong for sẽ thoát switch chứ không thoát for. Cần thoát vòng lặp thì phải có nhãn.
Thử ba mươi giây
Mở go.mod của dự án bạn và xem dòng go. Nếu nó dưới 1.22, chạy thử đoạn closure ở trên — bạn sẽ thấy 3 3 3 dù đang dùng Go mới nhất.
Đổi dòng đó thành go 1.22 rồi chạy lại. Ba mươi giây, và bạn vừa thấy một dòng trong go.mod đổi ngữ nghĩa của ngôn ngữ.
Ngày mai: hàm và nhiều giá trị trả về — nền tảng của cách Go xử lý lỗi.