用 Go 实现 RESTful 架构:《Build Web Application with Golang》第 8.3 节精读与实践

原创2026-10-05 21:38:001,826 阅读
文章标签:文档教程

用 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 的三个要点:

  1. 每一个 URI 都代表一种资源;
  2. 客户端与服务器之间存在一层用于传输资源的"表征层";
  3. 客户端通过上述四种 HTTP 方法实现"表现层状态转移",从而对远端资源进行操作。

REST 的两大核心约束:无状态与分层

实现 REST 的 Web 应用最重要的原则是客户端与服务器之间的交互必须无状态——每一个请求都应封装全部所需信息。这意味着:

  • 服务器可以在不通知客户端的情况下随时重启;
  • 同一服务的任意一台服务器都可以响应请求,这一特性非常适合云计算场景下的水平扩展;
  • 由于无状态,客户端可以对数据进行缓存,从而显著提升性能。

REST 的另一条重要原则是系统分层(delamination):某一层中的组件无法直接与其它层的组件交互。这可以限制系统的复杂度,并促进底层组件的独立演进。下图展示了 REST 架构在落地实现时的分层结构:

REST 架构的分层实现:API Front-End 接收 API Request 并返回 API Response,其下依次是 Business Logic 与 Data 层,右侧注释对应 Representation、State/Transfer、Resource 三大概念

当 RESTful 约束得到严格遵循时,Web 应用能够扩展到支撑海量客户端;同时,REST 架构还有助于减少客户端与服务器之间的延迟、简化系统架构、提升子系统端点的可观测性。下图示意了 REST 服务的扩展能力——通过负载均衡器将请求分发到多个同构的 API Front-End 实例,再经 Service Coordinator 协调到多个 Business Logic 实例,最终统一对接数据层:

REST 的水平扩展能力:Front Load Balancer 将 API 请求分发到多个 API Front-End 实例,Service Coordinator 再将业务逻辑分发到多个 Business Logic 实例

RESTful 实现的现实约束与成熟度分层

Go 语言本身并没有对 REST 提供直接支持,但由于 RESTful Web 应用全部基于 HTTP,因此完全可以借助标准库 net/http 自行实现——前提是先对默认路由行为做一些改造。

REST 要求针对同一资源的不同交互方式使用不同的 HTTP 方法。当前许多自称 RESTful 的应用其实并未真正实现 REST,可以依据它们实际支持的 HTTP 方法,将其划分为不同的成熟度等级:

REST 成熟度分层:Level 0 仅要求支持 GET,Level 1 要求支持 GET 与 POST,Level 2 要求支持 GET、POST、PUT、DELETE、PATCH 全部方法

上图展示了当前实际应用中最常见的三个等级。需要注意的是,开发自己的应用时并不一定要遵循全部 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)。
登录后查看全文
build-web-application-with-golang