net/http
HTTP server, client, middleware, graceful shutdown
Minimal HTTP Server
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
})
http.ListenAndServe(":8080", nil)
}Routing and ServeMux (Go 1.22)
Since Go 1.22, net/http's ServeMux supports method prefixes and path parameters; previously you needed libraries like chi / gorilla mux. The built-in mux is enough for everyday CRUD; reach for chi / gin only for complex routing.
package main
import (
"fmt"
"log"
"net/http"
)
func createUser(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "create user")
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "delete user:", r.PathValue("id"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // path parameter
q := r.URL.Query().Get("verbose") // query string
fmt.Fprintln(w, "user:", id, q)
})
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
log.Fatal(http.ListenAndServe(":8080", mux))
}Server Configuration (Don't Call ListenAndServe Directly)
A production service must always construct http.Server explicitly, for two reasons: 1) only then can you set layered timeouts; 2) only then can you do graceful shutdown. http.ListenAndServe has no timeouts at all, so a malicious slow connection can tie up your process for a long time.
package main
import (
"errors"
"fmt"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second, // prevent Slowloris
ReadTimeout: 30 * time.Second, // upper bound to read the whole request body
WriteTimeout: 30 * time.Second, // upper bound to write the response
IdleTimeout: 120 * time.Second, // keep-alive idle limit
MaxHeaderBytes: 1 << 20,
}
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}Graceful Shutdown
On SIGINT/SIGTERM, Shutdown stops accepting new connections, waits for in-flight requests to finish, then exits. Pair it with a context deadline so a "never-finishing" request can't block the process forever.
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
log.Println("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("graceful shutdown: %v", err)
}
}JSON API
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"sync"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
type Store struct {
mu sync.Mutex
n int
}
func (s *Store) Create(_ context.Context, u *User) error {
s.mu.Lock()
defer s.mu.Unlock()
s.n++
u.ID = s.n
return nil
}
func main() {
store := &Store{}
mux := http.NewServeMux()
mux.HandleFunc("POST /api/users", func(w http.ResponseWriter, r *http.Request) {
var u User
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // strict mode
if err := dec.Decode(&u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := store.Create(r.Context(), &u); err != nil {
http.Error(w, "save failed", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(u)
})
log.Fatal(http.ListenAndServe(":8080", mux))
}context in a Request
Every *http.Request carries a context: it's cancelled when the client disconnects or the Server calls Shutdown. Pass it on to DB queries and RPC calls so downstream can exit early too.
package main
import (
"context"
"database/sql"
"errors"
"log"
"net/http"
"time"
_ "modernc.org/sqlite"
)
var db *sql.DB
func handler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT 1")
if err != nil {
if errors.Is(err, context.Canceled) {
return // client disconnected; no need to report an error
}
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
w.Write([]byte("ok"))
}
func main() {
var err error
db, err = sql.Open("sqlite", ":memory:")
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}Middleware Chain
Middleware is just a function that "takes a Handler and returns a Handler". To compose several, wrap them by hand layer by layer — no framework needed.
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
"time"
)
func logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic: %v\n%s", rec, debug.Stack())
http.Error(w, "internal", 500)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
})
// compose: recover first, then log
handler := recoverer(logger(mux))
log.Fatal(http.ListenAndServe(":8080", handler))
}HTTP Client: Reuse the Transport
http.Client is thread-safe and should be reused globally — don't new one per request; reusing the Transport's underlying connection pool avoids a handshake every time. Setting timeouts and connection limits on the client is the watershed between "production code vs toy code".
package main
import (
"context"
"fmt"
"io"
"log"
"net"
"net/http"
"time"
)
var httpClient = &http.Client{
Timeout: 10 * time.Second, // total timeout for the whole request (connect, read, redirects)
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
IdleConnTimeout: 90 * time.Second,
DialContext: (&net.Dialer{
Timeout: 3 * time.Second, // TCP connection timeout
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
},
}
func fetch(ctx context.Context, url, token string) error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
return fmt.Errorf("upstream %d: %s", resp.StatusCode, body)
}
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
return nil
}
func main() {
if err := fetch(context.Background(), "https://example.com", "secret"); err != nil {
log.Println("err:", err)
}
}multipart File Upload
package main
import (
"bytes"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
func upload(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(32 << 20); err != nil { // 32 MB in-memory threshold
http.Error(w, err.Error(), 400)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, err.Error(), 400)
return
}
defer file.Close()
dst, err := os.Create(filepath.Join(os.TempDir(), header.Filename))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer dst.Close()
io.Copy(dst, file)
fmt.Fprintln(w, "ok", header.Size)
}
func postFile(url, path string) error {
src, err := os.Open(path)
if err != nil {
return err
}
defer src.Close()
body := &bytes.Buffer{}
mw := multipart.NewWriter(body)
fw, _ := mw.CreateFormFile("file", filepath.Base(path))
io.Copy(fw, src)
mw.Close()
req, _ := http.NewRequest("POST", url, body)
req.Header.Set("Content-Type", mw.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}
func main() {
http.HandleFunc("/upload", upload)
log.Fatal(http.ListenAndServe(":8080", nil))
}Static File Serving
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./public"))))
// a single file
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./public/favicon.ico")
})
log.Fatal(http.ListenAndServe(":8080", mux))
}