Build HTTP clients and servers using Go's standard library net/http: GET/POST requests, JSON APIs, middleware, routing, and a complete REST server.
Time
35 minutes
Prerequisites
Lab 10 (File I/O & JSON)
Tools
Docker image: zchencow/innozverse-go:latest
Lab Instructions
Step 1: HTTP Client — GET Requests
dockerrun--rmzchencow/innozverse-go:latestgorun-<<'EOF'package mainimport ( "encoding/json" "fmt" "io" "net/http" "time")type JSONPlaceholderPost struct { UserID int `json:"userId"` ID int `json:"id"` Title string `json:"title"` Body string `json:"body"`}func main() { // Default client — fine for simple use client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get("https://jsonplaceholder.typicode.com/posts/1") if err != nil { // Fallback for offline environments fmt.Println("HTTP GET (offline, showing structure):") mock := JSONPlaceholderPost{1, 1, "sunt aut facere repellat", "quia et suscipit..."} data, _ := json.MarshalIndent(mock, "", " ") fmt.Println(string(data)) return } defer resp.Body.Close() fmt.Println("Status:", resp.Status) fmt.Println("Content-Type:", resp.Header.Get("Content-Type")) body, _ := io.ReadAll(resp.Body) var post JSONPlaceholderPost json.Unmarshal(body, &post) fmt.Printf("Post #%d by user %d:\n %s\n", post.ID, post.UserID, post.Title)}EOF
💡 Always set a Timeout on your HTTP client — the default http.DefaultClient has no timeout, so a slow server can block your goroutine forever. Also always defer resp.Body.Close() — not closing it leaks the connection. Use io.ReadAll(resp.Body) to read the full response.
📸 Verified Output:
Step 2: HTTP Server with Standard Library
💡 net/http/httptest lets you test HTTP handlers without starting a real server. httptest.NewRequest + httptest.NewRecorder record the response. This is how the Go standard library tests its own HTTP code — fast, parallel, no port conflicts.
HTTP GET (offline, showing structure):
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat",
"body": "quia et suscipit..."
}
docker run --rm zchencow/innozverse-go:latest go run - << 'EOF'
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
)
// In-memory product store
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
type Store struct {
mu sync.RWMutex
products map[int]Product
nextID int
}
func NewStore() *Store {
s := &Store{products: make(map[int]Product), nextID: 1}
s.products[1] = Product{1, "Surface Pro", 864.00, 15}
s.products[2] = Product{2, "Surface Pen", 49.99, 80}
s.nextID = 3
return s
}
func (s *Store) List() []Product {
s.mu.RLock(); defer s.mu.RUnlock()
result := make([]Product, 0, len(s.products))
for _, p := range s.products { result = append(result, p) }
return result
}
func (s *Store) Get(id int) (Product, bool) {
s.mu.RLock(); defer s.mu.RUnlock()
p, ok := s.products[id]
return p, ok
}
func (s *Store) Create(p Product) Product {
s.mu.Lock(); defer s.mu.Unlock()
p.ID = s.nextID; s.nextID++
s.products[p.ID] = p
return p
}
// JSON helpers
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
// Handlers
func listHandler(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, store.List())
}
}
func createHandler(store *Store) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "POST required")
return
}
var p Product
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
if p.Name == "" || p.Price <= 0 {
writeError(w, http.StatusBadRequest, "name and price required")
return
}
created := store.Create(p)
writeJSON(w, http.StatusCreated, created)
}
}
// Middleware
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[%s] %s %s\n", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}
func main() {
store := NewStore()
mux := http.NewServeMux()
mux.Handle("GET /products", Logger(listHandler(store)))
mux.Handle("POST /products", Logger(createHandler(store)))
// Use httptest to simulate without binding a real port
fmt.Println("=== GET /products ===")
req := httptest.NewRequest("GET", "/products", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
fmt.Println("Status:", rr.Code)
fmt.Println("Body:", strings.TrimSpace(rr.Body.String()))
fmt.Println("\n=== POST /products ===")
body := `{"name":"Office 365","price":99.99,"stock":999}`
req2 := httptest.NewRequest("POST", "/products", strings.NewReader(body))
req2.Header.Set("Content-Type", "application/json")
rr2 := httptest.NewRecorder()
mux.ServeHTTP(rr2, req2)
fmt.Println("Status:", rr2.Code)
fmt.Println("Body:", strings.TrimSpace(rr2.Body.String()))
fmt.Println("\n=== POST /products (invalid) ===")
req3 := httptest.NewRequest("POST", "/products", strings.NewReader(`{"name":""}`))
req3.Header.Set("Content-Type", "application/json")
rr3 := httptest.NewRecorder()
mux.ServeHTTP(rr3, req3)
fmt.Println("Status:", rr3.Code)
fmt.Println("Body:", strings.TrimSpace(rr3.Body.String()))
}
EOF
=== GET /products ===
[GET] /products 192.0.2.1:1234
Status: 200
Body: [{"id":1,"name":"Surface Pro","price":864,"stock":15},{"id":2,"name":"Surface Pen","price":49.99,"stock":80}]
=== POST /products ===
[POST] /products 192.0.2.1:1234
Status: 201
Body: {"id":3,"name":"Office 365","price":99.99,"stock":999}
=== POST /products (invalid) ===
[POST] /products 192.0.2.1:1234
Status: 400
Body: {"error":"name and price required"}
docker run --rm zchencow/innozverse-go:latest go run - << 'EOF'
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
"time"
)
// Step 3: Middleware chain
type Middleware func(http.Handler) http.Handler
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent); return }
next.ServeHTTP(w, r)
})
}
func RequestID(next http.Handler) http.Handler {
var counter int64
var mu sync.Mutex
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock(); counter++; id := counter; mu.Unlock()
w.Header().Set("X-Request-ID", fmt.Sprintf("req-%d", id))
next.ServeHTTP(w, r)
})
}
// Step 4: Simple router (Go 1.22 pattern matching)
type Router struct{ mux *http.ServeMux }
func NewRouter() *Router { return &Router{mux: http.NewServeMux()} }
func (r *Router) GET(path string, h http.HandlerFunc) { r.mux.HandleFunc("GET "+path, h) }
func (r *Router) POST(path string, h http.HandlerFunc) { r.mux.HandleFunc("POST "+path, h) }
func (r *Router) PUT(path string, h http.HandlerFunc) { r.mux.HandleFunc("PUT "+path, h) }
func (r *Router) DELETE(path string, h http.HandlerFunc) { r.mux.HandleFunc("DELETE "+path, h) }
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.mux.ServeHTTP(w, req) }
// Step 5: In-memory store
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
CreatedAt time.Time `json:"created_at"`
}
type ProductStore struct {
mu sync.RWMutex
items map[int]Product
nextID int
}
func NewProductStore() *ProductStore {
s := &ProductStore{items: make(map[int]Product), nextID: 1}
for _, p := range []Product{
{0, "Surface Pro 12\"", 864.00, 15, time.Now()},
{0, "Surface Pen", 49.99, 80, time.Now()},
{0, "Office 365", 99.99, 999, time.Now()},
} {
p.ID = s.nextID; s.nextID++
s.items[p.ID] = p
}
return s
}
func (s *ProductStore) List() []Product {
s.mu.RLock(); defer s.mu.RUnlock()
result := make([]Product, 0, len(s.items))
for _, p := range s.items { result = append(result, p) }
return result
}
func (s *ProductStore) Get(id int) (Product, bool) {
s.mu.RLock(); defer s.mu.RUnlock()
p, ok := s.items[id]; return p, ok
}
func (s *ProductStore) Create(p Product) Product {
s.mu.Lock(); defer s.mu.Unlock()
p.ID = s.nextID; s.nextID++; p.CreatedAt = time.Now()
s.items[p.ID] = p; return p
}
func (s *ProductStore) Update(id int, patch Product) (Product, bool) {
s.mu.Lock(); defer s.mu.Unlock()
p, ok := s.items[id]
if !ok { return Product{}, false }
if patch.Name != "" { p.Name = patch.Name }
if patch.Price > 0 { p.Price = patch.Price }
if patch.Stock >= 0 { p.Stock = patch.Stock }
s.items[id] = p; return p, true
}
func (s *ProductStore) Delete(id int) bool {
s.mu.Lock(); defer s.mu.Unlock()
if _, ok := s.items[id]; !ok { return false }
delete(s.items, id); return true
}
// Step 6: Handler factory
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
func idFromPath(r *http.Request) (int, error) {
parts := strings.Split(r.URL.Path, "/")
if len(parts) < 3 { return 0, fmt.Errorf("no id") }
return strconv.Atoi(parts[len(parts)-1])
}
// Step 7: Build the REST API
func setupRoutes(store *ProductStore) http.Handler {
r := NewRouter()
r.GET("/api/products", func(w http.ResponseWriter, req *http.Request) {
writeJSON(w, 200, store.List())
})
r.GET("/api/products/{id}", func(w http.ResponseWriter, req *http.Request) {
id, err := idFromPath(req)
if err != nil { writeError(w, 400, "invalid id"); return }
p, ok := store.Get(id)
if !ok { writeError(w, 404, "product not found"); return }
writeJSON(w, 200, p)
})
r.POST("/api/products", func(w http.ResponseWriter, req *http.Request) {
var p Product
if err := json.NewDecoder(req.Body).Decode(&p); err != nil {
writeError(w, 400, "invalid JSON"); return
}
if p.Name == "" || p.Price <= 0 {
writeError(w, 400, "name and price required"); return
}
writeJSON(w, 201, store.Create(p))
})
r.PUT("/api/products/{id}", func(w http.ResponseWriter, req *http.Request) {
id, err := idFromPath(req)
if err != nil { writeError(w, 400, "invalid id"); return }
var patch Product
json.NewDecoder(req.Body).Decode(&patch)
updated, ok := store.Update(id, patch)
if !ok { writeError(w, 404, "product not found"); return }
writeJSON(w, 200, updated)
})
r.DELETE("/api/products/{id}", func(w http.ResponseWriter, req *http.Request) {
id, err := idFromPath(req)
if err != nil { writeError(w, 400, "invalid id"); return }
if !store.Delete(id) { writeError(w, 404, "product not found"); return }
w.WriteHeader(204)
})
return Chain(r, CORS, RequestID)
}
// Step 8: Capstone — run full CRUD test suite
func apiTest(handler http.Handler, method, path, body string) (int, string) {
var bodyReader *strings.Reader
if body != "" { bodyReader = strings.NewReader(body) } else { bodyReader = strings.NewReader("") }
req := httptest.NewRequest(method, path, bodyReader)
if body != "" { req.Header.Set("Content-Type", "application/json") }
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
return rr.Code, strings.TrimSpace(rr.Body.String())
}
func main() {
store := NewProductStore()
api := setupRoutes(store)
tests := []struct{ method, path, body, label string }{
{"GET", "/api/products", "", "List all"},
{"GET", "/api/products/1", "", "Get #1"},
{"GET", "/api/products/99", "", "Get missing"},
{"POST", "/api/products", `{"name":"USB-C Hub","price":29.99,"stock":50}`, "Create"},
{"PUT", "/api/products/1", `{"price":799.99}`, "Update price"},
{"DELETE", "/api/products/2", "", "Delete #2"},
{"GET", "/api/products", "", "List after changes"},
}
for _, tc := range tests {
status, body := apiTest(api, tc.method, tc.path, tc.body)
truncated := body
if len(truncated) > 80 { truncated = truncated[:80] + "..." }
fmt.Printf("[%d] %-6s %-25s → %s\n", status, tc.method, tc.path, truncated)
}
}
EOF
[200] GET /api/products → [{"id":1,"name":"Surface Pro 12\"","price":864,"stock":15...
[200] GET /api/products/1 → {"id":1,"name":"Surface Pro 12\"","price":864,"stock":15,...
[404] GET /api/products/99 → {"error":"product not found"}
[201] POST /api/products → {"id":4,"name":"USB-C Hub","price":29.99,"stock":50,"crea...
[200] PUT /api/products/1 → {"id":1,"name":"Surface Pro 12\"","price":799.99,"stock":...
[204] DELETE /api/products/2 →
[200] GET /api/products → [{"id":1,"name":"Surface Pro 12\"","price":799.99,"stock"...