The cmd Directory and Multiple Binaries in Go

How to build several programs from one Go module with cmd folders, keep main packages thin, and build or install each binary.

A single module often produces more than one program: an API server, a background worker, an admin CLI. Each program needs its own package main with its own func main(). The usual convention is one folder per program under cmd/.

The Layout

shop/
  go.mod                       module example.com/shop
  cmd/
    shop-api/main.go           package main
    shop-worker/main.go        package main
  internal/
    order/order.go             package order

The folder name becomes the binary name. go build ./cmd/shop-api produces an executable called shop-api (or shop-api.exe on Windows).

Building and Running Each Program

go run ./cmd/shop-api
go build ./cmd/shop-worker
go build -o bin/ ./cmd/...
go install ./cmd/shopctl
  • go run ./cmd/shop-api compiles and runs one program.
  • go build -o bin/ ./cmd/... builds every program under cmd/ into a bin/ folder.
  • go install puts the binary in $GOBIN, or $HOME/go/bin by default.

Other people can install a program from your module directly:

go install example.com/shop/cmd/shopctl@latest

Two main packages in one folder:

Putting server.go and cli.go, each with func main(), in the same folder fails with main redeclared in this block. Each program needs its own folder.

Keep main Thin

The code in a cmd/ folder should only start the program: read flags and config, create dependencies, and run. A clean pattern is a run function that returns an error:

// file: cmd/greeter/main.go
package main

import (
	"flag"
	"fmt"
	"io"
	"os"
)

func run(args []string, out io.Writer) error {
	fs := flag.NewFlagSet("greeter", flag.ContinueOnError)
	name := fs.String("name", "world", "who to greet")
	if err := fs.Parse(args); err != nil {
		return err
	}
	fmt.Fprintf(out, "hello, %s\n", *name)
	return nil
}

func main() {
	if err := run(os.Args[1:], os.Stdout); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

Running go run ./cmd/greeter -name asha prints hello, asha. Because run takes its arguments and output writer as parameters, a test can call it directly and check the output without starting a process.

Business logic, such as how orders are priced, belongs in internal/ packages, so both the API and the worker can share it.

One Binary at the Root Is Fine Too

If a module only ever builds one program, main.go at the root is simpler, and go install example.com/mytool@latest then installs a binary named mytool. Move to cmd/ when a second program appears.

Importing a main package:

A package main cannot be imported by other packages. If two programs need the same code, it must move into a normal package such as internal/order. Trying to import a cmd/ folder fails with import "example.com/shop/cmd/shop-api" is a program, not an importable package.

Well-organized binaries:

Each program has its own folder under cmd/, each main.go is short and mostly wiring, and all shared logic lives in importable packages under internal/.

Summary

Put each program in its own folder under cmd/, where the folder name becomes the binary name. Build them with go build ./cmd/name, all at once with ./cmd/..., or install them with go install. Keep main packages thin by moving logic into internal/ packages and using a run function that returns an error, and stay with a root main.go while you only have one program.