Bahan Teknis

Program Go menggunakan os.Exit atau log.Fatal* untuk keluar secara langsung (menggunakan panic bukan cara yang baik untuk keluar dari program, harap jangan menggunakan panic).

Panggil salah satu os.Exit atau log.Fatal* hanya di dalam main(). Semua fungsi lain harus mengembalikan kesalahan ke pemanggilnya.

Tidak disarankan:

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)
}

Disarankan:

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
}

Pada dasarnya, program dengan banyak titik keluar memiliki beberapa masalah:

  • Aliran kontrol yang membingungkan: Setiap fungsi dapat keluar dari program, membuatnya sulit untuk memahami aliran kontrol.
  • Sulit diuji: Fungsi yang keluar dari program juga keluar dari pengujian yang memanggilnya. Hal ini membuat fungsi sulit diuji dan memperkenalkan risiko melewatkan pengujian lain yang belum dijalankan oleh go test.
  • Melewatkan pembersihan: Ketika fungsi keluar dari program, fungsi tertunda akan dilewati. Ini meningkatkan risiko melewatkan tugas pembersihan penting.

Keluar Sekali

Jika memungkinkan, seharusnya hanya ada maksimal satu panggilan os.Exit atau log.Fatal dalam fungsi main() Anda. Jika ada beberapa skenario kesalahan yang memerlukan menghentikan eksekusi program, letakkan logika tersebut di dalam fungsi terpisah dan kembalikan kesalahan dari sana. Hal ini akan mempersingkat fungsi main() dan menempatkan semua logika bisnis kritis di dalam fungsi terpisah yang dapat diuji.

Cara yang tidak disarankan:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("file tidak ditemukan")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

Cara yang disarankan:

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("file tidak ditemukan")
  }
  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
  }
  // ...
}

Contoh di atas menggunakan log.Fatal, namun pedoman ini juga berlaku untuk os.Exit atau kode pustaka apa pun yang memanggil os.Exit.

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

Anda dapat mengubah tanda tangan run() sesuai kebutuhan. Misalnya, jika program Anda harus keluar dengan kode kegagalan tertentu, run() dapat mengembalikan kode keluar daripada kesalahan. Ini juga memungkinkan pengujian unit untuk secara langsung memvalidasi perilaku ini.

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

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

Harap dicatat bahwa fungsi run() yang digunakan dalam contoh-contoh ini tidak wajib ada. Nama, tanda tangan, dan pengaturan fungsi run() bersifat fleksibel. Di antara hal lain, Anda dapat:

  • Menerima argumen baris perintah yang belum diproses (misalnya, run(os.Args[1:]))
  • Memproses argumen baris perintah di dalam main() dan meneruskannya ke run
  • Mengembalikan kode keluar kembali ke main() dengan jenis kesalahan kustom
  • Menempatkan logika bisnis pada tingkat abstraksi yang berbeda dalam package main

Pedoman ini hanya mengharuskan adanya tempat di main() yang bertanggung jawab atas aliran keluar sebenarnya.