用 Go 实现 RESTful 架构:《Build Web Application with Golang》第 8.3 节精读与实践
用 Go 实现 RESTful 架构:《Build Web Application with Golang》第 8.3 节精读与实践
本篇技术指南围绕开源电子书《Build Web Application with Golang》第 8.3 节(th/08.3.md)展开,系统讲解 REST(表征状态转移)架构的三大核心概念——资源、表征与状态转移,并给出基于 Go 标准库 net/http 与第三方路由库 httprouter 的完整可运行示例。读完本文,你将掌握 REST 的成熟度分层、无状态与分层约束的内涵,以及如何为同一资源的不同 HTTP 方法(GET/POST/PUT/DELETE)编写独立处理器,从而在自己的 Go Web 应用中落地标准 RESTful 接口。
REST 是什么:从 Fielding 博士论文说起
REST(REpresentational State Transfer,表征状态转移)这一概念最早于 2000 年由 Roy Thomas Fielding 在其博士论文中正式提出——他同时也是 HTTP 协议的联合创始人之一。REST 并非一项具体技术,而是一组架构约束与原则的集合:任何遵循这套约束实现的系统,都可以被称为 RESTful 系统。
在理解 REST 之前,需要先厘清三个基础概念:
- 资源(Resources):图片、文档、视频等一切可以被 URI 定位的信息实体,都是资源。REST 本质上是"表现层状态转移",这里的表现层指的就是资源的表征层。
- 表征(Representation):资源是具体的信息实体,但在表现层中可以以多种形态呈现。例如一份 TXT 文档可以被呈现为 HTML、JSON、XML 等格式,一张图片可以呈现为 jpg、png 等格式。URI 用于唯一标识资源,而具体以哪种形态呈现,则由 HTTP 请求头中的
Accept与Content-Type两个字段共同决定。 - 状态转移(State Transfer):每次访问网站页面,客户端与服务器之间都会启动一次交互过程,过程中与当前页面状态相关的数据需要被保存。但 HTTP 本身是无状态协议,因此必须由服务器端保存客户端状态;当客户端修改了某些数据并希望持久化变更时,就必须有一种方式把新状态告知服务器。绝大多数情况下,客户端借助 HTTP 的四种操作方法来完成状态变更:
GET—— 获取资源;POST—— 创建或更新资源;PUT—— 更新资源;DELETE—— 删除资源。
综合以上三点,可以归纳出 REST 的三个要点:
- 每一个 URI 都代表一种资源;
- 客户端与服务器之间存在一层用于传输资源的"表征层";
- 客户端通过上述四种 HTTP 方法实现"表现层状态转移",从而对远端资源进行操作。
REST 的两大核心约束:无状态与分层
实现 REST 的 Web 应用最重要的原则是客户端与服务器之间的交互必须无状态——每一个请求都应封装全部所需信息。这意味着:
- 服务器可以在不通知客户端的情况下随时重启;
- 同一服务的任意一台服务器都可以响应请求,这一特性非常适合云计算场景下的水平扩展;
- 由于无状态,客户端可以对数据进行缓存,从而显著提升性能。
REST 的另一条重要原则是系统分层(delamination):某一层中的组件无法直接与其它层的组件交互。这可以限制系统的复杂度,并促进底层组件的独立演进。下图展示了 REST 架构在落地实现时的分层结构:
当 RESTful 约束得到严格遵循时,Web 应用能够扩展到支撑海量客户端;同时,REST 架构还有助于减少客户端与服务器之间的延迟、简化系统架构、提升子系统端点的可观测性。下图示意了 REST 服务的扩展能力——通过负载均衡器将请求分发到多个同构的 API Front-End 实例,再经 Service Coordinator 协调到多个 Business Logic 实例,最终统一对接数据层:
RESTful 实现的现实约束与成熟度分层
Go 语言本身并没有对 REST 提供直接支持,但由于 RESTful Web 应用全部基于 HTTP,因此完全可以借助标准库 net/http 自行实现——前提是先对默认路由行为做一些改造。
REST 要求针对同一资源的不同交互方式使用不同的 HTTP 方法。当前许多自称 RESTful 的应用其实并未真正实现 REST,可以依据它们实际支持的 HTTP 方法,将其划分为不同的成熟度等级:
上图展示了当前实际应用中最常见的三个等级。需要注意的是,开发自己的应用时并不一定要遵循全部 REST 规则与约束——因为某些规则并非在所有场景下都适用。RESTful Web 应用会使用包括 DELETE 与 PUT 在内的每一个 HTTP 方法,但现实中有两个常见的阻碍:
- HTML 标准只允许客户端通过链接和表单发送
GET与POST请求,没有 AJAX 支持就无法直接发送PUT或DELETE请求; - 一些防火墙会拦截
PUT和DELETE请求,导致客户端只能改用POST来间接实现。
针对上述情况,一种成熟的折中方案是:在 POST 请求中增加一个隐藏的 _method 字段来模拟 PUT 和 DELETE,再由服务器端在处理前将这些请求转换回原始 HTTP 方法。这也是许多生产级应用采用的工作流程。
基于 httprouter 的 RESTful 接口实现
Go 标准库的 DefaultServeMux 仅按 URL 路径匹配,无法区分同一路径下不同 HTTP 方法的分发;在 th/03.4.md 中已经讲解了如何自定义实现 Handler 接口的路由器——http.ListenAndServe 的第二个参数正是一个 Handler 接口,任何实现了 ServeHTTP(ResponseWriter, *Request) 的路由器都可以被直接传入。本节则引入第三方路由库 github.com/julienschmidt/httprouter,它提供了按方法 + 路径参数(如 :uid)映射处理器的便捷路由规则,非常适合实现 RESTful 架构。
下面的示例展示了如何编写一个最基本的 REST 应用:资源为"用户",针对用户的查看、添加、删除、修改分别注册不同方法对应的处理器:
package main
import (
"fmt"
"github.com/julienschmidt/httprouter"
"log"
"net/http"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}
func getuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are get user %s", uid)
}
func modifyuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are modify user %s", uid)
}
func deleteuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are delete user %s", uid)
}
func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// uid := r.FormValue("uid")
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are add user %s", uid)
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
router.GET("/user/:uid", getuser)
router.POST("/adduser/:uid", adduser)
router.DELETE("/deluser/:uid", deleteuser)
router.PUT("/moduser/:uid", modifyuser)
log.Fatal(http.ListenAndServe(":8080", router))
}
代码要点解读
- 处理器签名:
httprouter的处理器统一为func(w http.ResponseWriter, r *http.Request, ps httprouter.Params),第三个参数ps携带路径参数,可通过ps.ByName("uid")取出路由中:uid段对应的实际值。 - 方法级路由:
router.GET、router.POST、router.DELETE、router.PUT分别把同一资源的不同操作绑定到不同函数,这正是 REST"对同一资源的不同 HTTP 方法实现不同逻辑"的直接体现。 - 启动服务:
http.ListenAndServe(":8080", router)将自定义路由器作为第二个参数传入,替代默认的DefaultServeMux。
运行该程序后,向 http://localhost:8080/user/123 发起 GET 请求会返回 you are get user 123,向 /moduser/123 发起 PUT 请求会返回 you are modify user 123,向 /deluser/123 发起 DELETE 请求会返回 you are delete user 123,向 /adduser/123 发起 POST 请求则会返回 you are add user 123——同一资源"用户",通过四种方法完成了查询、新增、修改、删除四类操作,这就是 RESTful 接口的典型形态。代码中注释掉的 r.FormValue("uid") 提示我们:除路径参数外,也可以选择从表单中取值,具体取决于接口设计。
小结
REST 是一种建立在 WWW 成功经验之上的 Web 架构风格:无状态、以资源为中心、充分利用 HTTP 与 URI 协议、提供统一接口。这些优秀的设计考量使其成为当下最流行的 Web 服务标准。从某种意义上说,通过强调 URI 并复用 HTTP 这类早期互联网标准,REST 为大型、可扩展的 Web 应用铺平了道路。目前 Go 对 REST 的支持仍然比较基础,但正如本文所示,通过自定义路由规则、为每种 HTTP 请求类型实现不同的处理器,我们完全可以在 Go Web 应用中达成 RESTful 架构。
延伸阅读
- 本书第 8 章导览(th/08.0.md):REST 与 SOAP 两大 Web 服务风格的对比,以及 Socket、WebSocket、RPC 在本章中的编排;
- 自定义路由器的底层原理(th/03.4.md):
ServeMux、Handler接口与HandlerFunc的工作机制; - 上一节 WebSocket(th/08.2.md)与下一节 RPC(th/08.4.md):从 REST 的 HTTP 风格接口延伸到实时通信与远程过程调用;
- 本书目录(th/preface.md)。


