← Back to Index

Chapter 5: Network Programming

HTTP servers, REST APIs & JSON

1 net/http Basics

Go's standard library includes a production-ready HTTP server. Unlike most languages where you need a third-party framework, Go's net/http package is powerful enough for real-world services.

Basic HTTP Server

package main

import (
    "fmt"
    "log"
    "net/http"
)

func homeHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
    fmt.Fprintf(w, "Welcome to the Home Page!")
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        name := r.URL.Query().Get("name")
        if name == "" {
            name = "World"
        }
        fmt.Fprintf(w, "Hello, %s!", name)
    case http.MethodPost:
        fmt.Fprintf(w, "POST request received")
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func main() {
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/hello", helloHandler)

    log.Println("Server starting on :8080...")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Key Points

  • β€’ http.HandleFunc registers a handler function for a URL pattern
  • β€’ http.ResponseWriter writes the response; *http.Request holds request data
  • β€’ http.ListenAndServe starts the server β€” it blocks until the server stops
  • β€’ The default mux (router) is used when passing nil as the second argument

Using http.ServeMux (Go 1.22+ Enhanced Routing)

func main() {
    mux := http.NewServeMux()

    // Go 1.22+ supports method and path parameters
    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        fmt.Fprintf(w, "User ID: %s", id)
    })

    mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Create user")
    })

    log.Fatal(http.ListenAndServe(":8080", mux))
}

πŸ”„ Comparison: HTTP Server Approaches

Go net/http

Built-in, production-ready. No framework needed for simple services. Enhanced routing since Go 1.22.

Node.js Express

Third-party framework required (npm install express). Middleware-based, flexible routing.

Python Flask

Lightweight micro-framework. Decorator-based routing (@app.route). Needs WSGI server for production.

2 JSON Handling

Go's encoding/json package provides Marshal (struct β†’ JSON) and Unmarshal (JSON β†’ struct) functions. Struct tags control how fields map to JSON keys.

Struct Tags & Marshal/Unmarshal

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    Password  string `json:"-"`                    // never included in JSON
    Age       int    `json:"age,omitempty"`         // omitted when zero value
    CreatedAt string `json:"created_at,omitempty"`
}

func main() {
    // Marshal: struct β†’ JSON
    user := User{
        ID:    1,
        Name:  "Alice",
        Email: "alice@example.com",
        Password: "secret123",
        Age:   0, // will be omitted due to omitempty
    }

    data, err := json.Marshal(user)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(data))
    // {"id":1,"name":"Alice","email":"alice@example.com"}

    // Pretty print
    prettyData, _ := json.MarshalIndent(user, "", "  ")
    fmt.Println(string(prettyData))

    // Unmarshal: JSON β†’ struct
    jsonStr := `{"id":2,"name":"Bob","email":"bob@example.com","age":25}`
    var user2 User
    if err := json.Unmarshal([]byte(jsonStr), &user2); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Name: %s, Age: %d\n", user2.Name, user2.Age)
}

Streaming with Encoder/Decoder

// json.NewEncoder writes directly to an io.Writer (e.g., http.ResponseWriter)
func userHandler(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

// json.NewDecoder reads directly from an io.Reader (e.g., r.Body)
func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "Created user: %s", user.Name)
}

Struct Tag Cheat Sheet

  • β€’ `json:"name"` β€” map to JSON key "name"
  • β€’ `json:"-"` β€” exclude from JSON entirely
  • β€’ `json:"name,omitempty"` β€” omit if zero value
  • β€’ `json:",string"` β€” encode number as JSON string
  • β€’ Only exported (capitalized) fields are included in JSON

3 HTTP Client

Go's net/http package also provides a fully featured HTTP client. Always set timeouts in production and remember to close response bodies.

Basic GET & POST Requests

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
)

func main() {
    // Simple GET
    resp, err := http.Get("https://api.example.com/users/1")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println("Status:", resp.StatusCode)
    fmt.Println("Body:", string(body))

    // POST with JSON body
    payload := map[string]string{
        "name":  "Alice",
        "email": "alice@example.com",
    }
    jsonData, _ := json.Marshal(payload)

    resp2, err := http.Post(
        "https://api.example.com/users",
        "application/json",
        bytes.NewBuffer(jsonData),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer resp2.Body.Close()

    fmt.Println("Created:", resp2.StatusCode)
}

Custom Client with Timeout

import (
    "net/http"
    "time"
)

// Always create a custom client for production use
client := &http.Client{
    Timeout: 10 * time.Second,
}

// Custom request with headers
req, err := http.NewRequest("GET", "https://api.example.com/data", nil)
if err != nil {
    log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer my-token")
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

// Decode JSON response directly
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)

⚠️ Important

  • β€’ Always defer resp.Body.Close() to prevent resource leaks
  • β€’ The default http.Client has no timeout β€” always set one in production
  • β€’ Use http.NewRequest + client.Do for full control over headers and method

4 REST API Practice

Let's build a complete in-memory CRUD API for managing books using only the standard library.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
)

type Book struct {
    ID     int    `json:"id"`
    Title  string `json:"title"`
    Author string `json:"author"`
}

type BookStore struct {
    mu     sync.RWMutex
    books  map[int]Book
    nextID int
}

func NewBookStore() *BookStore {
    return &BookStore{
        books:  make(map[int]Book),
        nextID: 1,
    }
}

func (s *BookStore) handleBooks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    switch r.Method {
    case http.MethodGet:
        s.listBooks(w, r)
    case http.MethodPost:
        s.createBook(w, r)
    default:
        http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
    }
}

func (s *BookStore) handleBook(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, `{"error":"invalid id"}`, http.StatusBadRequest)
        return
    }

    switch r.Method {
    case http.MethodGet:
        s.getBook(w, id)
    case http.MethodPut:
        s.updateBook(w, r, id)
    case http.MethodDelete:
        s.deleteBook(w, id)
    default:
        http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
    }
}

func (s *BookStore) listBooks(w http.ResponseWriter, r *http.Request) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    books := make([]Book, 0, len(s.books))
    for _, b := range s.books {
        books = append(books, b)
    }
    json.NewEncoder(w).Encode(books)
}

func (s *BookStore) createBook(w http.ResponseWriter, r *http.Request) {
    var book Book
    if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
        http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest)
        return
    }

    s.mu.Lock()
    book.ID = s.nextID
    s.nextID++
    s.books[book.ID] = book
    s.mu.Unlock()

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(book)
}

func (s *BookStore) getBook(w http.ResponseWriter, id int) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    book, ok := s.books[id]
    if !ok {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }
    json.NewEncoder(w).Encode(book)
}

func (s *BookStore) updateBook(w http.ResponseWriter, r *http.Request, id int) {
    var book Book
    if err := json.NewDecoder(r.Body).Decode(&book); err != nil {
        http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest)
        return
    }

    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.books[id]; !ok {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }
    book.ID = id
    s.books[id] = book
    json.NewEncoder(w).Encode(book)
}

func (s *BookStore) deleteBook(w http.ResponseWriter, id int) {
    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.books[id]; !ok {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }
    delete(s.books, id)
    w.WriteHeader(http.StatusNoContent)
}

func main() {
    store := NewBookStore()
    mux := http.NewServeMux()

    mux.HandleFunc("GET /books", store.handleBooks)
    mux.HandleFunc("POST /books", store.handleBooks)
    mux.HandleFunc("GET /books/{id}", store.handleBook)
    mux.HandleFunc("PUT /books/{id}", store.handleBook)
    mux.HandleFunc("DELETE /books/{id}", store.handleBook)

    log.Println("API server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Testing with curl

# Create a book
curl -X POST http://localhost:8080/books \
  -H "Content-Type: application/json" \
  -d '{"title":"The Go Programming Language","author":"Donovan & Kernighan"}'

# List all books
curl http://localhost:8080/books

# Get a single book
curl http://localhost:8080/books/1

# Update a book
curl -X PUT http://localhost:8080/books/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Go in Action","author":"Kennedy, Ketelsen & Martin"}'

# Delete a book
curl -X DELETE http://localhost:8080/books/1

5 Middleware

Middleware in Go wraps an http.Handler to add cross-cutting concerns like logging, authentication, and CORS. The pattern is simple: a function that takes a handler and returns a new handler.

Logging Middleware

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("Started %s %s", r.Method, r.URL.Path)

        next.ServeHTTP(w, r)

        log.Printf("Completed %s %s in %v", r.Method, r.URL.Path, time.Since(start))
    })
}

Auth Middleware

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
            return
        }

        // Validate the token (simplified)
        if token != "Bearer valid-token" {
            http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
            return
        }

        next.ServeHTTP(w, r)
    })
}

CORS Middleware

func corsMiddleware(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")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Chaining Middleware

func chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /public", publicHandler)
    mux.HandleFunc("GET /api/data", apiHandler)

    // Apply middleware chain
    handler := chain(mux, corsMiddleware, loggingMiddleware)

    // Auth middleware only for specific routes
    protectedMux := http.NewServeMux()
    protectedMux.Handle("/api/", chain(mux, authMiddleware))

    log.Fatal(http.ListenAndServe(":8080", handler))
}

Middleware Pattern

The signature func(http.Handler) http.Handler is the standard Go middleware pattern. Middleware executes in reverse order of wrapping β€” the first middleware in the chain runs first (outermost wrapper).

6 Chapter Summary

🌐

net/http

HandleFunc, ServeMux, path parameters, method routing

πŸ“¦

JSON

Marshal/Unmarshal, struct tags, Encoder/Decoder

πŸ“‘

HTTP Client

GET/POST, custom Client, timeouts, headers

πŸ”§

REST API

CRUD operations, routing, request/response patterns

πŸ›‘οΈ

Middleware

Logging, auth, CORS, handler chaining

πŸš€

Production Ready

Standard library is powerful enough for real services