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)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay