A public, release‑early, release‑often journey. The goal: learn Go while shaping a small, real network service that could grow into a bare‑metal provisioner.
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.
Production‑shaped scaffolding first means every experiment runs inside sane boundaries. Learning Go and ops habits at the same time pays long‑term dividends.
Bootstrap a single binary that can log, expose metrics, respond to health checks, and shut down gracefully on SIGINT
/SIGTERM
.
module github.com/yourname/gobear
go 1.22
require (
github.com/prometheus/client_golang v1.18.0
)
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {{
// --- config ---
addr := env("GOBEAR_ADDR", ":8080")
readTimeout := dur("GOBEAR_READ_TIMEOUT", 5*time.Second)
writeTimeout := dur("GOBEAR_WRITE_TIMEOUT", 10*time.Second)
idleTimeout := dur("GOBEAR_IDLE_TIMEOUT", 60*time.Second)
// --- logger ---
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{{Level: slog.LevelInfo}}))
slog.SetDefault(logger)
logger.Info("starting gobear", "addr", addr)
// --- http mux ---
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {{
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}})
mux.Handle("/metrics", promhttp.Handler())
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {{
fmt.Fprintf(w, "gobear: hello from %s\n", addr)
}})
server := &http.Server{{
Addr: addr,
Handler: logMiddleware(logger, mux),
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}}
// --- run & graceful shutdown ---
errCh := make(chan error, 1)
go func() {{
logger.Info("http.listen", "addr", addr)
errCh <- server.ListenAndServe()
}}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
select {{
case sig := <-stop:
logger.Info("signal", "sig", sig.String())
case err := <-errCh:
if !errors.Is(err, http.ErrServerClosed) {{
logger.Error("http error", "error", err)
}}
}}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {{
logger.Error("shutdown", "error", err)
}} else {{
logger.Info("clean shutdown")
}}
}}
func env(key, def string) string {{
if v := os.Getenv(key); v != "" {{
return v
}}
return def
}}
func dur(key string, def time.Duration) time.Duration {{
if v := os.Getenv(key); v != "" {{
if n, err := time.ParseDuration(v); err == nil {{ return n }}
if i, err := strconv.Atoi(v); err == nil {{ return time.Duration(i) * time.Second }}
}}
return def
}}
func logMiddleware(l *slog.Logger, next http.Handler) http.Handler {{
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {{
start := time.Now()
rw := &resp{{w, http.StatusOK}}
next.ServeHTTP(rw, r)
l.Info("http", "method", r.Method, "path", r.URL.Path, "status", rw.status, "t", time.Since(start).String())
}})
}}
type resp struct{{
http.ResponseWriter
status int
}}
func (r *resp) WriteHeader(code int) {{
r.status = code
r.ResponseWriter.WriteHeader(code)
}}
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
A clean, single binary with structured logs, metrics, health checks, and graceful shutdown — the core of any network service.
Keep the starter simple but ready to grow.
cmd/gobear/
for the main, internal/
for packages you don’t want imported elsewhere, pkg/
for public packages later.version
, commit
, date
via -ldflags
to print at startup.// 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
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
Design the request/response and logging paths before touching PXE/TFTP. It keeps learning focused and increments small.
// 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"))
})
--addr
flags using the standard flag
package.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.
# quick quality loop
go test ./...
golangci-lint run # if you add it
go build ./cmd/gobear
./gobear