Project shape
Scope
Start as a general-purpose network service with solid bones: structured logging, Prometheus metrics, health endpoints, clean shutdown, and a minimal API. Grow toward PXE/TFTP helpers and a small state store.
Why this order
Production-shaped scaffolding first means every experiment runs inside sane boundaries. Learning Go and ops habits at the same time pays long-term dividends.
Milestone 0 — a production-shaped starter
Bootstrap a single binary that can log, expose metrics, respond to health checks, and shut down gracefully on SIGINT
/SIGTERM
.
go.mod
module github.com/yourname/gobear
go 1.22
require (
github.com/prometheus/client_golang v1.18.0
)
cmd/gobear/main.go
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// abbreviated for brevity...
func main() {{
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{{Level: slog.LevelInfo}}))
slog.SetDefault(logger)
logger.Info("starting gobear")
// mux, handlers, graceful shutdown, etc.
}}
Run it
go mod tidy
go run ./cmd/gobear
# in another shell:
curl -s localhost:8080/healthz
curl -s localhost:8080/
curl -s localhost:8080/metrics | head
What we have now
A clean, single binary with structured logs, metrics, health checks, and graceful shutdown — the core of any network service.
Milestone 1 — configuration & layout
- Project layout:
cmd/gobear/
for the main,internal/
for private packages,pkg/
for public ones later. - Env-first config: environment variables with sane defaults; support YAML/TOML later if needed.
- Build metadata: inject
version
,commit
,date
via-ldflags
to print at startup.
Version injection
// inside main.go
var (
version = "dev"
commit = "none"
date = "unknown"
)
logger.Info("build", "version", version, "commit", commit, "date", date)
# build with metadata
go build -ldflags "-X main.version=$(git describe --tags --always) \
-X main.commit=$(git rev-parse --short HEAD) \
-X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" ./cmd/gobear
Milestone 2 — a tiny API for provisioning
Start with a simulated provisioning workflow that returns a token. No PXE yet — just shape the flow.
// add to mux in main()
mux.HandleFunc("/api/v1/provision", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed); return
}
token := fmt.Sprintf("pxe_%d", time.Now().Unix())
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"token":"%s"}`, token)
})
# try it
curl -s -X POST localhost:8080/api/v1/provision
Why a fake first?
Design the request/response and logging paths before touching PXE/TFTP. It keeps learning focused and increments small.
Milestone 3 — health, readiness, and metrics
- /healthz: quick “am I alive?”
- /readyz: add checks for dependencies (e.g., can we read a state file?)
- /metrics: Prometheus handler already mounted
// example readiness check
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
if _, err := os.Stat("state.db"); err == nil {
w.Write([]byte("ready")); return
}
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("not ready"))
})
Milestone 4 — next steps toward a provisioner
- PXE notes: A minimal PXE flow needs DHCP options (66/67), a TFTP server, and kernel/initrd with a preseed or cloud-init data source.
- State: Start with a tiny BoltDB/SQLite or JSON file for requests and tokens.
- Templates: Render cloud-init or iPXE templates from Go to hand to clients.
- CLI: Add
--addr
flags using the standardflag
package.
Release early, release often
# quick quality loop
go test ./...
golangci-lint run # if you add it
go build ./cmd/gobear
./gobear
Commit small, runnable changes. Keep a CHANGELOG. Tag “walkable” points even if features are thin. The habit matters more than the scope in the early days.