مواد فنی

برنامه‌های Go از os.Exit یا log.Fatal* برای خروج فوری (استفاده از panic یک راه مناسب برای خروج از برنامه نیست، لطفاً از panic استفاده نکنید) استفاده می‌کنند.

فقط در main() یکی از os.Exit یا log.Fatal* را فراخوانی کنید. همه توابع دیگر باید خطاها را به تماس‌گیرنده برگردانند.

توصیه نمی‌شود:

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

توصیه می‌شود:

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
}

اصولاً برنامه‌هایی که دارای چند نقطه خروج هستند، مشکلاتی دارند:

  • جریان کنترل مبهم: هر تابع می‌تواند برنامه را خروجی دهد که جلوگیری از تصور درباره جریان کنترل را دشوار می‌کند.
  • دشواری در تست: توابعی که برنامه را خروجی می‌دهند، همچنین تست‌هایی که آن‌ها را فراخوانی می‌کنند را نیز خروجی می‌دهند. این باعث دشواری تست توابع و مخاطره از دست دادن سایر تست‌های هنوز اجرا نشده توسط go test می‌شود.
  • گذشتن از تمیز کاری: وقتی یک تابع برنامه را خروجی می‌دهد، هر تماس تاخیرانداز را نادیده می‌گیرد. این باعث افزایش مخاطره گذشتن از وظایف تمیز کاری مهم می‌شود.

خروج یک‌باری

اگر امکان دارد، تنها باید حداکثر یک فراخوانی به os.Exit یا log.Fatal در main() شما وجود داشته باشد. اگر چندین سناریو خطا وجود داشته باشد که نیاز به متوقف کردن اجرای برنامه دارند، منطق را در یک تابع جداگانه قرار دهید و خطا را از آنجا برگردانید. این باعث کوتاه شدن تابع main() و قرار دادن تمام منطق تجاری حیاتی در یک تابع جداگانه و قابل تست می‌شود.

روش توصیه نمی‌شود:

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()
  // If we call log.Fatal after this line
  // f.Close will be executed.
  b, err := os.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

روش توصیه شده:

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
  }
  // ...
}

مثال فوق از log.Fatal استفاده می‌کند، اما این رهنمود همچنین برای os.Exit و یا هر کد کتابخانه‌ای که os.Exit را فراخوانی می‌کند، اعمال می‌شود.

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

می‌توانید امضای run() را به دلخواه تغییر دهید. به عنوان مثال، اگر برنامه شما باید با یک کد خطای خاص خروج کند، run() می‌تواند کد خروج را به جای یک خطا برگرداند. همچنین این امکان را فراهم می‌کند که تست‌های واحد به‌طور مستقیم این رفتار را تأیید کنند.

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

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

لطفاً توجه داشته باشید که تابع run() استفاده شده در این مثال‌ها الزامی نیست. نام، امضا و راه‌اندازی تابع run() انعطاف‌پذیر است. از جمله موارد دیگر، می‌توانید:

  • آرگومان‌های خط فرمان برنامه‌ای ناپردازده را بپذیرید (به عنوان مثال، run(os.Args[1:]))
  • آرگومان‌های خط فرمان را در main() تجزیه کرده و به run منتقل کنید
  • کد تخصیص منطقی را به دسته بندی دیگری مانند package main ببرید

این رهنمود تنها نیازمندی دارد که در main() مسئول جریان خروج واقعی باشد.