# template A Go project template for **PoC, hobby projects, and small publishable packages**. Clone it, rename the module, run `make init`, and you're coding. Demonstrates idiomatic Go patterns: stdlib-only dependencies, consumer-defined interfaces, manual fakes for testing, and a clean `go.mod` that stays free of dev tool noise — so packages extracted from this template can be published without module graph pollution. --- ## Features | Area | Tool | Purpose | |---|---|---| | Language | Go 1.25 | Modules, toolchain directive | | Logging | standard `log/slog` | Structured JSON/text logging + `WithField` extension | | Config | standard `flag` | CLI flags with defaults, no config files | | Linting | [golangci-lint](https://golangci-lint.run) | Aggregated linters, one config file | | Security | [gosec](https://github.com/securego/gosec) + [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) | SAST + dependency vulnerability scan | | Tests | standard `testing` | Table-driven tests, manual fakes, no third-party test framework | | Git hooks | custom pre-push | gofmt + go vet + golangci-lint + gosec on every push | | Tool versions | `tools.versions` + `go run` | Pinned versions without polluting `go.mod` | | Dev environment | devcontainer | Reproducible VSCode / GitHub Codespaces setup | | IDE | VSCode | Launch configs, tasks, recommended settings | --- ## Getting started ### Prerequisites - Go 1.25+ - Git - (Optional) [VSCode](https://code.visualstudio.com/) + [Go extension](https://marketplace.visualstudio.com/items?itemName=golang.Go) - (Optional) Docker for devcontainer ### 1. Clone and rename ```bash git clone https://gitea.djmil.dev/go/template my-project cd my-project # Interactive rename — updates module path, config, devcontainer, and docs: ./rename.sh ``` ### 2. Init (run once) ```bash make init # fetches deps, configures git hooks make tools # (optional) install tool binaries to GOPATH/bin for IDE integration ``` ### 3. Build and run ```bash make build # compiles to ./bin/app make run # go run with default flags ``` --- ## Daily workflow ```bash make test # run all tests make test-race # … with race detector make lint # go vet + golangci-lint make lint-fix # go fix + golangci-lint auto-fix make security # gosec + govulncheck make release # list tags, or run full checks then tag+push (make release VERSION=v0.1.0) ``` > **Keyboard shortcut (VSCode):** `Ctrl+Shift+B` → build, `Ctrl+Shift+T` → test. --- ## Project structure ``` . ├── cmd/ │ └── app/ │ └── main.go # Composition root (thin — just wiring) ├── internal/ │ ├── config/ # flag-based config (config.Load) │ ├── logger/ # slog wrapper with WithField / WithFields │ └── greeter/ # Example domain package (replace with yours) ├── .devcontainer/ # VSCode / Codespaces container definition ├── .githooks/ # pre-push hook (installed by `make setup`) ├── .vscode/ # launch, tasks, editor settings ├── pkg/ │ └── result/ # Example publishable package (Result/Expect types) │ ├── doc.go # package-level godoc │ ├── result.go # implementation │ └── example_test.go # runnable examples (shown on pkg.go.dev) ├── tools.versions # Pinned tool versions (Makefile + hook source this) ├── .golangci.yml # Linter configuration └── Makefile # All development commands ``` --- ## Configuration All configuration is via CLI flags with sensible defaults: ```bash ./bin/app -help -env string environment: dev | staging | prod (default "dev") -log-level string log level: debug | info | warn | error (default "info") -name string greeter name (default "Gopher") -port int listen port (default 8080) ``` Override at runtime: ```bash ./bin/app -port 9090 -env prod -log-level warn ``` --- ## Logging The logger wrapper adds `WithField` and `WithFields` for ergonomic context chaining on top of the standard `log/slog` package: ```go 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 (`slog.NewJSONHandler`). In development, `logger.NewDevelopment()` uses the human-friendly text handler. --- ## Testing Tests use the standard `testing` package. Concrete types are returned from constructors — consumers (including tests) define their own minimal interfaces and satisfy them with manual fakes. No code generation required. ```go // Interface declared in the consumer (or _test.go), not in greeter package. type greeter interface { Greet(name string) result.Expect[string] } type fakeGreeter struct { greetFn func(name string) result.Expect[string] } func (f *fakeGreeter) Greet(name string) result.Expect[string] { return f.greetFn(name) } func TestSomething(t *testing.T) { fake := &fakeGreeter{ greetFn: func(name string) result.Expect[string] { return result.Ok("Hello, " + name + "!") }, } // pass fake to the system under test } ``` --- ## pkg/result — happy-path error handling `pkg/result` is a small publishable package that demonstrates the pattern of keeping `go.mod` clean. It provides `Expect[T]`, a generic type that holds either a success value or an error. The idea: deep call stacks write for the **happy path**, unwrapping results with `Expect` or `Must` (which panic on failure). A single `defer result.Catch(&err)` at the entry point recovers those panics and returns them as a normal Go error, with the stack trace captured at the original failure site. ```go // Wrap any (value, error) function: data := result.Of(os.ReadFile("config.json")).Expect("read config") // Or construct explicitly: func parsePort(s string) result.Expect[int] { n, err := strconv.Atoi(s) if err != nil { return result.Fail[int](fmt.Errorf("parsePort: %w", err)) } return result.Ok(n) } // Entry point catches panics from the whole call stack: func run() (err error) { defer result.Catch(&err) port := parsePort(cfg.Port).Expect("load config") _ = port // happy path continues … return nil } ``` `Catch` only intercepts error panics produced by this package. Real runtime panics (nil-pointer dereferences, etc.) are re-panicked so bugs are never silently swallowed. At boundaries where you need to re-join normal Go code: ```go port, err := parsePort("443").Unwrap() // back to (T, error) ``` See [`pkg/result/example_test.go`](pkg/result/example_test.go) for the full set of runnable examples. --- ## Git hooks The pre-push hook runs **gofmt + go vet + golangci-lint + gosec** against the full codebase before every `git push`. Running on push (not commit) keeps the inner commit loop fast while still blocking bad code from reaching the remote. `govulncheck` is intentionally excluded (it makes network calls and can be slow). Run it manually with `make security`. To skip the hook in an emergency: ```bash git push --no-verify ``` --- ## Tool version management Tool versions are pinned in `tools.versions` — a plain `KEY=value` file that both the `Makefile` and the pre-push hook source directly. **Why not `tools.go` + `go.mod`?** The conventional `//go:build tools` approach adds development tools (linters, scanners, debuggers) as entries in `go.mod`. This is fine for applications, but it's a problem when the repo is intended to publish reusable packages: consumers who `go get` your package see those tool dependencies in the module graph, even though they are never compiled into the binary. Keeping `go.mod` clean makes the module honest — it declares only what the package code actually needs. Instead, `go run tool@version` is used in Makefile targets and the pre-push hook. Go caches downloaded tools in the module cache, so after the first run there is no extra download cost. To update a tool, edit the version in `tools.versions`: ``` GOLANGCI_LINT_VERSION=v1.64.8 ← change this ``` Both `make lint` and the pre-push hook pick up the new version automatically. Run `make tools` if you also want the updated binary installed to `GOPATH/bin` (e.g. for IDE integration or direct CLI use). --- ## Releasing ```bash make release # list all existing tags make release VERSION=v0.1.0 # run full checks, then tag and push ``` The release target validates the version format (semver `vX.Y.Z`), ensures the working tree is clean, and runs `make test-race lint security` before tagging. The tag is only created and pushed if every check passes. --- ## Devcontainer Open this repo in VSCode and choose **"Reopen in Container"** — the Go toolchain is installed automatically. Run `make init` to configure git hooks, then `make tools` to install linter/scanner binaries to `GOPATH/bin`. Works with GitHub Codespaces out of the box. --- ## Next steps (not included) - **HTTP server** — add [chi](https://github.com/go-chi/chi) or [gin](https://github.com/gin-gonic/gin) - **Database** — add [sqlx](https://github.com/jmoiron/sqlx) or [ent](https://entgo.io/) - **CI** — add a Gitea Actions / CI pipeline running `make lint test security` - **Docker** — add a multi-stage `Dockerfile` to publish the binary as a container image (separate from the devcontainer) - **OpenTelemetry** — add tracing with `go.opentelemetry.io/otel`