Design - Software Engineering

Purpose of func main()

The purpose of func main() is to translate commandline arguments to application startup state. Once the state is prepared a specific entry function is called. More often than not, logging verbosity is one such state that needs to be configured early on.
Go provides the builtin flag package to define, document and parse the arguments.

Example CountStars(galaxy string)

Imagine an application that counts the stars in a named galaxy. The main function should then make sure the options are correct and forward them as arguments to the function doing the actual work. The name of the galaxy would be such an option and perhaps a verbosity flag for debugging purposes.

func main() {
	galaxy := "milkyway"
	flag.StringVar(
		&galaxy, "galaxy",
		galaxy, "name of galaxy to count stars in",
	)
	verbose := false
	flag.BoolVar(
		&verbose, "verbose",
		verbose, "enables verbose logging",
	)
	flag.Parse()
	SetVerbose(verbose)
	CountStars(os.Stdout, galaxy)
}

Now that you know what the main function should do, let us take a look at how to do it, apart of the option definition and argument passing.
First, the cyclomatic complexity of the main function is one. Ie. there is only one path through this program. There are however two exit points, apart from the obvious one flag.Parse() exits if the parsed options do not match the predefined. The single pathway means that testing the main function is simple. Execute this application with valid arguments and all lines are covered, leaving all other code for unittesting.
Also, if you execute the program you would note that second, the order of the options are sorted in the same way as the help output.

Cyclomatic complexity should be one.
Option order should match output.

Benefits

Adhering to the “keep it simple principle” and only doing one thing in each function, works out nicely for the main function as well. One could argue that, if you moved everything inside main into a start function, the option definitions would also be tested. Think about it for a minute and figure out what exactly you would be testing. If the flag package already makes sure it's functions work as expected the only thing left is testing which options you have defined. They would need to be updated each time you add or remove an option which is a sign of a poor test.
You could potentially refactor main and separate the option definitions into smaller functions for readability but you still wouldn't need to write unittests for them. However, when your application grows and command line arguments start having relations you ought to verify that. More on this in the next section.

But start of and keep main simple, constrain it to only set global startup state before calling the one function that does the actual work.
This works great for services and simpler commands that only do one thing.

More advanced commands

When the commands get more complex with many more options the above approach has its limits. Number of arguments to CountStars will grow and become hard to verify any relations between them. One solution is to turn func CountStars into a command. Advanced commands may also have logic for combination of options which would suggest you should verify command execution with various options. This is impossible to do with the above approach while tracking coverage.

Run is now testable and complexity can grow slightly
Alternate cmdline package for parsing arguments.
package main

import (
	"fmt"

	"github.com/gregoryv/cmdline"
	"github.com/gregoryv/fox"
	"github.com/gregoryv/wolf"
)

// NewStarCounter returns a new star counter command with logging
// enabled to cmd.Stderr.
func NewStarCounter(cmd wolf.Command) *StarCounter {
	return &StarCounter{
		Command: cmd,
		Logger:  fox.NewSyncLog(cmd.Stderr()).FilterEmpty(),
	}
}

type StarCounter struct {
	wolf.Command
	fox.Logger

	size   string
	weight string
}

// Run starts the application and waits for it to complete. Returns
// exit code 0 if completed ok, 1 otherwise.
func (me *StarCounter) Run() int {
	// Parse command line options
	var (
		cli    = cmdline.New(me.Args()...)
		size   = cli.Option("-size").String("all")
		weight = cli.Option("-weight").String("all")
		help   = cli.Flag("-h, --help")
	)

	switch {
	case help:
		cli.WriteUsageTo(me.Stdout())
		return 0

	case !cli.Ok():
		me.Log(cli.Error(), ", try --help")
		return 1
	}
	me.size = size
	me.weight = weight
	if err := me.countStars(); err != nil {
		return 1
	}
	return 0
}

// countStars writes the result using the configured Stdout writer
func (me *StarCounter) countStars() error {
	// count stars using filters
	if me.weight != "all" {
		return fmt.Errorf("bad weight")
	}
	return nil
}

Testing complex patterns is straight forward.

package main

import (
	"strings"
	"testing"

	"github.com/gregoryv/wolf"
)

func TestStarCounter(t *testing.T) {
	exp := func(exitCode int, args ...string) {
		tc := strings.Join(args, " ")
		t.Run(tc, func(t *testing.T) {
			cmd := wolf.NewTCmd(args...).Use(t)
			defer cmd.Cleanup()
			sc := NewStarCounter(cmd)
			got := sc.Run()
			if got != exitCode {
				t.Error(args, "got", got, "expected", exitCode)
			}
		})
	}

	exp(0, "name")
	exp(0, "name", "-h")
	exp(0, "name", "--help")
	exp(0, "name", "-size", "small")
	exp(1, "name", "-weight", "heavy")
	exp(1, "", "-nosuch")
}

Complexity of func main still remains at one, looking like

package main

import (
	"os"

	"github.com/gregoryv/wolf"
)

func main() {
	var (
		cmd  = wolf.NewOSCmd()
		sc   = NewStarCounter(cmd)
		code = sc.Run()
	)
	os.Exit(code)
}