API网关一直是基础设施里的黑箱。你知道它管认证、限流、路由,但内部怎么运转,大多数人只能猜。一位开发者决定亲手拆掉这个黑箱——用Go的标准库net/http(网络/超文本传输协议),从零写了一个网关。没有框架,247行代码,全在GitHub上公开。
这个项目叫simple-api-gateway。作者的核心洞察很直白:API网关本质上就是一个可配置的http.Handler(超文本传输协议处理器)。认证、限流、链路追踪,全是包在反向代理外面的中间件。堆够多,就是网关。
中间件链:俄罗斯套娃式编程
Go的中间件模式长这样:
type Middleware func(http.Handler) http.Handler
一个中间件接收一个处理器,返回一个新处理器,在新处理器里干点前置或后置的事。这种设计让测试变得极其简单——不需要起真服务器,直接调ServeHTTP(服务超文本传输协议)方法就能跑单元测试。
Gateway结构体直接实现了http.Handler接口:
type Gateway struct { config *types.Config router *Router middleware map[string]Middleware }
ServeHTTP方法里只做三件事:匹配路由、组装中间件链、执行。没有魔法,纯结构体加方法。作者特意强调这一点,因为市面上太多网关把简单问题复杂化,搞得开发者以为必须上Kubernetes(容器编排平台)才能玩。
路由设计:精确度优先的暴力匹配
路由器支持三种路径模式:精确匹配(/users)、参数化(/users/:id)、前缀通配(/users/*)。启动时按精确度排序,确保更具体的规则永远优先。
匹配逻辑是遍历:
func (ro *Router) Match(r *http.Request) (*types.Route, map[string]string) { for _, route := range ro.routes { // sorted: exact > param > prefix if matched, params := matchPath(route.Path, r.URL.Path); matched { if methodAllowed(route.Methods, r.Method) { return &route, params } } } return nil, nil }
作者承认这里有个故意留下的缺口::param片段能匹配并用于路由,但提取出的参数值不会注入请求上下文。对学习网关模式来说,这个取舍可以接受,但生产环境肯定得补。
路由命中后,buildChain方法把配置的中间件组装成链。注意循环是倒着走的——这样第一个列出的中间件会成为最外层,最后列出的紧贴上游服务。这个细节很容易写反,导致中间件执行顺序和配置顺序不一致。
反向代理:标准库就够了
核心转发逻辑用了net/http/httputil里的ReverseProxy(反向代理)。作者原话是:「Go的标准库已经做了90%的工作,很多人却不知道。」
代理配置里有个细节:Director函数负责修改请求,把原始URL换成上游地址。Transport(传输层)可以自定义,比如加连接池、TLS(传输层安全)配置。作者用了默认的,但留了扩展点。
错误处理也走标准库:上游返回5xx时,ReverseProxy会自动把错误写回响应。想要自定义错误页?包一层中间件拦截状态码就行。
限流实现:令牌桶的朴素版
项目里带了个基于内存的限流中间件,用令牌桶算法。每个路由独立计数,桶大小和填充速率从配置读。
实现用了sync.Mutex(互斥锁)保护状态,简单粗暴。作者备注:「生产环境应该用Redis(远程字典服务)集群状态,但这里为了演示核心模式,单机的够了。」
令牌桶的代码不到50行,关键逻辑是每次请求先尝试消耗令牌,不够就返回429状态码(请求过多)。没有滑动窗口的精确,但实现成本极低,适合作为教学示例。
认证中间件:JWT(JSON网页令牌)的极简解析
JWT验证中间件展示了怎么在不引入第三方库的情况下做认证。作者用了golang.org/x/crypto(加密库)的子包,手动解析token结构。
流程很标准:从Authorization头提取Bearer token,base64解码header和payload,验证签名,检查exp字段是否过期。没有刷新机制,没有权限角色,只有「这token是不是你签的」和「过期没」。
作者特意没做完整的OIDC(开放ID连接)或OAuth2(开放授权第二版)集成,理由是:「网关层的认证应该只做网关层的事。权限校验交给上游服务,否则网关会变成另一个单体。」
配置即代码:YAML(YAML ain't markup language)的取舍
路由和中间件配置用YAML文件,启动时加载。作者试过用Go代码直接写配置,但发现「改个路由要重新编译」在开发期太烦人。
YAML结构分层:顶层是网关监听地址,然后是routes数组,每个route指定路径、方法、上游地址、中间件列表。中间件参数用map[string]interface{}(字符串到任意类型的映射),类型安全靠运行时检查。
作者承认这里可以做得更好:「理想状态是配置有Schema校验,启动时就把错报出来。现在只能祈祷写YAML的人别手抖。」
测试策略:表驱动覆盖全路径
测试文件比实现文件还长。作者用了表驱动测试,每个测试用例包含请求构造、期望响应、中间件状态断言。
一个典型测试长这样:
tests := []struct { name string req *http.Request wantStatus int wantBody string }{ {"exact match", newRequest("GET", "/users"), 200, "upstream"}, {"param route", newRequest("GET", "/users/123"), 200, "123"}, {"method not allowed", newRequest("POST", "/users"), 405, ""}, }
这种写法让新增测试用例成本极低,也强迫开发者把边界情况想清楚。作者跑了90%的覆盖率,没覆盖的大多是错误分支——「比如配置文件解析失败直接log.Fatal(致命日志),这种测了也没意义。」
生产差距:作者自己列了7条
README里有个诚实的章节「Not Production Ready」。作者逐条列了缺口:没有持久化配置、没有分布式限流、没有健康检查、没有优雅关闭、没有指标暴露、没有请求日志结构化、没有TLS自动证书。
最扎心的一条:「没有文档。你现在看的这个README就是全部。」
但作者认为这些缺口恰恰是价值所在:「看完代码你就知道一个网关到底要解决哪些问题。下次用Kong或Envoy(服务代理)时,你能猜出它们内部大概怎么实现,出了问题也知道往哪挖。」
项目发布后,Hacker News(黑客新闻)上有条评论被顶到最高:「我维护了三年Kong,今天才第一次真正理解plugins是怎么加载的。」
如果让你从零写一个网关,你会先实现哪个功能——路由匹配、中间件链,还是反向代理本身?
热门跟贴