iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Bootcamp

A 60-minute Go bootcamp that ships a working HTTP service with graceful shutdown, JSON I/O, tests, and a Dockerfile. The smallest reproducible loop.

A 60-minute Go bootcamp

EXAMPLE
# ===== Objectives =====
# 1. Build a typed HTTP service
# 2. Parse + emit JSON safely
# 3. Add structured logging + graceful shutdown
# 4. Tests + race detector
# 5. Dockerfile + run

# ===== 0-5 min: scaffold =====
mkdir shop-api && cd shop-api
go mod init example.com/shop-api

# ===== 5-25 min: the service =====
# main.go
package main

import (
    "context"
    "encoding/json"
    "errors"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

type Order struct {
    ID         string \`json:"id"\`
    Customer   string \`json:"customer"\`
    TotalCents int64  \`json:"total_cents"\`
    Status     string \`json:"status"\`
}

type Store struct {
    mu     sync.RWMutex
    orders map[string]Order
}

func newStore() *Store { return &Store{orders: map[string]Order{}} }

func (s *Store) put(o Order) {
    s.mu.Lock(); defer s.mu.Unlock()
    s.orders[o.ID] = o
}

func (s *Store) get(id string) (Order, bool) {
    s.mu.RLock(); defer s.mu.RUnlock()
    o, ok := s.orders[id]
    return o, ok
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    store := newStore()

    mux := http.NewServeMux()
    mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK); _, _ = w.Write([]byte("ok"))
    })

    mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
        var o Order
        if err := json.NewDecoder(r.Body).Decode(&o); err != nil {
            http.Error(w, "bad json", http.StatusBadRequest); return
        }
        if o.ID == "" || o.TotalCents < 0 {
            http.Error(w, "validation", http.StatusBadRequest); return
        }
        if o.Status == "" { o.Status = "new" }
        store.put(o)
        w.Header().Set("content-type", "application/json")
        w.WriteHeader(http.StatusCreated)
        _ = json.NewEncoder(w).Encode(o)
    })

    mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        o, ok := store.get(id)
        if !ok { http.NotFound(w, r); return }
        w.Header().Set("content-type", "application/json")
        _ = json.NewEncoder(w).Encode(o)
    })

    srv := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
        IdleTimeout:       60 * time.Second,
    }

    idle := make(chan struct{})
    go func() {
        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
        <-sig
        logger.Info("shutting down")
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
        _ = srv.Shutdown(ctx)
        close(idle)
    }()

    logger.Info("listening", "addr", srv.Addr)
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        logger.Error("serve", "err", err)
        os.Exit(1)
    }
    <-idle
}

# Run
go run .
# In another terminal
curl -X POST http://localhost:8080/orders -d '{"id":"o1","customer":"alice","total_cents":4995}'
curl http://localhost:8080/orders/o1

# ===== 25-40 min: tests =====
# main_test.go
package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestCreateAndGetOrder(t *testing.T) {
    store := newStore()
    mux := http.NewServeMux()
    mux.HandleFunc("POST /orders", func(w http.ResponseWriter, r *http.Request) {
        var o Order
        _ = json.NewDecoder(r.Body).Decode(&o)
        if o.Status == "" { o.Status = "new" }
        store.put(o)
        w.WriteHeader(201)
    })
    mux.HandleFunc("GET /orders/{id}", func(w http.ResponseWriter, r *http.Request) {
        o, ok := store.get(r.PathValue("id"))
        if !ok { http.NotFound(w, r); return }
        _ = json.NewEncoder(w).Encode(o)
    })
    srv := httptest.NewServer(mux); defer srv.Close()

    res, _ := http.Post(srv.URL+"/orders", "application/json",
        strings.NewReader(\`{\"id\":\"o1\",\"total_cents\":4995}\`))
    if res.StatusCode != 201 { t.Fatalf("want 201, got %d", res.StatusCode) }

    res, _ = http.Get(srv.URL + "/orders/o1")
    if res.StatusCode != 200 { t.Fatalf("want 200, got %d", res.StatusCode) }
    var got Order
    _ = json.NewDecoder(res.Body).Decode(&got)
    if got.ID != "o1" { t.Fatalf("got %+v", got) }
}

# Run
# go test ./...
# go test -race ./...
# go test -cover -coverprofile=cover.out && go tool cover -html=cover.out

# ===== 40-55 min: Dockerfile =====
# Dockerfile
# FROM golang:1.22-alpine AS build
# WORKDIR /src
# COPY go.mod go.sum ./
# RUN go mod download
# COPY . .
# RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app .
#
# FROM gcr.io/distroless/static-debian12:nonroot
# COPY --from=build /out/app /app
# EXPOSE 8080
# ENTRYPOINT ["/app"]
#
# docker build -t shop-api:1.0 .
# docker run --rm -p 8080:8080 shop-api:1.0

# ===== 55-60 min: deploy =====
# - Cloud Run / Fly.io: 'gcloud run deploy' / 'fly deploy' takes the Dockerfile
# - Kubernetes: kubectl apply -f deployment.yaml
# - VPS: docker compose with reverse proxy (Caddy / Traefik)

# ===== Post-bootcamp checklist =====
# - Service listens, healthz works, JSON I/O works
# - Tests + race detector green
# - Graceful shutdown on SIGTERM
# - Static binary in a distroless image (< 20MB)
# - README with build + run instructions

# ===== Pitfalls =====
# - Forgetting context propagation -> shutdown does not cancel handlers
# - http.ListenAndServe with default mux + no timeouts -> Slowloris risk
# - JSON decode without size limit -> memory pressure
# - Mutating maps without a lock -> data race

Why it matters

Build the bootcamp once and the pattern fits every service: typed handlers, graceful shutdown, structured logging, race-detector-clean tests, distroless static binary. The same skeleton scales from a tiny tool to a fleet of microservices without rewrites.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// 30-day Go bootcamp in the lesson body.
Try it Yourself »

Discussion

Loading…