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)
}
执行顺序:
- 请求 →
loggingMiddleware记录开始时间 - 调用
next.ServeHTTP(进入 mux) - mux 找到对应的
helloHandler执行 - 返回,
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)
}
浙公网安备 33010602011771号