Skip to content

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

当前代理只有一个固定后端。下一讲会增加请求日志、响应记录和更灵活的请求处理。