What's in your main-dot-go? (aka New Go Project Boilerplate)

Sometimes I write small services in Go from scratch. And every time main.go ends up looking almost the same:

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigs := make(chan os.Signal, 2)
	signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)

	go func() {
		<-sigs
		signal.Stop(sigs)
		cancel()
	}()

	if err := run(ctx, os.Args[1:]); err != nil {
		log.Fatalln(err)
	}
}

type Config struct {
	HTTPAddr            string
	HTTPShutdownTimeout time.Duration
}

func run(ctx context.Context, args []string) error {
	flags := flag.NewFlagSet("", flag.ExitOnError)

	var conf Config
	flags.StringVar(&conf.HTTPAddr, "http-addr", "127.0.0.1:10080", "address to listen on")
	flags.DurationVar(&conf.HTTPShutdownTimeout, "http-shutdown-timeout", 5*time.Second, "server shutdown timeout")

	if err := flags.Parse(args); err != nil {
		return err
	}

	// TODO: define the handler, the routing, and wire the dependencies with the main context
	mux := http.NewServeMux()
	server := &http.Server{
			Addr:    conf.HTTPAddr,
			Handler: mux,
	}

	errs := make(chan error, 1)
	go func() {
			log.Printf("starting: addr %s", server.Addr)
			errs <- server.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
			log.Println("exiting...")
	case err := <-errs:
			return err
	}

	// create new context because top-most one is already canceled
	ctx, cancel := context.WithTimeout(context.Background(), conf.HTTPShutdownTimeout)
	defer cancel()

	return server.Shutdown(ctx)
}

Of course, not every service requires an HTTP server, but the general idea stands.


When Go will introduce signal.NotifyContext(), the signals handling in main() function will be much smaller (we’re at go1.15rc1 as I’m writing that and the change hasn’t landed into the release yet).


I love how transparent is the flow here and how everything is scoped inside run() function. This structure forces you to eliminate global state, making unit or integration testing almost trivial — at least, in theory ;)

It might feel like too much of boilerplate code for a “small” service. In practice, though, I don’t recall any time this caused any real troubles to me. The beauty of Go is in explicitness.