0

Go Zero to Hero - Bài 15: Kiểm soát vòng lặp với break, continue và goto

Trong quá trình xây dựng hệ thống backend, việc kiểm soát chính xác luồng thực thi bên trong các vòng lặp là tối quan trọng để xử lý lỗi hoặc tối ưu hóa hiệu năng. Golang cung cấp các công cụ cực kỳ mạnh mẽ nhưng cũng rất kỷ luật cho việc này.


1. break: Phanh khẩn cấp (Emergency Brake)

Từ khóa break thì quá quen thuộc rồi. Khi chương trình gặp break, nó sẽ ngay lập tức đập vỡ và thoát khỏi vòng lặp chứa nó, bỏ qua mọi điều kiện hay các lệnh còn lại phía dưới.

Ứng dụng thực chiến: Tìm kiếm hoặc dừng gấp khi có lỗi nghiêm trọng

Giả sử bạn đang quét một mảng log giao dịch dài dằng dặc. Chỉ cần tìm thấy một giao dịch "gian lận" (Fraud), bạn muốn dừng quét ngay lập tức để cảnh báo, không cần quan tâm phần còn lại nữa.

package main

import "fmt"

func main() {
    transactions := []string{"OK", "OK", "FRAUD", "OK", "OK"}

    for i := 0; i < len(transactions); i++ {
        if transactions[i] == "FRAUD" {
            fmt.Println("🚨 Phát hiện giao dịch gian lận tại vị trí:", i)
            break // Đạp phanh khẩn cấp, thoát vòng lặp!
        }
        fmt.Println("Đang xử lý giao dịch...", i)
    }
    fmt.Println("Hệ thống đã dừng xử lý batch này.")
}

2. continue: Bỏ qua và đi tiếp (Skip and Next)

Khác với break là dừng hẳn, continue có nghĩa là: "Bỏ qua tất cả các lệnh còn lại của vòng lặp HIỆN TẠI, và nhảy ngay sang vòng lặp TIẾP THEO".

Ứng dụng thực chiến: Tiền xử lý (Filter) dữ liệu

Khi bạn đọc dữ liệu từ Database hoặc Kafka stream lên, có những bản ghi bị lỗi (thiếu trường dữ liệu, null). Bạn không muốn dừng cả tiến trình (như break), bạn chỉ muốn bỏ qua bản ghi lỗi đó và đi xử lý bản ghi tiếp theo.

func main() {
    // Giả lập ID của các thiết bị đọc thẻ (ID = 0 là thiết bị hỏng)
    deviceIDs := []int{101, 0, 102, 0, 103}

    for i := 0; i < len(deviceIDs); i++ {
        if deviceIDs[i] == 0 {
            fmt.Println("⚠️ Bỏ qua thiết bị lỗi tại vị trí", i)
            continue // Nhảy qua các lệnh bên dưới, quay lại đầu vòng lặp với i mới
        }
        
        fmt.Printf("Đang ping tới thiết bị %d...\n", deviceIDs[i])
        // Các logic tốn thời gian (gọi API, lưu DB) đặt ở đây
    }
}

Nhờ continue, bạn tránh được việc phải viết các khối lệnh if-else lồng nhau quá sâu (Deep nesting), giúp code phẳng (flat) và dễ đọc hơn rất nhiều.


3. Tuyệt chiêu của Go: Label (Nhãn) kết hợp với break/continue

Đây là tính năng đáng giá nhất khi làm kiến trúc hệ thống, nơi bạn sẽ hay gặp cảnh vòng lặp lồng trong vòng lặp.

Giả sử bạn có 1 vòng lặp duyệt qua các "Nhà ga", bên trong đó lại có 1 vòng lặp duyệt qua các "Máy bán vé" (TVM) của nhà ga đó. Nếu phát hiện một máy bị chập điện gây cháy, bạn muốn dừng NGAY LẬP TỨC toàn bộ hệ thống (thoát cả 2 vòng lặp).

Nếu chỉ dùng break thông thường, nó sẽ chỉ thoát được vòng lặp bên trong (vòng lặp máy bán vé), và vòng lặp bên ngoài (nhà ga) vẫn tiếp tục chạy!

Giải pháp: Dùng Label (Nhãn)

Trong Go, bạn có thể đặt một cái tên (Label) ngay trước vòng lặp, sau đó chỉ định break hoặc continue tới đích danh cái tên đó.

func main() {
    // Đặt nhãn cho vòng lặp ngoài cùng
OuterLoop: 
    for ga := 1; ga <= 3; ga++ {
        fmt.Printf("--- Kiểm tra Nhà ga %d ---\n", ga)
        
        for may := 1; may <= 3; may++ {
            if ga == 2 && may == 2 {
                fmt.Println("🔥 LỖI CHÁY NỔ TẠI GA 2, MÁY 2! DỪNG TOÀN HỆ THỐNG!")
                
                // Thoát thẳng ra khỏi vòng lặp OuterLoop
                break OuterLoop 
            }
            fmt.Printf("  Máy %d hoạt động tốt.\n", may)
        }
    }
    
    fmt.Println("Quy trình kiểm tra đã kết thúc.")
}

Cú pháp break Label hoặc continue Label này thanh lịch hơn rất nhiều so với việc bạn phải tạo ra các biến cờ (flag variables như isError = true) để check ở từng vòng lặp như trong C++ hay Node.js.


4. goto: "Kẻ bị ruồng bỏ" trong thế giới lập trình

Cuối cùng, chúng ta nhắc đến từ khóa goto. Nó cho phép bạn nhảy vô điều kiện đến bất kỳ dòng code nào có đánh Label trong cùng một hàm.

Tại sao gọi nó là "kẻ bị ruồng bỏ"?

Kể từ bài báo huyền thoại "Go To Statement Considered Harmful" của Edgar Dijkstra năm 1968, giới lập trình viên đều đồng tình rằng lạm dụng goto sẽ biến cấu trúc chương trình thành một đĩa "mỳ ý" (Spaghetti code) rối rắm, chạy vòng vèo không thể dò được luồng (trace).

Tuy nhiên, Go vẫn giữ lại goto vì trong một số trường hợp cực kỳ đặc thù, nó giúp code gọn hơn.

Khi nào nên dùng goto?

Thường là để gom chung logic dọn dẹp lỗi (Cleanup/Error handling) ở cuối hàm khi xử lý các tài nguyên cấp thấp (như đóng kết nối CGO, giải phóng bộ nhớ manual). Mặc dù hiện nay Go đã có từ khóa defer làm việc này tốt hơn, nhưng đây là cú pháp của goto:

func process() {
    err := doStep1()
    if err != nil {
        goto ErrorHandler
    }

    err = doStep2()
    if err != nil {
        goto ErrorHandler
    }
    
    fmt.Println("Xử lý thành công!")
    return // Kết thúc hàm thành công

    // Nhãn đích đến của goto
ErrorHandler: 
    fmt.Println("Xảy ra lỗi, tiến hành dọn dẹp (Rollback)...")
}

Lời khuyên thực chiến: Với 99.9% các dự án Backend thông thường, hãy quên goto đi. Hãy dùng break, continue (kết hợp Label nếu cần) và defer để xử lý.


Tổng kết

Vậy là bạn đã làm chủ hoàn toàn các luồng điều khiển trong Go:

  1. break: Đạp phanh khẩn cấp, văng ra khỏi vòng lặp hiện tại.
  2. continue: Bỏ qua lượt hiện tại, nhảy sang vòng mới.
  3. break/continue Label: Vũ khí tối thượng để kiểm soát các vòng lặp lồng nhau sâu mà không làm bẩn code.
  4. goto: Biết để hiểu mã nguồn hệ thống cấp thấp, nhưng hạn chế tối đa việc sử dụng.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.