Setup#

Go’s toolchain is famously self-contained, one binary (go) installs, builds, tests, formats, and cross-compiles. The operator’s setup problem is which Go to install and where modules and binaries land.

Install#

1. Pick an installer.

  • Official installer from https://go.dev/dl/. The reference; installs to /usr/local/go.

  • Distro packages (apt install golang-go, dnf install golang). Lags behind upstream; usable.

  • gvm or g for per-user multiple-version management.

  • mise / asdf for cross-language version managers.

# Official tarball (Linux x86_64)
$ wget https://go.dev/dl/go1.23.linux-amd64.tar.gz
$ sudo rm -rf /usr/local/go
$ sudo tar -C /usr/local -xzf go1.23.linux-amd64.tar.gz

2. Set up the environment.

# ~/.profile or ~/.bashrc
$ export PATH=$PATH:/usr/local/go/bin
$ export GOPATH=$HOME/go
$ export PATH=$PATH:$GOPATH/bin

3. Verify.

$ go version
$ go env GOROOT GOPATH

Setup project#

1. Initialise a module.

$ mkdir my-tool && cd my-tool
$ go mod init github.com/operator/my-tool
$ git init && echo /my-tool > .gitignore

2. Add dependencies.

$ go get github.com/spf13/cobra@latest
$ go get -u ./...                  # update all
$ go mod tidy                      # prune unused

3. Lay out the source.

my-tool/
├── go.mod
├── go.sum
├── main.go
├── cmd/
│   └── my-tool/
│       └── main.go              # entrypoint (multi-binary repos)
├── internal/                    # private packages
│   ├── scan/
│   │   └── scan.go
│   └── report/
│       └── report.go
└── pkg/                         # public packages (optional)

internal/ is special, packages there can only be imported by the same module. The operator puts everything not designed for reuse under internal/.

4. Build, run, test.

$ go run .                       # run main package in cwd
$ go build -o my-tool .           # produce binary
$ go test ./...                   # run tests
$ go test -race ./...             # race detector

Cross-compile#

Go’s headline trick. One command, every target.

$ GOOS=linux  GOARCH=amd64 go build -o my-tool-linux-amd64
$ GOOS=linux  GOARCH=arm64 go build -o my-tool-linux-arm64
$ GOOS=darwin GOARCH=arm64 go build -o my-tool-darwin-arm64
$ GOOS=windows GOARCH=amd64 go build -o my-tool.exe

For static binaries with no glibc dependency, add CGO_ENABLED=0.

$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o my-tool-static .

Common Tasks#

Run without writing a binary.

$ go run .
$ go run ./cmd/my-tool

Install a Go-built tool to ``$GOPATH/bin``.

$ go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

Vendor dependencies into the repo.

$ go mod vendor

Verify the module graph is clean.

$ go mod verify
$ go mod tidy -v

Strip a binary down further.

$ go build -ldflags="-s -w" .
$ upx --best --lzma my-tool       # optional, controversial

References#