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.HandleFuncregisters a handler function for a URL pattern - β’
http.ResponseWriterwrites the response;*http.Requestholds request data - β’
http.ListenAndServestarts the server β it blocks until the server stops - β’ The default mux (router) is used when passing
nilas 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.Clienthas no timeout β always set one in production - β’ Use
http.NewRequest+client.Dofor 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