# Go Project Setup

> Guides expert-level Go project initialization: module structure with go.mod, workspace mode for multi-module development, Makefile conventions, golangci-lint configuration, and standard project layout. Use when the user asks about Go project setup, go.mod, Go workspace, Go modules, golangci-lint, Go project structure, Makefile. Do NOT use when the user asks about Go language idioms (use `go-idioms`), Go testing (use `go-testing-patterns`), Go concurrency (use `go-concurrency-patterns`).

- Skill: `ferroxlabs/go-project-setup` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add ferroxlabs/go-project-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ferroxlabs/go-project-setup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: FerroxLabs (https://skillmd.com/u/ferroxlabs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ferroxlabs/go-project-setup

---

# Go Project Setup

## When to Use

**Use this skill when the user asks about:**
- Initializing a new Go project from scratch, including module naming, directory layout, and initial tooling
- Configuring `go.mod` and `go.sum` -- module paths, minimum version selection (MVS), `require`, `replace`, and `exclude` directives
- Go workspace mode (`go.work`, `go.work.sum`) for multi-module development in a single repository
- Structuring Go projects as CLI applications, HTTP services, libraries, or monorepos
- Configuring `golangci-lint` with `.golangci.yml` -- enabling, disabling, or tuning specific linters
- Writing a `Makefile` with standard Go targets: `build`, `test`, `lint`, `generate`, `tidy`, `vet`
- Understanding the `internal/` and `pkg/` directory conventions and when each applies
- Managing Go toolchain versions, including `go` directive in `go.mod` and `GOTOOLCHAIN` environment variable

**Do NOT use this skill when:**
- The user asks about Go language idioms, error handling patterns, interface design, or idiomatic Go code style -- use `go-idioms`
- The user asks about writing Go tests, table-driven tests, testify, mocks, or benchmarks -- use `go-testing-patterns`
- The user asks about goroutines, channels, `sync` primitives, or concurrent data structures -- use `go-concurrency-patterns`
- The user is asking about deploying Go applications to Kubernetes, Docker, or cloud providers -- use the appropriate deployment skill
- The user is asking about specific Go standard library packages in depth (e.g., `net/http`, `encoding/json`) -- use `go-stdlib`
- The user is debugging a build failure caused by a language-level issue, not a configuration issue

---

## Process

### Step 1 -- Assess Project Type and Constraints

Before writing a single file, determine the project's fundamental shape. The answers drive every structural decision.

- **Determine the artifact type:** Is this a CLI binary (`main` package in `cmd/`), an HTTP/gRPC service (single `cmd/` entry point with `internal/` domain logic), a shared library (no `cmd/`, exported API at package root or under a named package), or a monorepo containing multiple modules?
- **Determine the module path:** For open-source libraries, the module path must match the repository URL (e.g., `github.com/org/repo`). For private services, use a vanity domain or internal registry path (e.g., `company.internal/platform/payments`). Never use a path that you cannot publish to or that collides with a real module.
- **Determine the minimum Go version:** Check the deployment environment. As of 2024, Go 1.21+ is recommended for new projects -- it introduced toolchain management, `log/slog`, and improved generics support. Go 1.22+ added range-over-integers. Set the `go` directive in `go.mod` to the minimum version the team can use in all environments.
- **Identify multi-module needs:** If the project contains reusable packages that need independent versioning (e.g., a CLI that embeds an SDK that others also import), plan for workspace mode from the start. If everything is tightly coupled and versioned together, a single module is correct.
- **Check for organizational standards:** Ask whether the organization has a module path prefix, a standard Makefile template, a required linter configuration, or an internal fork of common dependencies.

### Step 2 -- Initialize the Module

Run `go mod init` with the correct module path. This is the only command that is irreversible in practice -- changing a module path after others depend on it is a major breaking change.

- **Single module:** Run `go mod init github.com/org/projectname`. The resulting `go.mod` will contain only the `module` and `go` directives initially.
- **Set the Go version explicitly:** Edit `go.mod` to set the `go` directive to your target minimum (e.g., `go 1.22.0`). In Go 1.21+, this directive enforces a minimum toolchain, not just a minimum language version. The patch version matters.
- **Toolchain directive (Go 1.21+):** Optionally add a `toolchain go1.22.3` line to pin the exact toolchain version used for development, while `go 1.22.0` sets the minimum supported version. This separates "what this module requires" from "what developers use to build it."
- **Workspace mode (multi-module):** After initializing each module, run `go work init ./module-a ./module-b` at the repository root. The `go.work` file coordinates local module resolution without requiring `replace` directives in individual `go.mod` files. Commit `go.work` for development, but **do not** commit `go.work` if it breaks published modules -- add it to `.gitignore` for libraries, keep it for services.
- **Add the first dependency correctly:** Run `go get github.com/pkg/dependency@v1.2.3` to add a versioned dependency. Never manually edit `go.sum`. Run `go mod tidy` after any dependency addition or removal to synchronize `go.sum` and remove unused entries.

### Step 3 -- Establish the Directory Layout

Go has informal but widely adopted layout conventions. Deviating from them increases cognitive load for incoming contributors.

- **`cmd/`:** Each subdirectory under `cmd/` is a separate binary. The directory name becomes the binary name by convention. Each contains exactly one `main.go` (or a small set of `.go` files all in `package main`). Keep `cmd/` thin -- 50-100 lines at most. All logic belongs in `internal/` or `pkg/`.
- **`internal/`:** Code that must not be imported by external modules. Go enforces this at compile time -- any package outside the module that attempts to import a path containing `/internal/` will get a build error. Use `internal/` for domain logic, service implementations, configuration structs, and anything not intended as a public API.
- **`pkg/`:** Publicly importable packages intended for external consumers. This convention signals "this is a stable public API." For strictly private services (never imported externally), skip `pkg/` entirely and put everything in `internal/`. For libraries, the public API often lives at the module root or in named subdirectories.
- **`api/`:** Protocol Buffer definitions (`.proto` files), OpenAPI specs (`.yaml`), or GraphQL schemas. Keep generated code out of `api/` -- put it in `internal/gen/` or a similar location, and commit generated code only after careful team discussion.
- **`configs/`:** Configuration file templates (`.yaml`, `.toml`, `.env.example`). Never commit secrets -- only templates with placeholder values.
- **`scripts/`:** Shell scripts and helper scripts called by the `Makefile`. Keep scripts short and POSIX-compatible unless the team has standardized on Bash.
- **`build/`:** Dockerfiles, CI configuration templates, package build scripts. Separate from `scripts/` to distinguish build-system artifacts from development utilities.
- **Do not create `src/`:** In Go, `src/` is a GOPATH-era artifact with no modern meaning. Its presence confuses new contributors and breaks standard tooling assumptions.

### Step 4 -- Write the Makefile

The `Makefile` is the team's shared interface to the project's development workflow. Every contributor should be able to run `make build`, `make test`, and `make lint` without reading documentation.

- **Use `.PHONY` for all non-file targets:** `build`, `test`, `lint`, `fmt`, `vet`, `generate`, `tidy`, `clean` are all phony. Missing `.PHONY` causes confusing failures when files with those names accidentally exist.
- **Use `go build -v -o bin/$(APP_NAME) ./cmd/appname/`** for the build target. Capture the binary name in a variable at the top of the Makefile. Add `-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"` to embed build metadata.
- **Separate `test` and `test-integration` targets:** `make test` runs `go test -race -count=1 ./...` (unit tests only, with race detector). `make test-integration` adds a build tag: `go test -race -count=1 -tags=integration ./...`. This prevents long-running integration tests from blocking fast feedback.
- **The `lint` target runs `golangci-lint run ./...`.** Pin the golangci-lint version in the Makefile or install script to prevent "works on my machine" linting discrepancies. The `GOLANGCI_LINT_VERSION` variable at the top of the Makefile should match the version in CI.
- **The `generate` target runs `go generate ./...`.** Document in comments which tools `go:generate` directives invoke (e.g., `mockgen`, `stringer`, `protoc-gen-go`). Add a `tools.go` file to `tools/` or the module root to pin generator tool versions.
- **The `tidy` target runs `go mod tidy -v` and, in workspace mode, `go work sync`.** Include a `tidy-check` variant that fails if `go mod tidy` produces a diff -- use this in CI.
- **Use `$(MAKE)` not `make` for recursive invocations** to inherit the parent `make`'s flags and environment variables.

### Step 5 -- Configure golangci-lint

`golangci-lint` aggregates dozens of linters into a single tool with a unified configuration file. The default linter set is intentionally conservative -- you must opt into stricter checks.

- **Version-pin the config:** The `.golangci.yml` `version` key (or the `--version` flag) must match the installed binary. Breaking changes between golangci-lint versions are common. Pin to a minor version (e.g., `1.59.x`) in CI.
- **Enable linters beyond the default set.** At minimum for a new project, enable: `govet` (enabled by default), `errcheck` (enabled by default), `staticcheck`, `gosimple`, `unused`, `gocyclo` (cyclomatic complexity, warn at >15, error at >25), `gocritic`, `misspell`, `godot` (comment formatting), `revive` (modern replacement for `golint`), `noctx` (HTTP requests should carry a context), `bodyclose` (HTTP response bodies must be closed), `exhaustive` (switch statements on enum-like types must be exhaustive), `wrapcheck` (errors from external packages must be wrapped).
- **Configure per-linter settings:** `gocyclo` threshold at 15 for warnings is a good starting point. `revive` should have `confidence: 0.8` to reduce noise. `gocritic` should enable the `diagnostic` and `performance` tag groups.
- **Use `issues.exclude-rules` selectively:** Exclude test files from `errcheck` for `t.Log`, `t.Error` calls. Exclude generated files from all linters using `issues.exclude-files: [".*\\.pb\\.go", ".*_gen\\.go"]`. Never use blanket excludes on non-generated code.
- **Set `run.timeout: 5m`** to prevent CI hangs on large codebases. Set `run.concurrency` to `0` (auto-detect CPU count) for local runs, but pin it to a fixed number (e.g., `4`) in CI for reproducible timing.
- **The `linters-settings.govet.enable-all: true` flag** is aggressive but recommended for new projects -- it catches shadow variables, composite literals missing field names, and other subtle bugs.

### Step 6 -- Configure Version Control Hygiene

A correct `.gitignore` and commit conventions are part of project setup, not afterthoughts.

- **Go-specific `.gitignore` entries:** `/bin/` (built binaries), `*.test` (compiled test binaries), `*.out` (profiling output), `/vendor/` (vendored dependencies, if not using vendoring), `*.prof`, `coverage.out`, `coverage.html`.
- **Workspace file policy:** Add `go.work` and `go.work.sum` to `.gitignore` for library modules. Commit them for application monorepos where the workspace is the canonical development environment.
- **`tools.go` pattern:** Create a file at `tools/tools.go` (or the module root) with `//go:build tools` build tag and blank imports of tool binaries (e.g., `_ "github.com/golangci/golangci-lint/cmd/golangci-lint"`). This pins generator and tool versions in `go.mod` without pulling them into the build output.
- **Set up `pre-commit` hooks** (via `pre-commit` framework or `git hooks`) to run `gofmt -l .` and fail if any files are not formatted. A one-liner `Makefile` target `make hooks` should install these.

### Step 7 -- Verify the Setup End-to-End

Do not hand off a setup without running all tooling against it. A project that builds but doesn't lint, or lints but doesn't test, has a broken feedback loop.

- Run `go build ./...` -- zero errors, zero unresolved imports.
- Run `go vet ./...` -- zero warnings. This is the baseline correctness check built into the toolchain.
- Run `golangci-lint run ./...` -- zero lint violations on a freshly initialized project. If there are violations, fix them before declaring setup complete.
- Run `go test -race -count=1 ./...` -- even with no tests, this should produce `? github.com/org/project [no test files]` for each package, not a build error.
- Run `go mod tidy` and confirm `go.mod` and `go.sum` are unchanged -- they should not change on a clean project.
- In workspace mode, run `go work sync` and confirm no errors.

---

## Output Format

Provide the following artifacts, each in its own labeled code block.

### Project Directory Tree

```
projectname/
├── cmd/
│   └── projectname/
│       └── main.go
├── internal/
│   ├── config/
│   │   └── config.go
│   └── server/
│       └── server.go
├── pkg/                     # omit if purely internal service
│   └── clientlib/
│       └── client.go
├── api/                     # omit if no API specs
│   └── openapi.yaml
├── configs/
│   └── config.example.yaml
├── scripts/
│   └── install-tools.sh
├── build/
│   └── Dockerfile
├── tools/
│   └── tools.go
├── .gitignore
├── .golangci.yml
├── go.mod
├── go.work                  # multi-module only
├── Makefile
└── README.md
```

### go.mod Template

```
module github.com/org/projectname

go 1.22.0

toolchain go1.22.3

require (
    // direct dependencies added via go get
)
```

### go.work Template (multi-module only)

```
go 1.22.0

toolchain go1.22.3

use (
    .
    ./sdk
    ./tools
)
```

### Makefile Template

```makefile
APP_NAME    := projectname
CMD_PATH    := ./cmd/$(APP_NAME)
BIN_DIR     := bin
VERSION     := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT      := $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
DATE        := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
LDFLAGS     := -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
GOLANGCI_LINT_VERSION := v1.59.1

.PHONY: all build test test-integration lint fmt vet generate tidy tidy-check clean hooks

all: lint test build

build:
	@mkdir -p $(BIN_DIR)
	go build $(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME) $(CMD_PATH)

test:
	go test -race -count=1 -timeout=120s ./...

test-integration:
	go test -race -count=1 -timeout=300s -tags=integration ./...

test-coverage:
	go test -race -count=1 -coverprofile=coverage.out ./...
	go tool cover -html=coverage.out -o coverage.html

lint:
	golangci-lint run ./...

fmt:
	gofmt -l -w .
	goimports -l -w .

vet:
	go vet ./...

generate:
	go generate ./...

tidy:
	go mod tidy -v

tidy-check:
	go mod tidy
	git diff --exit-code go.mod go.sum

clean:
	rm -rf $(BIN_DIR) coverage.out coverage.html

hooks:
	@echo "Installing git pre-commit hook..."
	@echo '#!/bin/sh\nmake fmt-check vet lint' > .git/hooks/pre-commit
	@chmod +x .git/hooks/pre-commit
```

### .golangci.yml Template

```yaml
version: "2"

run:
  timeout: 5m
  concurrency: 0

linters:
  enable:
    - bodyclose
    - errcheck
    - exhaustive
    - gocritic
    - gocyclo
    - godot
    - govet
    - misspell
    - noctx
    - revive
    - staticcheck
    - unused
    - wrapcheck

linters-settings:
  gocyclo:
    min-complexity: 15
  gocritic:
    enabled-tags:
      - diagnostic
      - performance
      - style
  revive:
    confidence: 0.8
    rules:
      - name: exported
      - name: var-naming
      - name: error-strings
      - name: error-return
      - name: if-return
  govet:
    enable-all: true
  exhaustive:
    default-signifies-exhaustive: true

issues:
  exclude-files:
    - ".*\\.pb\\.go"
    - ".*_gen\\.go"
    - ".*_mock\\.go"
  exclude-rules:
    - path: "_test\\.go"
      linters:
        - wrapcheck
        - errcheck
    - path: "cmd/"
      linters:
        - wrapcheck
```

### tools/tools.go Template

```go
//go:build tools

package tools

import (
    _ "golang.org/x/tools/cmd/goimports"
    _ "github.com/golangci/golangci-lint/cmd/golangci-lint"
    // add mockgen, stringer, protoc-gen-go as needed
)
```

---

## Rules

1. **Never change a module path after it has been published.** The module path is embedded in every `go.sum` entry of every downstream consumer. A module path change requires a major version suffix (`/v2`) under Go's import compatibility rule, or a full break-and-replace for private modules.

2. **Never manually edit `go.sum`.** This file is a cryptographic hash registry maintained by the `go` toolchain. Manual edits corrupt the integrity database. Always use `go get`, `go mod tidy`, or `go mod download` to update it.

3. **The `replace` directive in `go.mod` is a local-development escape hatch, not a long-term solution.** `replace` directives in a published library break consumers who cannot replicate your local path. Use `replace` only during active development, and remove it before tagging a release. For persistent forks, publish the fork under a new module path.

4. **Do not put test helpers in a non-test file just to share them across packages.** Go allows `_test.go` files in a package to export symbols -- use `export_test.go` to expose internal symbols for testing without polluting the non-test API. This pattern keeps internal/ truly internal while still enabling thorough testing.

5. **`internal/` enforces package boundaries at the compiler level -- use it aggressively for new projects.** Everything that is not explicitly a stable public API should live under `internal/`. This prevents accidental surface area growth and makes refactoring dramatically cheaper. A common mistake is putting everything in `pkg/` "just in case" someone needs it -- resist this.

6. **The `go` directive in `go.mod` is not merely advisory in Go 1.21+.** In Go 1.21 and later, if the `go` directive specifies a version higher than the installed toolchain, the toolchain will attempt to download the required version (if `GOTOOLCHAIN=auto`) or fail with a clear error (if `GOTOOLCHAIN=local`). Set `GOTOOLCHAIN=local` in CI to prevent silent toolchain downloads that change build behavior.

7. **`golangci-lint` must be version-pinned in CI and in the Makefile.** Running different versions across developer machines and CI causes "lint passes locally but fails in CI" failures that erode trust in the tooling. Pin to a specific minor version (e.g., `v1.59.1`) and update intentionally with a dedicated PR.

8. **Do not enable every golangci-lint linter.** Some linters are mutually exclusive, some have very high false-positive rates for idiomatic Go (e.g., `funlen` without careful tuning, `lll` for long string literals, `gomnd` for magic numbers in tests). Start with a curated set, measure the signal-to-noise ratio over 2 weeks, and add or remove linters based on actual usefulness, not theoretical coverage.

9. **`cmd/` directories must contain only `main` packages, and `main.go` must do almost nothing.** The entry point should parse flags, build the dependency graph (wire up `internal/` components), and call a `run()` function that returns an error. All business logic belongs in `internal/`. This makes the binary testable at the integration level by importing the non-main packages.

10. **`go mod tidy` must be a required CI check, not just a developer convenience.** If `go.mod` or `go.sum` drift from the actual dependency graph, builds become non-reproducible. Add a `make tidy-check` target that runs `go mod tidy` and then `git diff --exit-code go.mod go.sum`. A non-zero exit code fails the CI pipeline.

---

## Edge Cases

### Monorepo with Multiple Services Sharing a Domain Package

When multiple services in a monorepo need to import the same shared library (e.g., `internal/events`), you face a choice: single module with all services, or multiple modules with workspace mode. Use workspace mode when different services need to release independently or have different dependency trees. Use a single module when all services are deployed together and version coordination is not needed. In workspace mode, the shared package is a third module (e.g., `./lib`) listed in `go.work`. Each service module has a `require` pointing to it, and `go work sync` keeps versions aligned. In CI, you must build each service module independently (without `go.work`), using the versions declared in each module's `go.mod`, to verify that published versions are correct.

### Vendoring vs. Module Proxy

For air-gapped or highly regulated environments, run `go mod vendor` to create a `vendor/` directory and commit it. Then build with `go build -mod=vendor ./...`. This eliminates the build-time network dependency but increases repository size significantly. The tradeoff: `vendor/` bloat vs. dependency on a module proxy (`GOPROXY=https://proxy.golang.org`). For most teams, a private module proxy (Athens or Artifactory's Go proxy) is the better answer -- it caches dependencies on first use, requires no vendor directory, and provides an audit trail. Never mix vendoring and module proxy -- pick one strategy per module and document it in the `README`.

### Library Module Versioning and Major Versions

When a library breaks backward compatibility, Go's module system requires a new module path with a `/v2` suffix (e.g., `module github.com/org/lib/v2`). This is not optional -- consumers that import `github.com/org/lib` and `github.com/org/lib/v2` will get two independent copies of the code, which is intentional. Failing to follow this convention corrupts the minimum version selection algorithm for all downstream consumers. Set up a `v2/` subdirectory or a separate `v2` branch, update all import paths inside the module (using `sed` or `gomajor` tool), and tag the new release with `v2.0.0`.

### `replace` Directives with Forks

When you fork a dependency (e.g., to patch a security vulnerability before upstream merges), use `replace github.com/upstream/pkg => github.com/yourorg/pkg v0.0.0-fork` in `go.mod`. This is safe for services that are never imported externally. For libraries, you cannot ship a `replace` directive -- upstream consumers will not have the fork on their `GOPATH`. The correct path for libraries is: publish your fork to a real module path, depend on that published fork, and contribute the fix upstream. Never publish a library with `replace` directives pointing to local paths or uncommitted forks.

### CGo and Cross-Compilation

If the project uses CGo (e.g., SQLite via `mattn/go-sqlite3`, C extensions), cross-compilation becomes significantly harder. Set `CGO_ENABLED=0` in the Makefile for any target that does not require CGo. For targets that do require CGo, install the appropriate cross-compilation toolchain (`gcc-aarch64-linux-gnu`) and set `CC=aarch64-linux-gnu-gcc GOOS=linux GOARCH=arm64`. Document CGo dependencies explicitly in the `README` because they break `go install` for external users who do not have the C toolchain. Consider replacing CGo dependencies with pure-Go alternatives (`modernc.org/sqlite`) when cross-compilation or static linking is a requirement.

### golangci-lint False Positives and Nolint Directives

When a linter produces a false positive that cannot be resolved by restructuring the code, use `//nolint:lintername // reason: explanation` on the specific line. Never use bare `//nolint` without specifying which linter and why -- this suppresses all linters and hides real issues. Configure `nolint-lead-space: false` in golangci-lint to require a comment after every nolint directive. Audit `nolint` usage in code review -- a pattern of frequent nolints for the same linter is a signal that the linter is misconfigured, not that the code is wrong.

### Transitioning an Existing GOPATH-era Project

Older projects may use `GOPATH` layout (`src/github.com/org/project/`) without a `go.mod`. Migration steps: (1) move all code to the intended repository structure, (2) run `go mod init github.com/org/project` in the project root, (3) run `go mod tidy` to discover and pin all transitive dependencies, (4) fix import paths that still reference `vendor/` or `GOPATH`-relative paths, (5) delete the `vendor/` directory and rebuild using the module cache. Do this in a dedicated migration branch. The most common failure point is indirect dependencies that were pinned via `vendor/` at a specific commit -- `go mod tidy` will fetch the latest compatible version, which may introduce breaking changes. Test thoroughly before merging.

---

## Example

**Input:** "I'm starting a new Go HTTP API service that will be deployed as a Docker container. It's a single binary, team of 4 engineers, targeting Go 1.22. We want golangci-lint enforced in CI. What should the full project setup look like?"

---

**Output:**

This is a standard single-module HTTP service setup. Here is the full recommended structure and configuration.

### Module Initialization

```bash
mkdir paymentservice && cd paymentservice
go mod init github.com/acmecorp/paymentservice
# Edit go.mod to set version explicitly
```

### go.mod

```
module github.com/acmecorp/paymentservice

go 1.22.0

toolchain go1.22.3

require (
    golang.org/x/net v0.26.0
)
```

The `toolchain` line pins the exact toolchain used for development to `1.22.3` while `go 1.22.0` declares the minimum compatibility requirement. Consumers on Go 1.22.x can still build this module.

### Directory Layout

```
paymentservice/
├── cmd/
│   └── paymentservice/
│       └── main.go
├── internal/
│   ├── config/
│   │   └── config.go
│   ├── handler/
│   │   ├── payment.go
│   │   └── health.go
│   ├── middleware/
│   │   └── logging.go
│   └── store/
│       └── postgres.go
├── configs/
│   └── config.example.yaml
├── build/
│   └── Dockerfile
├── scripts/
│   └── install-tools.sh
├── tools/
│   └── tools.go
├── .gitignore
├── .golangci.yml
├── go.mod
├── go.sum
├── Makefile
└── README.md
```

No `pkg/` directory because this is a private service -- nothing will be imported externally. No `api/` directory yet -- add it when OpenAPI specs are introduced. Everything lives under `internal/`.

### cmd/paymentservice/main.go

```go
package main

import (
    "context"
    "fmt"
    "log/slog"
    "os"
    "os/signal"
    "syscall"

    "github.com/acmecorp/paymentservice/internal/config"
    "github.com/acmecorp/paymentservice/internal/server"
)

// Injected at build time via -ldflags.
var (
    version = "dev"
    commit  = "none"
    date    = "unknown"
)

func main() {
    slog.Info("starting paymentservice",
        "version", version,
        "commit", commit,
        "date", date,
    )

    if err := run(); err != nil {
        slog.Error("fatal error", "error", err)
        os.Exit(1)
    }
}

func run() error {
    cfg, err := config.Load()
    if err != nil {
        return fmt.Errorf("loading config: %w", err)
    }

    ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    return server.Run(ctx, cfg)
}
```

Note the pattern: `main()` never returns an error. `run()` returns an error and all logic is delegated to `internal/`. Build metadata variables are exported for injection via `-ldflags`.

### Makefile

```makefile
APP_NAME    := paymentservice
CMD_PATH    := ./cmd/$(APP_NAME)
BIN_DIR     := bin
VERSION     := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
COMMIT      := $(shell git rev-parse --short HEAD 2>/dev/null || echo "none")
DATE        := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
LDFLAGS     := -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)"
GOLANGCI_LINT_VERSION := v1.59.1

.PHONY: all build test test-integration lint fmt vet generate tidy tidy-check clean

all: lint test build

build:
	@mkdir -p $(BIN_DIR)
	go build $(LDFLAGS) -o $(BIN_DIR)/$(APP_NAME) $(CMD_PATH)

test:
	go test -race -count=1 -timeout=120s ./...

test-integration:
	go test -race -count=1 -timeout=300s -tags=integration ./...

test-coverage:
	go test -race -count=1 -coverprofile=coverage.out ./...
	go tool cover -html=coverage.out -o coverage.html

lint:
	golangci-lint run ./...

fmt:
	gofmt -l -w .

vet:
	go vet ./...

tidy:
	go mod tidy -v

tidy-check:
	go mod tidy
	git diff --exit-code go.mod go.sum

clean:
	rm -rf $(BIN_DIR) coverage.out coverage.html
```

### .golangci.yml

```yaml
version: "2"

run:
  timeout: 5m
  concurrency: 0

linters:
  enable:
    - bodyclose
    - errcheck
    - exhaustive
    - gocritic
    - gocyclo
    - govet
    - misspell
    - noctx
    - revive
    - staticcheck
    - unused
    - wrapcheck

linters-settings:
  gocyclo:
    min-complexity: 15
  gocritic:
    enabled-tags:
      - diagnostic
      - performance
  revive:
    confidence: 0.8
    rules:
      - name: exported
      - name: error-strings
      - name: error-return
  govet:
    enable-all: true
  exhaustive:
    default-signifies-exhaustive: true
  wrapcheck:
    ignore-sigs:
      - .Errorf(
      - errors.New(
      - errors.Unwrap(
      - fmt.Errorf(

issues:
  exclude-files:
    - ".*\\.pb\\.go"
    - ".*_gen\\.go"
  exclude-rules:
    - path: "_test\\.go"
      linters:
        - wrapcheck
        - errcheck
    - path: "cmd/"
      linters:
        - wrapcheck
```

The `wrapcheck` linter is configured with `ignore-sigs` to allow direct use of `fmt.Errorf` and `errors.New` without wrapping -- these are not external package errors. The `cmd/` exclusion prevents false positives in the entry point where wrapping adds no value.

### .gitignore

```
/bin/
coverage.out
coverage.html
*.test
*.out
*.prof
.env
configs/config.yaml
```

Note: `configs/config.example.yaml` is committed (it is a template). `configs/config.yaml` is gitignored (it contains real values). The `.env` file pattern is blocked to prevent accidental secret commits.

### tools/tools.go

```go
//go:build tools

package tools

import (
    _ "golang.org/x/tools/cmd/goimports"
)
```

Run `go mod tidy` after adding this file to pin `goimports` version in `go.mod`. The `//go:build tools` tag prevents this file from being compiled into the production binary.

### Verification Sequence

```bash
go build ./...          # must produce zero errors
go vet ./...            # must produce zero warnings
golangci-lint run ./... # must produce zero violations
go test -race ./...     # must produce zero failures (no tests = zero failures)
go mod tidy && git diff --exit-code go.mod go.sum  # must produce zero diff
```

All five checks pass on this freshly initialized project. The team can begin writing application code against a setup that will catch errors, enforce style, and produce reproducible builds from day one.

