go-template/README.md
2026-03-09 20:01:08 +01:00

6.1 KiB

go-template

A batteries-included Go project template optimised for PoC and hobby projects. Clone it, rename the module, run make init, and you're coding.


Features

Area Tool Purpose
Language Go 1.24 Modules, toolchain directive
Logging zap Structured JSON logging + WithField extension
Config viper YAML file + env var overlay
Linting golangci-lint Aggregated linters, one config file
Security gosec + govulncheck SAST + dependency vulnerability scan
Mocks mockery v2 Type-safe, EXPECT()-style generated mocks
Tests testify Assertions (assert/require) + mock framework
Git hooks custom pre-commit gofmt + golangci-lint + gosec on every commit
Tool versions tools.go + go.mod Single source of truth for tool versions
Dev environment devcontainer Reproducible VSCode / GitHub Codespaces setup
IDE VSCode Launch configs, tasks, recommended settings

Getting started

Prerequisites

1. Clone and rename

git clone https://gitea.djmil.dev/djmil/go-template my-project
cd my-project

# Interactive rename — updates module path, config, devcontainer, and docs:
./rename.sh

2. Init (run once)

make init   # fetches deps, installs tools, configures git hooks

3. Build and run

make build  # compiles to ./bin/app
make run    # go run with config/dev.yaml

Daily workflow

make test          # run all tests
make test-race     # … with race detector
make lint          # golangci-lint
make lint-fix      # auto-fix what can be fixed
make security      # gosec + govulncheck
make mocks         # regenerate mocks after interface changes
make generate      # run all //go:generate directives

Keyboard shortcut (VSCode): Ctrl+Shift+B → build, Ctrl+Shift+T → test.


Project structure

.
├── cmd/
│   └── app/
│       └── main.go          # Composition root (thin — just wiring)
├── config/
│   └── dev.yaml             # Local dev config (safe to commit)
├── internal/
│   ├── config/              # Viper config loader
│   ├── logger/              # Zap wrapper with WithField
│   └── greeter/             # Example domain package (replace with yours)
├── mocks/
│   └── greeter/             # Generated by `make mocks` — commit these
├── .devcontainer/           # VSCode / Codespaces container definition
├── .githooks/               # pre-commit hook (installed by `make setup`)
├── .vscode/                 # launch, tasks, editor settings
├── tools.go                 # Tool version pinning (build tag: tools)
├── .golangci.yml            # Linter configuration
├── .mockery.yaml            # Mockery configuration
└── Makefile                 # All development commands

Configuration

Config is loaded from a YAML file (default config/dev.yaml) with env var overrides.

app:
  name: "go-template"
  port: 8080
  env: "dev"        # dev | staging | prod

logger:
  level: "debug"    # debug | info | warn | error

Override any key at runtime:

APP_PORT=9090 LOGGER_LEVEL=info make run

The CONFIG_PATH env var points to the config file:

CONFIG_PATH=config/staging.yaml ./bin/app

Logging

The logger wrapper adds WithField and WithFields for ergonomic context chaining:

log.WithField("request_id", rid).
    WithField("user", uid).
    Info("handling request")

log.WithFields(map[string]any{
    "component": "greeter",
    "name":      name,
}).Debug("generating greeting")

In production (app.env != dev) the output is JSON. In development, use logger.NewDevelopment() for coloured console output.


Mocking

  1. Define your interface in an internal/ package.
  2. Add a //go:generate mockery --name=YourInterface directive.
  3. Register the interface in .mockery.yaml under packages:.
  4. Run make mocks.

Generated mocks support type-safe EXPECT() call chains:

import mocks "github.com/you/my-project/mocks/greeter"

func TestSomething(t *testing.T) {
    g := mocks.NewMockGreeter(t)
    g.EXPECT().Greet("Alice").Return("Hello, Alice!", nil)

    result, err := myConsumer(g)
    require.NoError(t, err)
}

Git hooks

The pre-commit hook runs gofmt + golangci-lint + gosec before every commit. It checks only staged Go files to keep it fast.

govulncheck is intentionally excluded from the hook (it makes network calls and can be slow). Run it manually with make security.

To skip the hook in an emergency:

git commit --no-verify -m "wip: skip hooks"

Tool version management

Tool versions are pinned via tools.go (a //go:build tools file) and tracked in go.mod/go.sum. This ensures every developer and CI run uses exactly the same versions.

To update a tool:

go get github.com/vektra/mockery/v2@latest
go mod tidy
make tools   # reinstall binaries

Devcontainer

Open this repo in VSCode and choose "Reopen in Container" — everything (Go toolchain, golangci-lint, mockery, gosec, govulncheck) is installed automatically via make init.

Works with GitHub Codespaces out of the box.


Next steps (not included)

  • HTTP server — add chi or gin
  • Database — add sqlx or ent
  • CI — add .github/workflows/ci.yml running make lint test security
  • Docker — add a multi-stage Dockerfile for the binary
  • OpenTelemetry — add tracing with go.opentelemetry.io/otel