Tài liệu kỹ thuật

Các chương trình Go sử dụng os.Exit hoặc log.Fatal* để thoát ngay lập tức (sử dụng panic không phải là cách tốt để thoát chương trình, hãy không sử dụng panic).

Gọi một trong các hàm os.Exit hoặc log.Fatal* chỉ trong main(). Tất cả các hàm khác nên trả về lỗi cho người gọi.

Không khuyến khích:

func main() {
  body := readFile(path)
  fmt.Println(body)
}
func readFile(path string) string {
  f, err := os.Open(path)
  if err != nil {
    log.Fatal(err)
  }
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  return string(b)
}

Được khuyến khích:

func main() {
  body, err := readFile(path)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(body)
}
func readFile(path string) (string, error) {
  f, err := os.Open(path)
  if err != nil {
    return "", err
  }
  b, err := os.ReadAll(f)
  if err != nil {
    return "", err
  }
  return string(b), nil
}

Về cơ bản, các chương trình có nhiều điểm thoát có một số vấn đề:

  • Luồng kiểm soát không rõ ràng: Bất kỳ hàm nào cũng có thể thoát chương trình, làm cho việc suy luận về luồng kiểm soát trở nên khó khăn.
  • Khó kiểm tra: Các hàm thoát chương trình cũng thoát các bài kiểm tra gọi chúng. Điều này làm cho các hàm khó kiểm tra và tạo ra rủi ro bỏ qua các bài kiểm tra khác chưa chạy bằng go test.
  • Bỏ qua việc dọn dẹp: Khi một hàm thoát chương trình, nó bỏ qua bất kỳ cuộc gọi hàm trì hoãn nào. Điều này tăng nguy cơ bỏ qua các công việc dọn dẹp quan trọng.

Thoát một lần

Nếu có thể, chỉ nên có tối đa một lời gọi tới os.Exit hoặc log.Fatal trong hàm main() của bạn. Nếu có nhiều tình huống lỗi yêu cầu dừng thực thi chương trình, hãy đặt logic đó trong một hàm riêng và trả về lỗi từ đó. Điều này sẽ rút ngắn hàm main() và đặt tất cả logic kinh doanh quan trọng trong một hàm riêng, có thể kiểm tra được.

Không khuyến khích:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("missing file")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // Nếu chúng ta gọi log.Fatal sau dòng này
  // f.Close sẽ được thực thi.
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

Được khuyến khích:

package main
func main() {
  if err := run(); err != nil {
    log.Fatal(err)
  }
}
func run() error {
  args := os.Args[1:]
  if len(args) != 1 {
    return errors.New("missing file")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    return err
  }
  defer f.Close()
  b, err := os.ReadAll(f)
  if err != nil {
    return err
  }
  // ...
}

Ví dụ trên sử dụng log.Fatal, nhưng nguyên tắc này cũng áp dụng cho os.Exit hoặc bất kỳ mã thư viện nào gọi os.Exit.

func main() {
  if err := run(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

Bạn có thể thay đổi chữ ký của run() theo cần. Ví dụ, nếu chương trình của bạn phải thoát với một mã lỗi cụ thể, run() có thể trả về mã thoát thay vì một lỗi. Điều này cũng cho phép bài kiểm tra đơn vị kiểm tra trực tiếp hành vi này.

func main() {
  os.Exit(run(args))
}

func run() (exitCode int) {
  // ...
}

Vui lòng lưu ý rằng việc sử dụng hàm run() trong các ví dụ này không bắt buộc. Tên, chữ ký và cài đặt của hàm run() là linh hoạt. Ngoài ra, bạn có thể:

  • Chấp nhận đối số dòng lệnh chưa được phân tích (ví dụ, run(os.Args[1:]))
  • Phân tích các đối số dòng lệnh trong main() và chuyển chúng vào run
  • Trả mã thoát trở lại main() với một loại lỗi tùy chỉnh
  • Đặt logic kinh doanh ở một cấp độ trừu tượng khác package main

Hướng dẫn này chỉ yêu cầu có một nơi trong main() chịu trách nhiệm cho luồng thoát thực tế.