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