# Go Read [conventions.md](/conventions.md) first — this page only covers what is specific to Go. ## Layout ``` cmd// one directory per binary; main.go and nothing else of substance internal// everything real; the compiler forbids other modules importing it ``` Put logic in `internal/`, not in `main.go`. Code in `main` cannot be tested by anything else and cannot be reused. `main` should read arguments, wire things together, and call into `internal`. ## Errors Wrap with `%w` and say what was being attempted. The verb belongs at the start: ```go if err := os.WriteFile(path, data, 0o600); err != nil { return fmt.Errorf("write firewall rules to %s: %w", path, err) } ``` Never `_ = err`. Never a bare `panic` outside `main` — return the error and let the caller decide. Compare with `errors.Is` and `errors.As`, never by string matching on the message. Messages are for humans and will be reworded. ## Tests Table-driven, with a `name` on every case so a failure says which one: ```go for _, tc := range []struct{ name, in string; want int }{ {"empty input is rejected", "", 0}, {"leading hyphen reads as a flag", "-rf", 0}, } { t.Run(tc.name, func(t *testing.T) { ... }) } ``` Run `go test ./...` before every commit. `go vet ./...` too — it catches real bugs, not style. ## Concurrency Do not reach for a goroutine because the work looks parallel. Reach for one when something genuinely waits — network, disk, a subprocess. Every goroutine needs a clear answer to "who stops this, and when". Pass a `context.Context` as the first argument of anything that blocks, and honour it. A goroutine with no way to be cancelled is a leak with a delay on it. Run tests with `-race` when anything is shared. ## Running commands Use `exec.Command` with separate arguments. Never build a command as one string and hand it to a shell — that is how a name like `a; rm -rf /` becomes a disaster. ```go // GOOD: the name is one argument, whatever characters it contains exec.Command("incus", "delete", "--force", name) // NEVER exec.Command("sh", "-c", "incus delete --force "+name) ``` ## Building for the server The server has no Go toolchain and no C library you should depend on. Build a single static binary on your own machine and copy it over: ```bash CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \ -ldflags "-s -w -X main.version=$(git describe --tags --always --dirty)" \ -o harbor ./cmd/harbor ``` `CGO_ENABLED=0` is what makes it static — without it the binary depends on the build machine's C library and may not run on the server. `-trimpath` keeps local directory names out of the binary. ## Dependencies The standard library covers more than people expect: HTTP client and server, JSON, TLS, templating, testing, crypto. Check it before adding anything. Commit `go.sum`. Run `go mod tidy` before committing.