V
Vel·ToolKit
简洁 · 高效 · 即开即用
ZH
第 19 章 / 共 20 章

net/http

HTTP 服务、客户端、中间件、优雅关闭

最小 HTTP 服务

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)
}

路由与 ServeMux(Go 1.22)

Go 1.22 起 net/http 的 ServeMux 支持方法前缀与路径参数;之前需要靠 chi / gorilla mux 之类的库才能做到。日常 CRUD 用内置 mux 已足够,复杂路由再上 chi / gin。

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")           // 路径参数
        q := r.URL.Query().Get("verbose") // 查询字符串
        fmt.Fprintln(w, "user:", id, q)
    })
    mux.HandleFunc("POST /users", createUser)
    mux.HandleFunc("DELETE /users/{id}", deleteUser)
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Server 配置(不要直接 ListenAndServe)

生产服务永远要显式构造 http.Server,原因有二:1) 才能设置超时分层,2) 才能做优雅关闭。http.ListenAndServe 没有任何超时,恶意慢连接可以让你的进程长时间占资源。

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,   // 防 Slowloris
        ReadTimeout:       30 * time.Second,  // 读完整个请求体的上限
        WriteTimeout:      30 * time.Second,  // 写完响应的上限
        IdleTimeout:       120 * time.Second, // keep-alive 闲置上限
        MaxHeaderBytes:    1 << 20,
    }
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}

优雅关闭

收到 SIGINT/SIGTERM 时,Shutdown 会停止接受新连接、等已有请求处理完,再退出。配合 context 设上限避免“永远等不完”的请求把进程卡住。

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() // 严格模式
        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 在请求中

每个 *http.Request 都自带一个 context:客户端断开或 Server 调用 Shutdown 时它会被取消。把它继续传给 DB 查询、RPC 调用——这样下游能跟着提早退出。

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 // 客户端断开,不必报错
        }
        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))
}

中间件链

中间件就是“接 Handler 返回 Handler”的函数。多个组合时手动一层层包即可——不需要框架。

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")
    })

    // 组合:先 recover 再 log
    handler := recoverer(logger(mux))
    log.Fatal(http.ListenAndServe(":8080", handler))
}

HTTP 客户端:复用 Transport

http.Client 是线程安全的,应该全局复用一个,不要每次请求 new 一个;Transport 复用底层连接池才能避免每次握手。给客户端设超时和连接限制是“生产代码 vs 玩具代码”的分水岭。

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net"
    "net/http"
    "time"
)

var httpClient = &http.Client{
    Timeout: 10 * time.Second, // 整个请求(含连接、读、重定向)总超时
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 20,
        IdleConnTimeout:     90 * time.Second,
        DialContext: (&net.Dialer{
            Timeout:   3 * time.Second, // TCP 连接超时
            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 文件上传

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 { // 32MB 内存阈值
        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))
}

静态文件服务

package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./public"))))

    // 单个文件
    mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "./public/favicon.ico")
    })

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