Một chút về hàm main trong Golang

Chào các bác,

Khi các bác viết code Golang, đặc biệt là với các bác mới học Go, các bác sẽ rất dễ sa đà vào việc ném nhiều code vào hàm main. Với những chương trình nhỏ thì cũng oke thôi, nhưng với những chương trình lớn hơn thì nó lại trở thành một vấn đề.

Ví dụ, các bác có thể cố nhét code khởi tạo đủ thứ vào hàm main như này:

db, err := postgresql.Connect(...)
if err != nil {
  panic(err)
}
app := MyApp{db}
app.Start()

Ngày xưa lúc mới học Golang tôi cũng bị vướng vào thói quen này. Hôm nay tôi sẽ chia sẻ với các bác một cách tiếp cận khác: dùng một hàm run() trả về lỗi. Mục tiêu khi chúng ta làm điều này là để giúp cho việc thay đổi đầu vào và đầu ra (input/output) của chương trình trở nên dễ dàng hơn, cũng như giúp cho việc trả lỗi về và kết thúc chương trình đúng cách. Những mục tiêu này giúp kiểm thử và bảo trì code hiệu quả hơn, đem lại ít gánh nặng về mặt kỹ thuật hơn trong tương lai.

Về cơ bản thì ý tưởng chỉ là kéo tất cả logic ra khỏi hàm main và đưa chúng vào một hàm run() ở ngoài. Sau đó chúng ta chỉ việc gọi hàm run() này, kiểm tra xem có lỗi không và kết thúc chương trình.

func main() {
  if err := run(); err != nil {
    fmt.Fprintf(os.Stderr, "%s\n", err)
    os.Exit(exitFail)
  }
}

func run() error {
  db, err := postgresql.Connect(...)
  if err != nil {
    return fmt.Errorf("connecting to db: %w", err)
  }
  app := MyApp{db}
  return app.Start()
}

Nếu các bác đã từng sử dụng thư viện cobra, các bác sẽ thấy cách làm này không có gì mới mẻ cả. Lợi ích đầu tiên là chương trình của chúng ta có thể log lại lỗi và thoát luôn trong một chỗ. Không làm như này thì hàm main có thể sẽ tồn tại 3 hay 4 chỗ kiểm tra lỗi rồi gọi os.Exit(). Nó cũng loại bỏ sự cần thiết phải gọi panic trong hàm run, vì chúng ta chỉ đơn giản là trả về lỗi và thoát chương trình.

Chúng ta cũng có thể mở rộng việc này để làm việc kiểm thử trở nên đơn giản hơn. Cụ thể là các bác có thể cung cấp cho hàm run() một cái log writer, khiến cho chương trình của chúng ta đẩy log ra standard output nhưng đồng thời cũng cho phép việc log những thứ khác khi test.

func main() {
  if err := run(os.Stdout); err != nil {
    // ...
  }
}

func run(stdout io.Writer) error {
  // ...
}

Các bác cũng có thể làm điều này với các tham số của command line (command line arguments) để làm đơn giản hóa việc kiểm thử.

func main() {
  if err := run(os.Args, os.Stdout); err != nil {
    // ...
  }
}

func run(args []string, stdout io.Writer) error {
  // ...
}

Mặc dù đây chỉ là một thay đổi nhỏ, tôi thấy nó làm cho code sạch đẹp hơn mà không gây ra vấn đề gì. Nếu các bác dùng IDE như Goland hay VSCode, các bác có thể tạo sẵn templates/snippets để làm khung chương trình trong vài phút và tận dụng chúng để giúp code chương trình nhanh hơn.

Chúc các bác code vui!