11、net_http

11、net/http


HTTP 协议前言

客户端与服务器

  • 客户端:发起请求的一方(浏览器、命令行工具、手机 App)。
  • 服务器:接收请求并返回数据的一方(运行着你的 Go 程序)。

它们之间的通信基于 TCP 连接(可以简单理解为一条双向稳定的数据通道),使用请求-响应模式:一问一答,答完即止(每个请求独立)。

一个 HTTP 请求长什么样?

当访问 http://example.com/users?id=1 时,浏览器实际发送了一段文本:

GET /users?id=1 HTTP/1.1
Host: example.com
User-Agent: Chrome/...
Accept: text/html
...

只需要关注三样东西:

  • 方法(Method):GET(拿数据)、POST(提交数据)、PUT(更新)、DELETE(删除)等。
  • 路径(Path):/users,代表你要访问的资源。
  • 查询参数(Query):?id=1,附加条件。

除此之外还有头部(Header,描述请求的元数据)和可选的请求体(Body)(例如 POST 提交的表单数据)。

一个 HTTP 响应长什么样?

服务器处理后返回:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<html>...</html>

包含:

  • 状态码(Status Code):200 表示成功,404 表示找不到,500 服务器错误。
  • 响应头(Header):描述返回内容的类型、长度等。
  • 响应体(Body):实际的数据(HTML、JSON、图片二进制等)。

net/http 服务端:Hello World 服务

最简单的 HTTP 服务

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, World!")
}

func main() {
    http.HandleFunc("/", helloHandler)
    fmt.Println("Server is running at http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}
  • http.HandleFunc 将路径模式与处理函数绑定。
  • http.ListenAndServe 启动 HTTP 服务器,监听指定地址。
  • 访问 http://localhost:8080 即可看到 “Hello, World!”。
  • http.ResponseWriter:用于构造 HTTP 响应,写入状态码、头部和响应体。
  • http.Request:包含请求的所有信息(方法、URL、请求头、Body 等)。

http.Request

处理函数的第二个参数 r 包含了客户端发来的所有信息。

  • 获取请求方法、路径、查询参数
func detailHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Method:", r.Method)        // GET / POST ...
    fmt.Println("Path:", r.URL.Path)        // /detail
    fmt.Println("Query:", r.URL.RawQuery)  // id=123&name=jack

    // 取单个参数
    id := r.URL.Query().Get("id")  // "123"
    fmt.Fprintf(w, "You asked for item %s", id)
}
  • 读取请求头
// 获取单个头
contentType := r.Header.Get("Content-Type")
// 遍历所有头
for key, values := range r.Header {
    fmt.Println(key, ":", values)
}
  • 读取请求体(POST 数据),对于 POST 请求,数据通常在 Body 中。
    • 表单格式(application/x-www-form-urlencoded)
func loginHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
        return
    }
    // 解析表单(会自动处理 URL 编码)
    err := r.ParseForm()
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }
    username := r.FormValue("username")
    password := r.FormValue("password")
    fmt.Fprintf(w, "Welcome, %s", username)
}
  • JSON 格式(application/json)
body, _ := io.ReadAll(r.Body)
defer r.Body.Close()
// 再用 json.Unmarshal 解析

注意:r.Body 是流,只能读一次。

构造响应:http.ResponseWriter

处理函数的第一个参数 w 用于向客户端返回数据。

  • 写入内容
fmt.Fprintln(w, "plain text")     // 普通文本
w.Write([]byte("raw bytes"))      // 字节流
  • 设置状态码和头部
// 必须写在 Write 之前,否则无效
w.Header().Set("X-Custom-Header", "hello")
w.WriteHeader(http.StatusCreated) // 201

fmt.Fprintln(w, `{"status":"ok"}`)

w.WriteHeader 会触发实际的状态行发送,默认第一次 Write 会隐式发送 200 OK,所以要先设置。

  • 返回 JSON(常用)
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
user := User{"Alice", 30}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)

路由分派

上面的 http.HandleFunc 其实属于 DefaultServeMux(默认的路由分发器)。它只支持前缀匹配,且容易冲突,所以我们常自己创建一个 ServeMux。

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/users", usersHandler)
    mux.HandleFunc("/products", productsHandler)

    // 使用自定义 mux
    http.ListenAndServe(":8080", mux)
}

如果需要支持 HTTPS,只需将 ListenAndServe 换成 ListenAndServeTLS,并提供证书和私钥文件。

http.ListenAndServeTLS(":8080", "cert.pem", "key.pem", mux)

RESTful 风格路由

自定义路由分派器

标准库的 http.HandleFunc 只支持简单前缀匹配。要实现 RESTful 风格,需自己解析路径与方法:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/items", itemsHandler)
    mux.HandleFunc("/items/", itemHandler) // 带 ID 的请求

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

func itemsHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // 列出所有 items
        fmt.Fprintln(w, "List items")
    case http.MethodPost:
        // 创建新 item
        fmt.Fprintln(w, "Create item")
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func itemHandler(w http.ResponseWriter, r *http.Request) {
    // 提取 URL 中的 ID:/items/123
    id := strings.TrimPrefix(r.URL.Path, "/items/")
    // 可以加入更严谨的路径解析
    switch r.Method {
    case http.MethodGet:
        fmt.Fprintf(w, "Get item %s", id)
    case http.MethodPut:
        fmt.Fprintf(w, "Update item %s", id)
    case http.MethodDelete:
        fmt.Fprintf(w, "Delete item %s", id)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}
  • Go 1.22 之后,http.NewServeMux 已支持路径参数和方法匹配(如 GET /items/{id}),但学习阶段手动实现有助于深入理解。
  • 使用 r.URL.Path 解析路径,用 r.Method 获取 HTTP 方法。

设置响应头与状态码

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"id":1}`))
  • 必须在 WriteHeader 之前设置 Headers。
  • WriteHeader 调用后不能再更改状态码。

中间件概念 + 日志中间件

中间件是什么?

一个中间件接收一个 http.Handler,返回一个新的 http.Handler:

中间件是一个函数,它接收一个 http.Handler 并返回一个新的 http.Handler,用于在请求前后执行公共逻辑(如日志、鉴权、恢复、度量等)。

假设希望记录每个请求的处理时间,或者在请求到达处理函数前检查权限。如果每个函数都写一遍相同代码,会非常冗余。中间件就是用来解决这个问题的。

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // 调用下一个 handler
        next.ServeHTTP(w, r)
        // 请求结束后打印耗时
        duration := time.Since(start)
        fmt.Printf("[%s] %s %s %v\n", r.Method, r.URL.Path, r.RemoteAddr, duration)
    })
}

中间件链的执行顺序

中间件像一层层的包装,请求从最外层进入,逐个穿过中间件到达真正的处理函数,响应再从内层返回。

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", helloHandler)

    // 包装中间件
    handler := loggingMiddleware(mux)
    // 还可以继续加其他中间件,形成链
    // handler = authMiddleware(handler)
    // handler = corsMiddleware(handler)

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

执行顺序:

  1. 请求 → loggingMiddleware 记录开始时间
  2. 调用 next.ServeHTTP(进入 mux)
  3. mux 找到对应的 helloHandler 执行
  4. 返回,loggingMiddleware 记录耗时并打印

多个中间件按包装的顺序从外到内执行请求部分,从内到外执行响应部分。

HTTP 客户端

net/http 也提供客户端功能,方便调用其他 API 或抓取网页。

最简单的 GET 请求

resp, err := http.Get("https://api.github.com")
if err != nil {
    panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))

发送 POST 请求(JSON)

data := map[string]string{"name": "Alice"}
jsonData, _ := json.Marshal(data)

resp, err := http.Post(
    "https://httpbin.org/post",
    "application/json",
    bytes.NewReader(jsonData),
)
// 检查 err,读取 body

自定义请求头、超时等

使用 http.Client 和 http.NewRequest:

client := &http.Client{
    Timeout: 5 * time.Second,
}

req, _ := http.NewRequest("GET", "https://api.example.com", nil)
req.Header.Set("Authorization", "Bearer token123")

resp, err := client.Do(req)
// ...

使用 Context 控制超时

context 的作用

  • 取消信号:在多个 goroutine 之间传递取消指令。
  • 超时控制:设定操作的最长执行时间。
  • 传递请求范围的值(少用,避免滥用)。

每个 http.Request 都自带一个 context.Context,通过 r.Context() 获取。

带超时的 HTTP 请求(客户端)

func main() {
    // 1. 设置 5 秒的超时时间
    // ctx: 带有超时控制的上下文
    // cancel: 一个取消函数,用来手动释放资源
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    
    // ⚠️ 极其重要:习惯性地加上 defer cancel()
    // 如果任务在 5 秒内顺利完成了,我们需要手动调用 cancel() 通知底层释放定时器和子协程资源,防止内存泄漏。
    defer cancel() 

    go doSomething(ctx)

    select {
    case <-ctx.Done():
        // 这里会被触发,要么是 5 秒超时了,要么是主动调用了 cancel()
        fmt.Println("任务结束或超时, reason:", ctx.Err())
    }
}

func doSomething(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            // 2. 在 Goroutine 里监听 ctx.Done()
            // 一旦收到信号,立刻清理并退出
            fmt.Println("收到停止信号,优雅退出")
            return
        default:
            // 正常执行耗时任务
            time.Sleep(1 * time.Second)
            fmt.Println("正在处理中...")
        }
    }
}
  • context.WithTimeout 创建一个带超时的 context,到期后自动取消。
  • defer cancel() 确保资源释放。
  • 若服务端也使用相同的 context,可以感知到客户端断开(r.Context().Err())。

在服务端使用 context 控制耗时操作

func slowHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    select {
    case <-time.After(5 * time.Second): // 模拟长时操作
        fmt.Fprintln(w, "finished")
    case <-ctx.Done():
        // 客户端已断开或超时
        http.Error(w, "request cancelled", http.StatusRequestTimeout)
    }
}

综合练习:简易文件服务器

实现功能:

  • 列出当前目录的文件,提供下载(静态文件服务)。
  • 支持通过 POST 上传文件(multipart/form-data)。

静态文件服务

直接使用 http.FileServer 即可,但为了演示手动实现:

func listFilesHandler(w http.ResponseWriter, r *http.Request) {
    files, err := os.ReadDir(".")
    if err != nil {
        http.Error(w, "Unable to list files", http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    fmt.Fprintln(w, "<h1>Files</h1><ul>")
    for _, f := range files {
        name := f.Name()
        fmt.Fprintf(w, `<li><a href="/download?file=%s">%s</a></li>`, name, name)
    }
    fmt.Fprintln(w, "</ul>")
    fmt.Fprintln(w, `<form action="/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file"><button>Upload</button></form>`)
}

func downloadHandler(w http.ResponseWriter, r *http.Request) {
    filename := r.URL.Query().Get("file")
    if filename == "" {
        http.Error(w, "file parameter missing", http.StatusBadRequest)
        return
    }
    // 安全检查:防止目录穿越
    if strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
        http.Error(w, "invalid file name", http.StatusBadRequest)
        return
    }
    http.ServeFile(w, r, filename)
}

文件上传处理

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    // 限制最大 10MB
    r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
    err := r.ParseMultipartForm(10 << 20) // 内存最大 10MB,超出写入临时文件
    if err != nil {
        http.Error(w, "Unable to parse form", http.StatusBadRequest)
        return
    }

    file, handler, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "Error retrieving file", http.StatusBadRequest)
        return
    }
    defer file.Close()

    // 创建目标文件
    dst, err := os.Create(handler.Filename)
    if err != nil {
        http.Error(w, "Unable to create file", http.StatusInternalServerError)
        return
    }
    defer dst.Close()

    _, err = io.Copy(dst, file)
    if err != nil {
        http.Error(w, "Failed to save file", http.StatusInternalServerError)
        return
    }
    fmt.Fprintf(w, "File uploaded successfully: %s", handler.Filename)
}

组装服务

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", listFilesHandler)
    mux.HandleFunc("/download", downloadHandler)
    mux.HandleFunc("/upload", uploadHandler)

    // 添加日志中间件
    handler := loggingMiddleware(mux)

    fmt.Println("File server at http://localhost:8080")
    http.ListenAndServe(":8080", handler)
}
posted @ 2026-09-24 16:07  爱喝奶茶的CC怪  阅读(16)  评论(0)    收藏  举报