303 lines
9.5 KiB
Markdown
303 lines
9.5 KiB
Markdown
# 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`
|