Appearance
16|HTTP 代理基础
从这一讲开始,所有代码围绕 go-gateway 项目展开。这是一个高性能 API 网关,从最小代理开始,逐步叠加路由匹配、负载均衡、限流熔断、管理 API 等功能。网关的核心职责是接收客户端请求,按规则转发到后端服务,再把后端响应返回给客户端。
一、项目初始化
创建项目目录并初始化模块:
bash
mkdir go-gateway
cd go-gateway
go mod init example.com/go-gateway初始目录只有一个 go.mod 和一个 main.go。
二、最小代理
Go 标准库的 net/http/httputil 提供了 ReverseProxy,能处理 HTTP 请求转发的核心逻辑:复制请求头、处理 Hop-by-Hop 头、支持 WebSocket 升级等。最小代理只需要指定目标地址,把收到的请求全部转发过去。
main.go:
go
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
func main() {
// 解析后端地址
target, err := url.Parse("http://127.0.0.1:8081")
if err != nil {
fmt.Fprintln(os.Stderr, "invalid target:", err)
os.Exit(1)
}
// 创建反向代理
proxy := httputil.NewSingleHostReverseProxy(target)
// 启动 HTTP 服务
addr := ":8080"
fmt.Println("proxy listening on", addr)
if err := http.ListenAndServe(addr, proxy); err != nil {
fmt.Fprintln(os.Stderr, "server failed:", err)
os.Exit(1)
}
}运行前需要一个后端服务来接收转发。临时用一个简单的 HTTP 服务:
go
// backend.go(单独一个文件,临时测试用)
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "backend received: %s %s\n", r.Method, r.URL.Path)
})
fmt.Println("backend listening on :8081")
http.ListenAndServe(":8081", nil)
}启动后端:
bash
go run backend.go启动代理:
bash
go run main.go
curl http://127.0.0.1:8080/hello返回:
backend received: GET /hello代理把客户端的 GET /hello 转发到了后端。httputil.ReverseProxy 会自动处理请求路径的拼接——如果代理监听 /api/,后端地址是 http://127.0.0.1:8081/,客户端请求 /api/hello 会被转发为 http://127.0.0.1:8081/hello。
三、HTTP 服务核心概念
go-gateway 里反复出现的几个核心对象:
| 对象 | 作用 |
|---|---|
http.Handler | 处理 HTTP 请求的接口,只有一个 ServeHTTP 方法 |
http.HandlerFunc | 函数类型,满足 Handler 接口,写路由处理时常用 |
http.ServeMux | 路由表,按路径匹配请求到对应的 Handler |
http.ListenAndServe | 监听端口,接收请求,分发给 Handler |
httputil.ReverseProxy | 实现了 Handler 接口,负责反向代理转发 |
http.ListenAndServe 会阻塞当前 goroutine,直到监听失败或进程退出。端口被占用时,错误通常是 bind: address already in use。
四、自定义 Director
ReverseProxy 的默认行为是把请求原样转发到目标地址。如果需要修改请求(比如加 Header、改路径),可以自定义 Director:
go
proxy := httputil.NewSingleHostReverseProxy(target)
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
req.Host = target.Host
}Director 在转发前被调用,可以修改请求对象。上面的代码加了 X-Forwarded-For 头,并把请求 Host 改成后端地址。X-Forwarded-For 是代理场景的标准头,后端服务通过它知道原始客户端地址。
五、错误处理
代理转发失败时(后端服务没启动、网络不通、后端返回 502),ReverseProxy 会自动返回错误响应。如果想自定义错误行为,设置 ErrorHandler:
go
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
fmt.Printf("proxy error: %v\n", err)
http.Error(w, "gateway unavailable", http.StatusBadGateway)
}ErrorHandler 在转发失败时被调用,可以记录日志、返回自定义错误页、触发告警等。
六、当前代码
go
package main
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"os"
)
func main() {
target, err := url.Parse("http://127.0.0.1:8081")
if err != nil {
fmt.Fprintln(os.Stderr, "invalid target:", err)
os.Exit(1)
}
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.Director = func(req *http.Request) {
proxy.Director(req) // 注意:这会递归调用自己,实际代码需要保存原始 Director
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
req.Host = target.Host
}
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
fmt.Printf("proxy error: %v\n", err)
http.Error(w, "gateway unavailable", http.StatusBadGateway)
}
addr := ":8080"
fmt.Println("proxy listening on", addr)
if err := http.ListenAndServe(addr, proxy); err != nil {
fmt.Fprintln(os.Stderr, "server failed:", err)
os.Exit(1)
}
}注意:上面的 proxy.Director 覆盖代码有递归调用自己的 bug,正确做法是先保存原始 Director:
go
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
req.Header.Set("X-Forwarded-For", req.RemoteAddr)
req.Host = target.Host
}七、验证
启动后端和代理后,用 curl 验证:
bash
# 正常转发
curl -i http://127.0.0.1:8080/hello
# 后端关闭时
curl -i http://127.0.0.1:8080/hello
# HTTP/1.1 502 Bad Gateway
# gateway unavailable当前代理只有一个固定后端。下一讲会增加请求日志、响应记录和更灵活的请求处理。