Enterprise Delivery Basics
10 examples to get you started with Enterprise Delivery - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir deliverylab && cd deliverylab && go mod init example.com/deliverylab. - Optional for flag and metrics examples:
go get github.com/open-feature/go-sdk/openfeature github.com/prometheus/client_golang/prometheus.
Basic Examples
1. Embed Build Version at Link Time
Stamp git SHA into the binary for deploy correlation and /healthz responses.
package main
import (
"fmt"
"runtime/debug"
)
func main() {
fmt.Println("version:", buildVersion())
}
func buildVersion() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "dev"
}
for _, s := range info.Settings {
if s.Key == "vcs.revision" {
if len(s.Value) >= 7 {
return s.Value[:7]
}
return s.Value
}
}
return "unknown"
}debug.ReadBuildInfoworks when built with module info (go buildfrom a git repo).- Pass
-ldflags "-X main.version=..."for explicit stamping in CI. - Log and expose this value on every deploy for rollback decisions.
Related: Shipping Go Services Safely at Enterprise Scale - delivery overview
2. CI Test Gate Script
A minimal pipeline step that fails the build on test or race failures.
#!/usr/bin/env bash
set -euo pipefail
go version
go test ./... -count=1 -race -timeout 5m
go vet ./...-racecatches data races before prod - worth the CI time on service code.-count=1disables test cache so CI always re-runs.- Wrap this in GitHub Actions, GitLab CI, or Buildkite before image build.
Related: DORA Metrics for Go Teams - measuring pipeline health
3. Multi-Stage Dockerfile for Reproducible Images
Compile in a builder stage; ship a minimal runtime image.
FROM golang:1.26-bookworm AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/api
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]CGO_ENABLED=0yields a static binary suitable for distroless.-trimpathremoves local paths from binaries for reproducibility.- Tag the built image with git SHA, not
:latest, in your registry.
Related: Release Strategy: Blue-Green & Canary for Go - deploying images
4. Readiness Probe Handler
Separate cheap liveness from dependency-checked readiness.
package main
import (
"database/sql"
"net/http"
"os"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
db, _ := sql.Open("pgx", os.Getenv("DATABASE_URL"))
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, _ *http.Request) {
if err := db.Ping(); err != nil {
http.Error(w, "db down", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
http.ListenAndServe(":8080", mux)
}- Liveness should not call external systems - only readiness should.
- Return 503 on readiness failure so load balancers stop traffic during rollout.
- Kubernetes uses these paths in
livenessProbeandreadinessProbe.
Related: Progressive Delivery & Automated Verification - automated checks
5. Graceful Shutdown for Rolling Deploys
Drain in-flight requests before SIGKILL during pod termination.
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{Addr: ":8080", Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(2 * time.Second)
w.WriteHeader(http.StatusOK)
})}
go func() { _ = srv.ListenAndServe() }()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("shutdown", "err", err)
}
}- Kubernetes sends SIGTERM before removing endpoints from Service.
Shutdownwaits for active handlers up to the timeout.- Match
terminationGracePeriodSecondsto your worst-case request duration.
Related: Rollback Runbooks for Go Deployments - recovery during bad rollouts
6. Environment-Based Config (12-Factor)
Same binary across staging and prod; config from environment only.
package main
import (
"log/slog"
"os"
)
type Config struct {
Port string
DatabaseURL string
LogLevel string
}
func loadConfig() Config {
return Config{
Port: envOr("PORT", "8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
LogLevel: envOr("LOG_LEVEL", "info"),
}
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func main() {
cfg := loadConfig()
slog.Info("starting", "port", cfg.Port, "log_level", cfg.LogLevel)
}- Never bake environment-specific values into the image at build time.
- Validate required vars at startup and fail fast with clear logs.
- Secrets belong in a secret manager, referenced as env vars in the pod spec.
Related: Change Management & Compliance Gates - controlled prod changes
7. Post-Deploy Smoke Test
A tiny Go test binary CI runs against staging after deploy.
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
base := os.Getenv("SMOKE_BASE_URL")
if base == "" {
base = "http://localhost:8080"
}
client := &http.Client{Timeout: 5 * time.Second}
for _, path := range []string{"/healthz", "/readyz"} {
resp, err := client.Get(base + path)
if err != nil || resp.StatusCode != http.StatusOK {
fmt.Fprintf(os.Stderr, "smoke failed %s: %v status=%v\n", path, err, resp)
os.Exit(1)
}
resp.Body.Close()
}
fmt.Println("smoke ok")
}- Run smoke immediately after promote to staging; block prod if it fails.
- Extend with authenticated happy-path API calls for critical flows.
- Keep smoke fast (under 30 seconds) so pipelines stay snappy.
Related: Progressive Delivery & Automated Verification - verification depth
Intermediate Examples
8. Feature Flag Interface (Dark Launch)
Deploy new code with behavior off until ops enables the flag.
package main
import (
"context"
"os"
)
type Flags interface {
Bool(ctx context.Context, key string, defaultVal bool) bool
}
type EnvFlags struct{}
func (EnvFlags) Bool(_ context.Context, key string, defaultVal bool) bool {
if v := os.Getenv("FLAG_" + key); v == "true" {
return true
}
if v == "false" {
return false
}
return defaultVal
}
func runNewCheckout(ctx context.Context) error { return nil }
func runLegacyCheckout(ctx context.Context) error { return nil }
func handleCheckout(ctx context.Context, flags Flags) error {
if flags.Bool(ctx, "NEW_CHECKOUT", false) {
return runNewCheckout(ctx)
}
return runLegacyCheckout(ctx)
}- Start with env-backed flags for simplicity; graduate to OpenFeature or LaunchDarkly.
- Default new flags to
falsein production config. - Dark launch lets canary pods run new paths without user exposure.
Related: Feature Flags in Go Services - production flag SDKs
9. Prometheus Deploy Info Metric
Expose version labels so dashboards mark deploy boundaries.
package main
import (
"net/http"
"runtime"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var buildInfo = prometheus.NewGaugeVec(
prometheus.GaugeOpts{Name: "go_build_info", Help: "Build metadata"},
[]string{"version", "goversion"},
)
func main() {
prometheus.MustRegister(buildInfo)
buildInfo.WithLabelValues("abc1234", runtime.Version()).Set(1)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":9090", nil)
}- Register metrics once in
main, never per request. - Grafana annotations can watch
go_build_infolabel changes for deploy lines. - Pair with RED charts to spot CFR spikes after version label changes.
Related: DORA Metrics for Go Teams - change failure rate
10. Expand-Contract Migration Guard
Check schema compatibility before serving traffic on a new version.
package main
import (
"database/sql"
"errors"
)
func assertColumnExists(db *sql.DB, table, column string) error {
var exists bool
q := `SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = $1 AND column_name = $2
)`
if err := db.QueryRow(q, table, column).Scan(&exists); err != nil {
return err
}
if !exists {
return errors.New("missing required column: " + table + "." + column)
}
return nil
}- Call from
/readyzwhen a new version depends on an expanded column. - Run expand migrations before rolling out binaries that read the column.
- Contract (drop old column) only after 100% of pods run the new code.
Related: Rollback Runbooks for Go Deployments - schema rollback steps
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).