DEV Community

Momchil Atanasov
Momchil Atanasov

Posted on

5 1 1 1 1

Go main-run pattern

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:

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

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

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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.

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay