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.modandgo.sum-- module paths, minimum version selection (MVS),require,replace, andexcludedirectives - 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-lintwith.golangci.yml-- enabling, disabling, or tuning specific linters - Writing a
Makefilewith standard Go targets:build,test,lint,generate,tidy,vet - Understanding the
internal/andpkg/directory conventions and when each applies - Managing Go toolchain versions, including
godirective ingo.modandGOTOOLCHAINenvironment 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,
syncprimitives, or concurrent data structures -- usego-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) -- usego-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 (
mainpackage incmd/), an HTTP/gRPC service (singlecmd/entry point withinternal/domain logic), a shared library (nocmd/, 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 thegodirective ingo.modto 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 resultinggo.modwill contain only themoduleandgodirectives initially. - Set the Go version explicitly: Edit
go.modto set thegodirective 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.3line to pin the exact toolchain version used for development, whilego 1.22.0sets 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-bat the repository root. Thego.workfile coordinates local module resolution without requiringreplacedirectives in individualgo.modfiles. Commitgo.workfor development, but do not commitgo.workif it breaks published modules -- add it to.gitignorefor libraries, keep it for services. - Add the first dependency correctly: Run
go get github.com/pkg/dependency@v1.2.3to add a versioned dependency. Never manually editgo.sum. Rungo mod tidyafter any dependency addition or removal to synchronizego.sumand 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 undercmd/is a separate binary. The directory name becomes the binary name by convention. Each contains exactly onemain.go(or a small set of.gofiles all inpackage main). Keepcmd/thin -- 50-100 lines at most. All logic belongs ininternal/orpkg/.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. Useinternal/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), skippkg/entirely and put everything ininternal/. For libraries, the public API often lives at the module root or in named subdirectories.api/: Protocol Buffer definitions (.protofiles), OpenAPI specs (.yaml), or GraphQL schemas. Keep generated code out ofapi/-- put it ininternal/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 theMakefile. Keep scripts short and POSIX-compatible unless the team has standardized on Bash.build/: Dockerfiles, CI configuration templates, package build scripts. Separate fromscripts/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
.PHONYfor all non-file targets:build,test,lint,fmt,vet,generate,tidy,cleanare all phony. Missing.PHONYcauses 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
testandtest-integrationtargets:make testrunsgo test -race -count=1 ./...(unit tests only, with race detector).make test-integrationadds a build tag:go test -race -count=1 -tags=integration ./.... This prevents long-running integration tests from blocking fast feedback. - The
linttarget runsgolangci-lint run ./.... Pin the golangci-lint version in the Makefile or install script to prevent "works on my machine" linting discrepancies. TheGOLANGCI_LINT_VERSIONvariable at the top of the Makefile should match the version in CI. - The
generatetarget runsgo generate ./.... Document in comments which toolsgo:generatedirectives invoke (e.g.,mockgen,stringer,protoc-gen-go). Add atools.gofile totools/or the module root to pin generator tool versions. - The
tidytarget runsgo mod tidy -vand, in workspace mode,go work sync. Include atidy-checkvariant that fails ifgo mod tidyproduces a diff -- use this in CI. - Use
$(MAKE)notmakefor recursive invocations to inherit the parentmake'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.ymlversionkey (or the--versionflag) 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 forgolint),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:
gocyclothreshold at 15 for warnings is a good starting point.reviveshould haveconfidence: 0.8to reduce noise.gocriticshould enable thediagnosticandperformancetag groups. - Use
issues.exclude-rulesselectively: Exclude test files fromerrcheckfort.Log,t.Errorcalls. Exclude generated files from all linters usingissues.exclude-files: [".*\\.pb\\.go", ".*_gen\\.go"]. Never use blanket excludes on non-generated code. - Set
run.timeout: 5mto prevent CI hangs on large codebases. Setrun.concurrencyto0(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: trueflag 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
.gitignoreentries:/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.workandgo.work.sumto.gitignorefor library modules. Commit them for application monorepos where the workspace is the canonical development environment. tools.gopattern: Create a file attools/tools.go(or the module root) with//go:build toolsbuild tag and blank imports of tool binaries (e.g.,_ "github.com/golangci/golangci-lint/cmd/golangci-lint"). This pins generator and tool versions ingo.modwithout pulling them into the build output.- Set up
pre-commithooks (viapre-commitframework orgit hooks) to rungofmt -l .and fail if any files are not formatted. A one-linerMakefiletargetmake hooksshould 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 tidyand confirmgo.modandgo.sumare unchanged -- they should not change on a clean project. - In workspace mode, run
go work syncand 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
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
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: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
Never change a module path after it has been published. The module path is embedded in every
go.sumentry 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.Never manually edit
go.sum. This file is a cryptographic hash registry maintained by thegotoolchain. Manual edits corrupt the integrity database. Always usego get,go mod tidy, orgo mod downloadto update it.The
replacedirective ingo.modis a local-development escape hatch, not a long-term solution.replacedirectives in a published library break consumers who cannot replicate your local path. Usereplaceonly during active development, and remove it before tagging a release. For persistent forks, publish the fork under a new module path.Do not put test helpers in a non-test file just to share them across packages. Go allows
_test.gofiles in a package to export symbols -- useexport_test.goto expose internal symbols for testing without polluting the non-test API. This pattern keeps internal/ truly internal while still enabling thorough testing.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 underinternal/. This prevents accidental surface area growth and makes refactoring dramatically cheaper. A common mistake is putting everything inpkg/"just in case" someone needs it -- resist this.The
godirective ingo.modis not merely advisory in Go 1.21+. In Go 1.21 and later, if thegodirective specifies a version higher than the installed toolchain, the toolchain will attempt to download the required version (ifGOTOOLCHAIN=auto) or fail with a clear error (ifGOTOOLCHAIN=local). SetGOTOOLCHAIN=localin CI to prevent silent toolchain downloads that change build behavior.golangci-lintmust 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.Do not enable every golangci-lint linter. Some linters are mutually exclusive, some have very high false-positive rates for idiomatic Go (e.g.,
funlenwithout careful tuning,lllfor long string literals,gomndfor 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.cmd/directories must contain onlymainpackages, andmain.gomust do almost nothing. The entry point should parse flags, build the dependency graph (wire upinternal/components), and call arun()function that returns an error. All business logic belongs ininternal/. This makes the binary testable at the integration level by importing the non-main packages.go mod tidymust be a required CI check, not just a developer convenience. Ifgo.modorgo.sumdrift from the actual dependency graph, builds become non-reproducible. Add amake tidy-checktarget that runsgo mod tidyand thengit 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
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
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
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
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: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
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.