Go main-run pattern
Tags: go, beginners
Published: 2023-09-12 21:24:47

I'll try to keep this short. It's just something I have seen way too many times and that has always resulted in poor code down the road.

Most example code online will have you write your main function as follows:

```go
func main() {
	client := somelib.NewClient("https://example.com")
	err := client.DoSomething()
	if err != nil {
		log.Fatalf("Error: %v", err)
	}
}
```

For a simple hello world example, this is fine. However, it then often leads to the following code:

```go
func main() {
	cfg, err := loadConfig()
	if err != nil {
		log.Fatalf("Error: %v", err)
	}

	db, err := openDB(cfg.DB)
	if err != nil {
		log.Fatalf("Error: %v", err)
	}
	defer db.Close()

	client := somelib.NewClient(cfg.ClientURL)
	err := client.DoSomething()
	if err != nil {
		log.Fatalf("Error: %v", err)
	}
}
```

It gets even worse if you are using a custom logging package that doesn't have `Fatalf`.

```go
func main() {
	cfg, err := loadConfig()
	if err != nil {
		log.Printf("Error: %v", err)
		os.Exit(1)
	}

	db, err := openDB(cfg.DB)
	if err != nil {
		log.Printf("Error: %v", err)
		os.Exit(1)
	}
	defer db.Close()

	client := somelib.NewClient(cfg.ClientURL)
	err := client.DoSomething()
	if err != nil {
		log.Printf("Error: %v", err)
		os.Exit(1)
	}
}
```

There are a number of problems with this code.

1. If you happen to forget `os.Exit` in some error branch, you will likely visit panic-land.
2. The code gets bloated. I have seen code that has 10+ such checks in its main function.
3. Any decent linter will complain that `defer db.Close()` might not get called since `os.Exit` halts the program immediately and no cleanup is performed.

## The main-run pattern

The `main-run` pattern (not an official name, just something I have used to reference it throughout the years in internal discussions) is as trivial as it gets.

```go
func main() {
	if err := runApp(); err != nil {
		log.Printf("Error: %v", err)
		os.Exit(1)
	}
}

func runApp() error {
	cfg, err := loadConfig()
	if err != nil {
		return fmt.Errorf("error loading config: %w", err)
	}

	db, err := openDB(cfg.DB)
	if err != nil {
		return fmt.Errorf("error connecting to db: %w", err)
	}
	defer db.Close()

	client := somelib.NewClient(cfg.ClientURL)
	err := client.DoSomething()
	if err != nil {
		return fmt.Errorf("error calling client: %w", err)
	}
	return nil
}
```

With larger main functions, this is clearly better:
1. There is only one place that has `os.Exit` so its hard to forget it.
2. The code is much more compact and straight to the point.
3. The defer statements are run even on failure, ensuring linters are happy and proper cleanup is performed.

### Signal handling

With this new design, there is also an elegant way to introduce a lifecycle context to the function.

```go
func main() {
	ctx, ctxDone := signal.NotifyContext(context.Background(), os.Interrupt)
	defer ctxDone()
	if err := runApp(ctx); err != nil {
		log.Printf("Error: %v", err)
		os.Exit(1)
	}
}

func runApp(ctx context.Context) error {
	...
	return nil
}
```

### Custom error codes

Ok, you might object, saying that the original design allows for custom exit codes.

To be honest, I have rarely seen an app that really needs that. But even if you do want custom exit codes, you can always use custom errors and a mapping function.

```go
func main() {
	ctx, ctxDone := signal.NotifyContext(context.Background(), os.Interrupt)
	defer ctxDone()
	if err := runApp(ctx); err != nil {
		log.Printf("Error: %v", err)
		os.Exit(exitCode(err))
	}
}

var ErrConfigNotFound = errors.New("config not found")
var ErrDatabaseOffline = errors.New("database offline")

func exitCode(err error) int {
	switch {
	case errors.Is(err, ErrConfigNotFound):
		return 2
	case errors.Is(err, ErrDatabaseOffline):
		return 3
	default:
		return 1
	}
}
```

## Summary

Well, that's pretty much it. Just something to keep in mind. And even if you don't like the proposed approach, use whatever option best suits you, just make sure to avoid the pitfalls that the `os.Exit` approach has.