Go 代理项目:14 周每日学习路线
项目全貌
一个从零开始的 Go 反向代理,分四个阶段进化成一个生产级 SRE 工具。每个阶段解决一类真实问题,边学语法边写功能。每周按 5 个工作日拆分,周末可以复习或补进度。
先说一下 gRPC
你原始规划里提到了”协议升级:把 HTTP 请求转成 gRPC”。简单解释一下这是什么。
gRPC 是 Google 搞的一个远程过程调用(RPC)框架。普通的 HTTP API 用 JSON 传数据,人能读懂但体积大、解析慢。gRPC 用一种叫 Protocol Buffers(protobuf)的二进制格式传数据,体积小、速度快,而且跑在 HTTP/2 上,天然支持多路复用和流式传输。
在微服务架构里,服务之间的内部通信经常用 gRPC 而不是 HTTP+JSON,因为性能好很多。代理做”协议升级”的意思是:外部客户端发普通 HTTP+JSON 请求进来,代理把它转成 gRPC 格式发给后端微服务。这样外部用户不需要知道后端用的是 gRPC,代理帮你做了翻译。
gRPC 和普通 HTTP API 的区别在于三个层面。传输层:普通 HTTP API 跑在 HTTP/1.1 上,一个连接同一时间只能处理一个请求;gRPC 跑在 HTTP/2 上,一个连接可以同时处理多个请求(多路复用)。序列化格式:普通 API 用 JSON(文本),gRPC 用 protobuf(二进制),同样的数据 protobuf 通常比 JSON 小 3-10 倍。接口定义:gRPC 用 .proto 文件严格定义接口,用 protoc 编译器自动生成代码,客户端和服务端的数据结构是编译时确定的。
gRPC 代理涉及 HTTP/2 帧处理、protobuf 序列化/反序列化、服务定义文件编译、动态反射调用等,复杂度比较高,放在第 13-14 周作为进阶目标。
阶段一:隐身斗篷——透明代理 / 协议转换(第 1-4 周)
第 1 周:Go 基础 + 读配置
这周不急着写代理,先把 Go 的基础语法打牢。
Day 1:环境搭建 + A Tour of Go(上)
安装 Go,配置好编辑器。打开 A Tour of Go(https://go.dev/tour/),过 Basics 部分:变量声明(var 和 :=)、基本类型(int、string、bool)、函数定义(func)、多返回值。不用全记住,有个印象就行。
Day 2:A Tour of Go(中)
继续过 Flow Control(for、if、switch)和 More Types 的前半部分(指针、struct)。Go 没有 while 循环,所有循环都用 for。struct 是 Go 里组织数据的核心方式,类似其他语言的 class 但没有继承。
Day 3:A Tour of Go(下)
过 More Types 的后半部分(slice、map、range)和 Methods and Interfaces 的前半部分。slice 和 map 会贯穿整个项目。如果 interface 看不懂没关系,后面第 9 周会专门学。
Day 4:第一个程序 + struct 和 json
关掉教程,自己写一个 main.go。定义一个 Config struct,包含 ListenPort int 和 TargetURL string 两个字段,加上 json tag(比如 `json:"listen_port"`)。创建一个 config.json 文件,用 os.ReadFile 读取,用 json.Unmarshal 映射到 struct,用 fmt.Println 打印出来。确认 go run main.go 能跑通。
Day 5:错误处理 + 练习
Go 的错误处理模式是 if err != nil { log.Fatal(err) },从今天开始习惯它。给 Day 4 的代码加上完整的错误处理:文件不存在怎么办、JSON 格式错误怎么办。然后做个小练习:改 config.json 的内容,故意写错 JSON 格式,看程序怎么报错。学会用 log.Printf 输出带格式的日志。
第 2 周:HTTP 服务器 + 请求转发
Day 1:启动一个 HTTP 服务器
学 net/http 包。用 http.HandleFunc("/", handler) 注册一个处理函数,用 http.ListenAndServe(":8080", nil) 启动服务器。handler 函数的签名是 func(w http.ResponseWriter, r *http.Request),先让它返回一个固定字符串 “hello proxy”。用 curl localhost:8080 验证。
Day 2:理解 http.Request 和 http.ResponseWriter
在 handler 里打印请求的各种信息:r.Method(GET/POST)、r.URL.Path(路径)、r.Header(所有请求头)、r.RemoteAddr(客户端地址)。用 w.WriteHeader(200) 设置状态码,用 w.Write([]byte("hello")) 写响应体。用 curl 发不同的请求(GET、POST、带 Header),观察打印结果。
Day 3:用 http.Client 发送请求
学 http.NewRequest 构造请求和 http.Client{} 的 Do 方法发送请求。写一个小程序:向 http://httpbin.org/get 发 GET 请求,打印响应状态码和 Body。理解 resp.Body 是一个 io.ReadCloser,必须用 defer resp.Body.Close() 关闭。
Day 4:实现请求转发
把前几天学的串起来:handler 收到请求后,用 http.NewRequest 构造一个新请求(方法、URL、Body 都从原始请求复制),用 http.Client 发送到 config.json 里配置的目标地址,用 io.Copy(w, resp.Body) 把上游响应写回客户端。
Day 5:处理请求头复制 + 测试
转发时还需要把原始请求的 Header 复制到新请求上(遍历 r.Header,逐个 Set 到新请求)。同时把上游响应的 Header 也复制回客户端。用 curl localhost:8080/get 测试,确认返回的内容和直接访问 httpbin.org/get 一样。这就是一个能跑的最简代理了。
第 3 周:伪装 Host + HTTPS 卸载
Day 1:伪装 Host
在转发逻辑里,转发之前加两行:req.Header.Set("Host", targetHost) 和 req.Host = targetHost。这样目标服务器看到的 Host 头就是你指定的值,而不是 localhost:8080。用 httpbin.org/headers 验证:转发后返回的 headers 里 Host 应该是 httpbin.org。
Day 2:理解 TLS/HTTPS + 生成自签名证书
花半天理解 HTTPS 的工作原理:客户端和服务器通过 TLS 握手协商加密方式,服务器出示证书证明身份,之后所有数据加密传输。”HTTPS 卸载”的意思是代理对外提供 HTTPS,对内用 HTTP 转发给后端。然后用 openssl 生成自签名证书:openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes。
Day 3:启动 HTTPS 服务器
学 crypto/tls 包,用 tls.LoadX509KeyPair("cert.pem", "key.pem") 加载证书。把 http.ListenAndServe 换成 http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)。用 curl -k https://localhost:8443/get 测试(-k 跳过证书验证)。转发到后端仍然用 HTTP,HTTPS 卸载完成。
Arch Linux 提示:你可以用
mkcert代替 openssl 生成证书(pacman -S mkcert)。mkcert 会自动创建一个本地信任的 CA 并安装到系统信任链里,生成的证书浏览器和 curl 都不会报红,测试时不需要-k参数。运行mkcert -install初始化 CA,然后mkcert localhost 127.0.0.1生成证书。
Day 4:同时监听 HTTP 和 HTTPS
实际场景中代理可能需要同时提供 HTTP(8080)和 HTTPS(8443)。用两个 goroutine 分别启动两个服务器:go http.ListenAndServe(...) 和 http.ListenAndServeTLS(...)。这里会第一次用到 goroutine,但很简单,就是在函数调用前加个 go。
Day 5:学习 httputil.ReverseProxy + 回顾
Go 标准库自带了 net/http/httputil.ReverseProxy,它就是一个现成的反向代理实现。读一下它的源码(不长),理解它怎么处理请求复制、响应转发、错误处理。对比你自己写的转发逻辑,看看有什么可以改进的。不需要替换你的代码,理解思路就行。回顾这三周的代码,整理一下结构。
第 4 周:WebSocket 协议升级
这周比较有挑战,不要急。
Day 1:理解 WebSocket 协议
不写代码。WebSocket 通过一次 HTTP 升级握手(Upgrade: websocket)建立连接后,双方可以随时互发消息,连接一直保持。适合聊天、实时推送这类场景。用浏览器开发者工具打开一个 WebSocket 网站(比如 websocket.org 的 echo 测试),观察握手过程和消息收发。
Day 2:搭建 WebSocket 测试服务
安装 gorilla/websocket 库(go get github.com/gorilla/websocket),这里会学到 go get 安装第三方库和 go.mod 依赖管理。写一个最简单的 WebSocket echo 服务器:客户端发什么,服务端原样返回。安装 wscat(npm install -g wscat)测试连接。
Day 3:学习 http.Hijacker 接口
http.Hijacker 能从 HTTP 连接中”劫持”底层的 TCP 连接。调用 w.(http.Hijacker).Hijack() 后,你拿到的是一个裸的 net.Conn,HTTP 层不再管这个连接了。写一个小例子:收到请求后 Hijack 连接,直接往 TCP 连接上写一段文字,用 curl 观察效果。
Day 4:实现 WebSocket 代理转发
代理收到 WebSocket 升级请求后,先向后端建立 WebSocket 连接,然后用两个 goroutine 做双向转发:一个从客户端读数据写到后端,一个从后端读数据写到客户端。用 wscat 通过代理连接 Day 2 的 echo 服务,验证双向通信。
Day 5:错误处理 + 连接关闭
处理各种边界情况:客户端断开时关闭后端连接,后端断开时通知客户端。加上日志,打印 WebSocket 连接的建立和关闭事件。如果 Day 4 没搞定,今天继续调试,不要急着往下走。
阶段二:安检员——认证与过滤(第 5-6 周)
原来的鉴权和过滤分了两周,但 Token 鉴权和 UA 黑名单的语法很简单(就是 if-else 和 slice 遍历),所以压缩成一周,把省下来的时间给内容过滤和动态路由多留一天缓冲。
第 5 周:Token 鉴权 + UA 黑名单 + 内容过滤
Day 1:学 slice 和 for…range + 实现 Token 鉴权
Go 的 slice(切片)是动态数组,用 []string{"a", "b"} 创建,用 len() 获取长度,用 append() 添加元素,用 for i, v := range slice 遍历。在 config.json 里加一个 "token": "my-secret-token" 字段。在转发逻辑的最前面加检查:r.Header.Get("X-Token") 读取请求头,不等于配置里的 token 就返回 403。用 curl -H "X-Token: my-secret-token" localhost:8080/get 测试通过,不带 header 测试返回 403。
Day 2:实现 UA 黑名单
在 config.json 里加 "ua_blacklist": ["BadBot", "Scrapy"]。读取请求的 User-Agent 头,用 for...range 遍历黑名单,用 strings.Contains(配合 strings.ToLower 做大小写不敏感匹配)检查 UA 是否包含黑名单关键词。命中就返回 403。把鉴权和 UA 检查抽成独立函数:func checkToken(r *http.Request, token string) bool 和 func checkUA(r *http.Request, blacklist []string) bool,保持 handler 干净。
Day 3:学 map + bytes 包 + 理解 Body 一次性读取问题
Go 的 map[string]string 是键值对集合,用 m[key] 读取,用 for k, v := range m 遍历。然后理解一个关键问题:HTTP 请求的 Body 是一个 io.ReadCloser,只能读一次。读完之后如果还要转发,必须用 bytes.NewReader(bodyBytes) 重新包装成一个新的 Reader 赋回 r.Body。写个小例子验证这个行为。
Day 4:实现内容过滤(敏感词拦截)
在 config.json 里加 "sensitive_words": ["password", "secret"]。在转发之前,用 io.ReadAll(r.Body) 读取整个 Body,用 bytes.Contains 检查是否包含敏感词。如果包含,返回 400。如果不包含,用 bytes.NewReader(bodyBytes) 重新包装 Body 继续转发。用 curl -X POST -d "my password is 123" localhost:8080/post 测试拦截。
Day 5:测试所有安检功能
同时测试 Token 鉴权、UA 黑名单、内容过滤的各种组合:不带 Token、带错误 Token、带正确 Token 但 UA 在黑名单里、Token 和 UA 都通过但 Body 有敏感词。加上日志,打印每个被拒绝的请求的原因。确保所有情况都正确处理。
第 6 周:动态路由 + 轮询负载均衡
原来路由和负载均衡分了两周,但路由的核心就是 map + 前缀匹配,负载均衡的核心就是取模运算,合并到一周刚好。
Day 1:实现动态路由
把 config.json 的单个 target_url 改成路由表:"routes": {"/api": "http://backend-a:8080", "/static": "http://backend-b:8080"}。在 handler 里用 strings.HasPrefix(r.URL.Path, prefix) 遍历路由表找到匹配的后端。注意 map 遍历顺序是随机的,所以把路由前缀放进一个 slice 里按长度从长到短排序,先匹配最具体的路由。加一个默认路由兜底。
Day 2:学方法(method)和指针 + 定义 LoadBalancer struct
Go 的 struct 可以绑定方法:func (lb *LoadBalancer) Next() string。*LoadBalancer 是指针接收者,方法可以修改 struct 的字段。如果用值接收者,每次调用都是在副本上操作,修改不会生效。定义一个 LoadBalancer struct,包含 backends []string 和 counter int64,写一个 Next() 方法返回 backends[counter % len(backends)] 并递增 counter。
Day 3:用 atomic 实现并发安全 + 改造路由表
学 sync/atomic 包,把 counter++ 换成 atomic.AddInt64(&lb.counter, 1)。然后改造 config.json 的路由表,支持多后端:"routes": {"/api": ["http://pod1:8080", "http://pod2:8080", "http://pod3:8080"]}。Config struct 对应改成 Routes map[string][]string。每个路由前缀对应一个 LoadBalancer 实例。
Day 4:集成到代理 + 测试轮询
在 handler 里先匹配路由,再调用对应 LoadBalancer 的 Next() 获取后端地址。启动 3 个简单后端(分别返回 “I am pod1”、”I am pod2”、”I am pod3”),用 for i in {1..9}; do curl localhost:8080/api; done 连续请求 9 次,确认响应依次是 pod1、pod2、pod3 循环。
进阶思考:基础的 Round Robin 假设所有后端性能一样,但 SRE 场景下后端节点性能不一是很常见的。如果有余力,可以尝试实现加权轮询(Weighted Round Robin)——在 config.json 里给每个后端配一个权重,权重高的分到更多请求。或者实现最少连接(Least Connections)——每次选当前活跃连接数最少的后端。这两个算法不复杂,但能让你的代理在真实场景下更实用。不急,可以留到 12 周收尾时再加。
Day 5:整合测试阶段一和阶段二
这是一个里程碑。同时验证所有已完成的功能:HTTPS 卸载、Host 伪装、WebSocket 转发、Token 鉴权、UA 黑名单、内容过滤、动态路由、轮询负载均衡。整理代码结构,把不同功能拆到不同的 .go 文件里(比如 auth.go、router.go、loadbalancer.go)。
阶段三:交警——流量调度与限流(第 7-9 周)
这是整个路线里最陡的坡,goroutine、channel、select、context、Mutex 集中在这里。原来 2 周,现在给 3 周,多出来的一周用来消化并发模型。
第 7 周:Go 并发模型 + 限流器
Day 1:学 goroutine 基础
goroutine 是 Go 的轻量级线程,用 go func() { ... }() 启动。写几个小练习:启动 5 个 goroutine 分别打印不同的数字,观察输出顺序是随机的。理解 time.Sleep 可以让 goroutine 等待,但主 goroutine 退出后所有子 goroutine 都会被杀掉。
Day 2:学 channel 基础
channel 是 goroutine 之间传递数据的管道。ch := make(chan int) 创建无缓冲 channel,ch <- 42 发送(会阻塞直到有人接收),v := <-ch 接收(会阻塞直到有人发送)。写一个小练习:一个 goroutine 往 channel 发 10 个数字,主 goroutine 接收并打印。然后学带缓冲的 channel:ch := make(chan int, 5),缓冲没满时发送不阻塞。
Day 3:学 select + time.Ticker
select 同时监听多个 channel,谁先有数据就执行谁的分支,类似 switch 但用于 channel。time.Ticker 是一个定时器,ticker := time.NewTicker(time.Second) 每秒往 ticker.C 这个 channel 发一个信号。写一个小程序:用 select 同时监听一个 ticker(每秒打印 “tick”)和一个 quit channel(收到信号就退出)。
Day 4:学 sync.Map + 实现限流器核心逻辑
sync.Map 是并发安全的 map,不需要手动加锁。用 m.Store(key, value) 存,m.Load(key) 取,m.Range(func(k, v) bool { ... }) 遍历。创建一个 RateLimiter struct,内部用 sync.Map 存储每个 IP 的请求计数。Allow(ip string) bool 方法:读取当前计数,超过 5 就返回 false,否则加 1 返回 true。启动一个 goroutine 监听 Ticker,每秒清零所有计数。
Day 5:集成限流器到代理 + 测试
用 net.SplitHostPort(r.RemoteAddr) 提取客户端 IP,调用 limiter.Allow(ip),返回 false 就响应 429 Too Many Requests。用 ab -n 100 -c 10 http://localhost:8080/api 压测,观察日志确认同一个 IP 每秒只有前 5 个请求通过。
第 8 周:熔断与降级
Day 1:理解熔断器状态机
不写代码,先在纸上画状态图。三种状态:Closed(关闭,正常转发)→ 连续错误超过阈值 → Open(打开,直接返回 503)→ 等待超时到期 → Half-Open(半开,放一个请求试探)→ 试探成功回到 Closed,失败回到 Open。理解每个状态转换的触发条件。
Day 2:学 sync.Mutex + 定义 CircuitBreaker struct
sync.Mutex 是互斥锁,mu.Lock() 加锁,mu.Unlock() 解锁,通常配合 defer mu.Unlock() 使用。定义一个 CircuitBreaker struct,包含:state string(当前状态)、failCount int(连续失败次数)、threshold int(阈值,比如 10)、lastFailTime time.Time、timeout time.Duration(恢复等待时间,比如 60 秒)、mu sync.Mutex。
Day 3:学 context.WithTimeout + 实现 Allow 和 RecordResult
context.WithTimeout 给操作设超时:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second),别忘了 defer cancel()。实现 Allow() bool:Closed 状态返回 true;Open 状态检查是否超过 timeout,超过了切到 Half-Open 返回 true,没超过返回 false;Half-Open 状态返回 true(放一个请求试探)。实现 RecordResult(success bool):成功则重置 failCount 切到 Closed;失败则递增 failCount,超过阈值切到 Open。
Day 4:集成到代理
在转发逻辑里加入熔断器:转发前调用 cb.Allow(),返回 false 就直接响应 503 “系统繁忙”。转发请求时用 req.WithContext(ctx) 加上 5 秒超时。转发后根据响应状态码调用 cb.RecordResult(statusCode < 500)。
Day 5:测试熔断和恢复
启动后端,正常请求确认通过。关掉后端,连续发请求,观察日志:前 10 个请求报错,第 11 个开始直接返回 503。等 60 秒后再发请求,观察半开状态的试探行为。重启后端,确认代理自动恢复转发。如果有 bug,今天调试。
第 9 周:并发复习 + 耗时统计 + defer
这周节奏放缓,消化前两周的并发知识,同时开始做可观测性。
Day 1:并发复习
回顾 goroutine、channel、select、Mutex 的用法。重新读一遍限流器和熔断器的代码,确保每一行都理解。如果有不清楚的地方,去 Go by Example(https://gobyexample.com/)查对应的例子。
Day 2:学 defer + time 包做耗时统计
defer 在函数退出时执行,不管是正常 return 还是 panic。在 handler 开头写 start := time.Now(),然后 defer func() { log.Printf("[%s] %s → %d (%v)", r.Method, r.URL.Path, statusCode, time.Since(start)) }()。注意 statusCode 需要用一个自定义的 ResponseWriter 包装器来捕获(因为标准的 ResponseWriter 写完状态码后你读不回来)。
Day 3:写一个 StatusCapture ResponseWriter
定义一个 struct 包装 http.ResponseWriter,重写 WriteHeader 方法来记录状态码。这里会更深入理解 Go 的 interface:你的 struct 只要实现了 http.ResponseWriter 的三个方法(Header()、Write()、WriteHeader()),就可以当 ResponseWriter 用。
Day 4:学 interface + 理解 http.Handler
Go 的 http.Handler 接口只有一个方法:ServeHTTP(w http.ResponseWriter, r *http.Request)。任何实现了这个方法的 struct 都自动满足接口,不需要显式声明。理解中间件模式的签名:func(next http.Handler) http.Handler——接收一个 Handler,返回一个新的 Handler。
Day 5:写第一个中间件(Logging)
把 Day 2 的耗时统计逻辑封装成一个中间件函数。在调用 next.ServeHTTP(w, r) 之前记录开始时间,之后计算耗时并打印。验证中间件能正确包装现有的 handler。
阶段四:监控探针——可观测性(第 10-14 周)
第 10 周:Prometheus 指标 + 全量日志存数据库
Day 1:引入 Prometheus 客户端库
运行 go get github.com/prometheus/client_golang/prometheus 和 go get github.com/prometheus/client_golang/prometheus/promhttp。注册两个指标:一个 Counter proxy_requests_total(按状态码和路径分标签),一个 Histogram proxy_request_duration_seconds(记录延迟分布)。在 Logging 中间件里调用 counter.Inc() 和 histogram.Observe(elapsed.Seconds())。
Day 2:暴露 /metrics 端点 + 验证
用 http.Handle("/metrics", promhttp.Handler()) 暴露指标端点。启动代理,发几个请求,然后 curl localhost:8080/metrics 查看输出。你应该能看到 Prometheus 格式的指标数据。如果你的 K3s 集群里跑着 Prometheus,可以配置它来抓取这个端点。
Day 3:学 database/sql + 连接 SQLite
Go 的 database/sql 是标准库的数据库接口,具体数据库通过驱动实现。先用 SQLite(轻量,不需要装数据库服务):go get github.com/mattn/go-sqlite3。用 sql.Open("sqlite3", "./proxy.db") 连接,用 db.Exec 创建一张 request_logs 表(字段:id、timestamp、method、url、status_code、duration_ms、request_body)。
避坑:SQLite 在并发写时会报
database is locked错误。你的代理是高并发的,必须在连接后设置db.SetMaxOpenConns(1)限制为单连接,让写操作排队。这能解决问题但会成为瓶颈。如果后续发现 SQLite 扛不住,可以换成 PostgreSQL(驱动用github.com/jackc/pgx/v5/stdlib),连接字符串改一下就行,database/sql的接口是通用的,业务代码几乎不用改。
Day 4:学 io.TeeReader + 实现同步写库
io.TeeReader(r, w) 返回一个 Reader,从它读取数据时,数据会同时写入 w。用它复制请求 Body:var bodyBuf bytes.Buffer,teeReader := io.TeeReader(r.Body, &bodyBuf)。转发完成后,用 db.Exec("INSERT INTO request_logs ...") 把请求信息写入数据库。先做同步写,验证数据能正确存入。
Day 5:用带缓冲 channel 改成异步写库
同步写库会拖慢请求。创建 logCh := make(chan LogEntry, 1000),handler 里把日志条目扔进 channel 就返回,另一个 goroutine 从 channel 取数据写库。用 select 加 default 分支处理 channel 满的情况——丢弃日志而不是阻塞请求。加一个 Prometheus Counter 记录丢弃数量。
第 11 周:流量镜像
Day 1:理解流量镜像的原理
流量镜像就是把线上请求复制一份发到测试环境。生产后端正常处理并返回响应给客户端,测试后端也收到一模一样的请求但响应被丢弃。关键原则:镜像是”发了就忘”(fire and forget),镜像失败不能影响正常请求。
Day 2:用 bytes.Buffer 复制请求 Body
用 io.ReadAll(r.Body) 把 Body 读出来存到 []byte,然后用 bytes.NewReader(bodyBytes) 分别创建两个 Reader——一个给正常转发,一个给镜像请求。在 config.json 里加 "mirror_target": "http://test-backend:8080"。
Day 3:用 goroutine 异步发送镜像请求
用 http.NewRequest 构造一个新请求(复制方法、URL、Header、Body),go sendMirror(mirrorReq) 异步发送。主流程不等待镜像结果。给镜像请求用 context.WithTimeout 设独立的 3 秒超时。
Day 4:学 recover + 保护镜像 goroutine
recover 能捕获 goroutine 里的 panic。在镜像 goroutine 开头加 defer func() { if r := recover(); r != nil { log.Printf("mirror panic: %v", r) } }(),防止镜像崩溃影响主进程。
Day 5:测试镜像功能
启动两个后端:一个”生产”后端返回正常响应,一个”测试”后端只打印收到的请求。发请求到代理,确认客户端收到生产后端的响应,同时测试后端也出现了请求日志。关掉测试后端,确认镜像失败不影响正常响应。
第 12 周:中间件链整合 + 收尾
Day 1:把所有功能重构为中间件
把鉴权、限流、UA 黑名单、内容过滤、Prometheus 指标、日志等逻辑都重构成独立的中间件函数,签名统一为 func(next http.Handler) http.Handler。比如 func TokenAuth(token string) func(http.Handler) http.Handler,检查 Token 不通过就返回 403,通过就调用 next.ServeHTTP(w, r)。
Day 2:串联中间件链
写一个 Chain 函数把多个中间件串起来。最终效果:请求依次经过 日志 → Prometheus → 限流 → Token 认证 → UA 黑名单 → 内容过滤 → 路由 → 负载均衡 → 熔断 → 转发。每一层都可以决定继续传递还是直接返回错误。
Day 3:学 flag 包 + 命令行参数
用 flag 包支持命令行参数:--config 指定配置文件路径,--port 指定监听端口。调用 flag.Parse() 解析。这样启动代理时可以 ./proxy --config /etc/proxy/config.json --port 9090。
Day 4:实现优雅关闭(Graceful Shutdown)
用 os/signal 监听 SIGINT 和 SIGTERM。收到信号后调用 server.Shutdown(ctx) 停止接受新连接,等待在途请求处理完再退出。同时关闭数据库连接、停止日志 goroutine。
Docker 化:写一个多阶段构建(Multi-stage Build)的 Dockerfile。第一阶段用
golang:1.22编译,第二阶段用scratch或gcr.io/distroless/static作为底座,只复制编译好的二进制文件进去。Go 编译出来的是静态链接的单文件,打包出来的镜像可能只有 10-15MB。然后docker build -t go-proxy .构建,docker run -p 8080:8080 go-proxy运行,确认代理在容器里正常工作。这个镜像可以直接推到你的 K3s 集群里跑。
Day 5:写单元测试 + 最终验收
学 testing 包。给限流器的 Allow() 写测试:连续调用 6 次,前 5 次返回 true 第 6 次返回 false。给熔断器写测试:连续 RecordResult(false) 超过阈值后 Allow() 返回 false。给轮询写测试:3 个后端调用 6 次,每个出现 2 次。用 go test ./... 运行。然后做一次完整的端到端验收。
进阶:gRPC 协议升级(第 13-14 周)
不需要重写代理。是在已有的中间件链和路由系统上加一个新的路由规则:当请求路径匹配 /grpc/... 时,走 gRPC 转发 handler;其他路径还是走原来的 HTTP 转发。限流、鉴权、Prometheus 指标这些中间件照常工作,gRPC 转换只是在链的末端替换了转发方式。
第 13 周:protobuf + gRPC 基础
Day 1:安装 protoc + 写第一个 .proto 文件
安装 Protocol Buffers 编译器(macOS 用 brew install protobuf),安装 Go 插件(go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 和 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest)。写一个 greeter.proto,定义一个 Greeter 服务,有一个 SayHello 方法,请求参数是 name(string),返回值是 message(string)。
Day 2:生成 Go 代码 + 写 gRPC 服务端
运行 protoc --go_out=. --go-grpc_out=. greeter.proto 生成两个 .go 文件。写一个 gRPC 服务端:实现生成的接口,用 grpc.NewServer() 创建服务器,监听 50051 端口。学 google.golang.org/grpc 这个核心库。
Day 3:写 gRPC 客户端 + 验证通信
写一个 gRPC 客户端:用 grpc.Dial 连接服务端,调用 SayHello 方法。运行服务端和客户端,确认通信正常。理解完整链路:.proto 定义 → protoc 生成代码 → 服务端实现 → 客户端调用。
Day 4:学 JSON ↔ protobuf 转换
学 google.golang.org/protobuf/encoding/protojson 包。protojson.Unmarshal 把 JSON 转成 protobuf 消息,protojson.Marshal 把 protobuf 转回 JSON。写个小程序验证:JSON {"name": "world"} → protobuf HelloRequest → JSON,确认数据一致。
Day 5:学 gRPC 反射 + grpcurl
gRPC 反射让代理在运行时查询后端支持哪些方法和参数结构,不需要编译时知道 .proto 定义。在 gRPC 服务端开启反射(reflection.Register(server))。安装 grpcurl(brew install grpcurl),用它测试:grpcurl -plaintext localhost:50051 list 列出服务,grpcurl -plaintext -d '{"name": "world"}' localhost:50051 greeter.Greeter/SayHello 调用方法。
第 14 周:在代理中实现 HTTP→gRPC 转换
Day 1:学动态调用 API
学 google.golang.org/grpc 的 Invoke 方法和 google.golang.org/protobuf/types/dynamicpb 包。dynamicpb 允许在不 import 生成代码的情况下,根据运行时获取的服务描述符动态构造 protobuf 消息。这是通用 gRPC 代理的关键。
性能考量:动态反射转换(JSON ↔ Protobuf)虽然通用,但每次请求都要通过反射查询服务描述符,性能开销很大。可以加一个 LRU Cache(用
github.com/hashicorp/golang-lru或自己用sync.Map+ 链表实现),缓存已经解析过的服务描述符和方法描述符。第一次请求某个方法时走反射,之后直接从缓存取,减少反射频率。
Day 2:在代理中加 gRPC 路由
加一个新路由规则 /grpc/<service>/<method>。当请求匹配时:读取 JSON Body → 通过反射获取目标方法的描述符 → 用 protojson.Unmarshal 转成动态 protobuf 消息 → 用 grpc.Invoke 发送到后端 → 把 protobuf 响应用 protojson.Marshal 转回 JSON → 返回给客户端。
Day 3:处理 gRPC 错误码映射
gRPC 有自己的错误码(NotFound、Internal、Unavailable 等),需要映射到 HTTP 状态码:NotFound → 404,Internal → 500,Unavailable → 503。学 google.golang.org/grpc/status 包提取错误码。
Day 4:端到端测试
启动 gRPC 后端(开启反射),启动代理。用 curl -X POST -d '{"name": "world"}' localhost:8080/grpc/greeter.Greeter/SayHello 发送 HTTP+JSON 请求,确认代理翻译成 gRPC 发给后端,再翻译回 JSON 返回。客户端全程只用了 HTTP 和 JSON。
Day 5:最终收尾
验证 gRPC 路由和 HTTP 路由共存:/grpc/... 走 gRPC 转发,其他路径走 HTTP 转发。确认中间件链(限流、鉴权、Prometheus)对两种路由都生效。整理代码,更新 README。
14 周总览
1 | 阶段一:隐身斗篷(透明代理 / 协议转换)— 4 周 |
推荐资源
- A Tour of Go:语法速通,第 1 周前三天过完。
- Go by Example:每个语法点一个可运行的例子,当字典查。
- 《Go 语言圣经》:系统学习,重点看 struct、interface、goroutine、channel 章节。
- 直接写代理项目:边做边学,遇到不会的语法再查,这是最快的路径。
一句话
14 周后你手里会有一个能伪装 Host、能卸载 HTTPS、能转发 WebSocket 和 gRPC、能鉴权、能过滤、能路由、能负载均衡、能限流、能熔断、能出 Prometheus 指标、能全量记录日志、能镜像流量的 Go 代理——装进你封装的 Linux 发行版里,就是一套完整的自动化运维体系。





