Menu

Vòng lặp for trong Golang: cả bốn dạng kèm ví dụ

Go chỉ có một từ khóa vòng lặp là for, và nó đảm nhận mọi việc: vòng lặp đếm ba mệnh đề, vòng lặp while chỉ có điều kiện, vòng lặp vô hạn, và range trên slice, map, chuỗi, channel, số nguyên và hàm. Cùng với break, continue có nhãn và thay đổi về biến vòng lặp trong Go 1.22.

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.

Go có đúng một từ khóa vòng lặp (loop): for. Nó có bốn dạng, và cùng nhau chúng thay thế mọi vòng lặp bạn vẫn viết bằng for, while, do while hay foreach ở các ngôn ngữ khác.

Phần đầu vòng lặp không có dấu ngoặc tròn, và dấu ngoặc nhọn luôn bắt buộc.

Vòng lặp ba mệnh đề

for init; condition; post {
	// body
}
  1. init chạy một lần trước vòng lặp. Thường đây là một khai báo biến ngắn như i := 0, và biến đó chỉ tồn tại bên trong vòng lặp.
  2. condition được kiểm tra trước mỗi lần lặp. Khi nó sai, vòng lặp kết thúc.
  3. Phần thân được thực thi.
  4. post chạy sau mỗi lần lặp, thường là i++.

Trong Go, i++ là một câu lệnh chứ không phải biểu thức, nên post không thể chứa i++, j--. Hãy dùng phép gán song song: i, j = i+1, j-1.

Cả ba mệnh đề đều không bắt buộc. for ; i < 10; {} là hợp lệ, và gofmt sẽ viết lại nó thành for i < 10 {}, chính là dạng tiếp theo.

Vòng lặp chỉ có điều kiện (while của Go)

Chỉ với một điều kiện, for hoạt động như while ở các ngôn ngữ khác:

Go không có do while riêng. Trang vòng lặp while hướng dẫn cách đạt được hành vi "chạy ít nhất một lần".

Vòng lặp vô hạn

for không có gì phía sau sẽ lặp mãi mãi. Thoát ra bằng break, return, os.Exit hoặc một panic:

for {
	line, err := reader.ReadString('\n')
	if err != nil {
		break
	}
	process(line)
}

Đây là cấu trúc chuẩn cho server, worker và vòng lặp đọc đầu vào. Một for {} với phần thân rỗng sẽ chiếm trọn một lõi CPU; trong code thực tế, phần thân sẽ chờ một thứ gì đó (một lần đọc, một lần nhận từ channel, một select).

Vòng lặp range

range duyệt qua một tập hợp và trả về một hoặc hai giá trị mỗi lần lặp. Đó là giá trị gì phụ thuộc vào thứ bạn duyệt:

Duyệt quaGiá trị thứ nhấtGiá trị thứ hai
slice hoặc mảngchỉ sốphần tử (một bản sao)
chuỗichỉ số byterune (ký tự Unicode)
mapkhóagiá trị
channelphần tử(không có)
số nguyên n (Go 1.22)từ 0 đến n trừ 1(không có)
hàm iterator (Go 1.23)giá trị hàm yield ragiá trị hàm yield ra

Đầu ra cho thấy ba chi tiết:

  • Biến giá trị là một bản sao. v *= 2 bên trong vòng lặp sẽ không thay đổi slice; để sửa phần tử, hãy truy cập slice qua chỉ số với nums[i], như vòng lặp thứ ba.
  • Chỉ số của chuỗi là vị trí theo byte. é chiếm hai byte, nên chỉ số nhảy từ 1 lên 3.
  • Thứ tự của map là ngẫu nhiên có chủ ý. Go xáo trộn nó để code không thể phụ thuộc vào thứ tự. slices.Sorted(maps.Keys(m)) (Go 1.23) trả về các khóa theo thứ tự.

range được trình bày đầy đủ, bao gồm channel, hàm iterator và các quy tắc sao chép, ở trang range.

break và continue

break kết thúc vòng lặp trong cùng. continue bỏ qua phần còn lại để sang lần lặp tiếp theo (trong vòng lặp ba mệnh đề, post được chạy trước):

break và continue có nhãn

Một break thông thường chỉ thoát khỏi vòng lặp trong cùng. Để thoát khỏi vòng lặp ngoài từ bên trong một vòng lặp lồng nhau, hãy gắn nhãn cho vòng lặp ngoài:

Nhãn là một định danh theo sau bởi dấu hai chấm, đặt ngay trước for. Go báo lỗi biên dịch với nhãn không được dùng, giống như với biến không được dùng.

break bên trong switch và select

Bên trong một for, break trong một case của switch hoặc select sẽ thoát khỏi switch hoặc select, không phải vòng lặp:

for {
	switch cmd := next(); cmd {
	case "quit":
		break // only exits the switch; the loop keeps running
	}
}

Hãy dùng break có nhãn, hoặc return, để thoát khỏi vòng lặp từ bên trong một switch hoặc select.

Biến vòng lặp từ Go 1.22

Go 1.22 đã thay đổi phạm vi của biến vòng lặp: giờ đây mỗi lần lặp có một bản sao mới của i và v. Trước đây, một biến duy nhất được dùng lại cho cả vòng lặp, làm hỏng các closure và goroutine bắt giữ nó:

Với Go 1.22 trở lên, chương trình này in ra 0 1 2 và [aa bb cc]. Theo ngữ nghĩa cũ, cùng chương trình viết bằng for i := 0; i < 3; i++ (range trên số nguyên chưa tồn tại trước 1.22) in ra 3 3 3, và các goroutine tranh chấp (race) trên i và name dùng chung. Cách khắc phục cũ, i := i ở đầu thân vòng lặp, không còn cần thiết.

Hành vi này được chọn theo từng module qua dòng go trong go.mod, không phải theo phiên bản trình biên dịch. Một module vẫn ghi go 1.21 sẽ giữ ngữ nghĩa cũ ngay cả khi được build bằng Go 1.24.

Những lỗi thường gặp

Sửa bản sao. for _, v := range items { v.Count++ } chỉ thay đổi một bản sao. Hãy dùng items[i].Count++, hoặc duyệt qua một slice chứa con trỏ.

Thêm phần tử trong lúc range. range tính slice một lần duy nhất, lúc bắt đầu. Các phần tử được thêm bên trong vòng lặp sẽ không được duyệt, nên vòng lặp vẫn kết thúc, nhưng nó sẽ không thấy các phần tử mới.

Xóa phần tử khỏi slice theo chỉ số trong vòng lặp tiến. Xóa phần tử i sẽ dồn phần còn lại sang trái, nên phần tử kế tiếp bị bỏ qua. Hãy tạo một slice mới, lặp ngược, hoặc dùng slices.DeleteFunc.

Lệch một đơn vị với <=. for i := 0; i <= len(s); i++ đọc s[len(s)] ở lần lặp cuối và panic với index out of range. Hãy dùng <, hoặc tốt hơn, dùng range.

Việc nặng trong điều kiện. Điều kiện được chạy ở mỗi lần lặp. for i := 0; i < expensiveCount(); i++ gọi hàm ở mọi lần lặp; hãy tính giá trị một lần trước vòng lặp.

Dùng số thực làm biến đếm. for x := 0.0; x != 1.0; x += 0.1 không bao giờ kết thúc, vì 0.1 không có biểu diễn nhị phân chính xác. Hãy đếm bằng số nguyên và tính số thực từ đó.

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

Viết vòng lặp for trong Go như thế nào?

Dạng cổ điển có ba phần cách nhau bằng dấu chấm phẩy và không có dấu ngoặc tròn:

for i := 0; i < 5; i++ {
	fmt.Println(i)
}

Từ Go 1.22, bạn cũng có thể viết for i := range 5, đếm từ 0 đến 4. Để duyệt một slice, dùng for i, v := range items.

Go có vòng lặp while không?

Không có từ khóa while, nhưng for chỉ với một điều kiện chính là vòng lặp while: for n > 0 { n /= 2 }. for { ... } không có điều kiện là vòng lặp vô hạn, và bạn thoát ra bằng break hoặc return.

Làm sao duyệt map trong Go?

for key, value := range m { ... }. Thứ tự duyệt không được đặc tả, và runtime cố ý xáo trộn nó nên mỗi vòng lặp có thể khác nhau. Đừng bao giờ dựa vào thứ tự này. Để có thứ tự ổn định, hãy gom các khóa lại, sắp xếp bằng slices.Sort, rồi duyệt qua các khóa đã sắp xếp.

Làm sao thoát khỏi vòng lặp lồng nhau trong Go?

Đặt một nhãn trước vòng lặp ngoài và ghi tên nhãn trong break: outer: for ... { for ... { if found { break outer } } }. Một break thông thường chỉ thoát khỏi for, switch hoặc select trong cùng. Tương tự, continue outer nhảy sang lần lặp tiếp theo của vòng lặp có nhãn.

Biến vòng lặp trong Go 1.22 đã thay đổi gì?

Trước Go 1.22, một vòng lặp for chỉ có một biến dùng chung cho mọi lần lặp, nên các closure và goroutine khởi tạo trong vòng lặp thường đều thấy giá trị cuối cùng. Từ Go 1.22, mỗi lần lặp có một bản sao riêng, nên việc bắt i hoặc v trong closure hoạt động đúng như mong đợi. Hành vi mới áp dụng khi go.mod của module ghi go 1.22 trở lên.

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