- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
REST(REpresentational State Transfer)是当下互联网上最流行的软件架构风格,它以资源为中心、充分复用 HTTP 协议,并以无状态与系统分层为设计基石。本篇技术指南以《build-web-application-with-golang》电子书第 8.3 节(en/08.3.md)为核心,先讲透 REST 的三大核心概念与架构约束,再通过完整的 Go 代码示例演示如何借助net/http与第三方路由库github.com/julienschmidt/httprouter,为同一资源的不同 HTTP 方法(GET/POST/PUT/DELETE)绑定不同处理逻辑,从而构建真正 RESTful 的 Web 应用。读完本文,你将掌握 REST 架构的判定标准、REST 成熟度分级,以及一套可直接复制运行的 Go REST 接口实现方案。
REST 是什么:三个必须理解的核心概念
REST 概念首次出现于 2000 年 Roy Thomas Fielding 的博士论文中——Fielding 同时也是 HTTP 协议规范的主要编写者之一。REST 指的是一组架构约束条件和原则,任何满足这些约束条件和原则的应用程序或设计都可以被称为 RESTful。要真正理解 REST,需要先理解以下三个概念。
资源(Resources)
REST 是"表现层状态转化"的缩写,它其实省略了主语——"表现层"指的是"资源"的表现层。那么什么是资源?我们平时上网访问的一张图片、一个文档、一个视频等,都是资源的例子。这些资源通过 URI 来定位,也就是说:每一个 URI 就代表一种资源。URI 是资源的唯一标识符,也是 REST 架构中"以资源为中心"这一理念的直接体现。
表现层(Representation)
资源是一个具体的实体信息,它可以有多种展现方式,而把实体展现出来就是表现层。例如,一个 TXT 文本信息可以输出为 HTML、JSON、XML 等格式;一张图片可以用 JPG、PNG 等方式展现——这就是"表现层"的含义。
URI 确定了一个资源,但如何确定它的具体表现形式呢?答案在 HTTP 请求的头信息中:Accept和Content-Type字段用于描述表现层。Accept声明客户端期望接收的格式(如application/json),Content-Type声明请求体实际使用的媒体类型,这两个字段共同决定了资源在客户端与服务器之间以何种"表现"进行传递。
状态转化(State Transfer)
访问一个网站,就代表了客户端和服务器之间的一次互动过程。在这个过程中,必然涉及数据和状态的变化。而 HTTP 协议本身是无状态的——这意味着这些状态必须保存在服务器端;当客户端想要通知服务器端改变数据和状态时,必须通过某种方式来告知它。
客户端能通知服务器端的手段只能是 HTTP 协议,具体来说,是 HTTP 协议中四个表示操作方式的动词,它们分别对应四种基本操作:
| HTTP 方法 | 操作语义 |
|---|---|
| GET | 获取资源 |
| POST | 新建资源(也可以用于更新资源) |
| PUT | 更新资源 |
| DELETE | 删除资源 |
综合总结:RESTful 架构的三条判据
综合上面的解释,可以总结出 RESTful 架构的完整定义:
- 每一个 URI 代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个 HTTP 动词,对服务器端资源进行操作,实现"表现层状态转化"。
REST 的两大架构原则:无状态与系统分层
一个 Web 应用要满足 REST,最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的。也就是说,从客户端到服务器的每个请求都必须包含理解该请求所必需的全部信息。这一原则带来三个直接收益:
- 如果服务器在请求之间的任何时间点重启,客户端不会得到通知,也无需关心——因为每个请求都是自包含的;
- 同一个请求可以由任何一台可用的服务器来应答,这非常适合云计算之类的多副本环境;
- 因为无状态,客户端可以缓存数据来改进性能。
另一个重要的 REST 原则是系统分层(delamination)。它表示组件无法了解除了与它直接交互的层次以外的其他组件。通过将系统知识限制在单个层内,可以限制整个系统的复杂性,从而促进底层组件的独立性。下图即是书中给出的 REST 架构图(en/images/8.3.rest2.png):
当 REST 的约束条件作为一个整体应用时,将生成一个可以扩展到大量客户端的应用程序,同时还能降低客户端与服务器之间的交互延迟,统一接口简化了整个系统的架构,改进子系统之间交互的可见性。下图展示了 REST 的扩展性——通过负载均衡器、并行 API 前端节点与服务协调器的分层设计实现水平扩展(en/images/8.3.rest.png):
RESTful 的实现:Go 中如何落地
Go 并没有为 REST 提供直接的语法支持,但由于 RESTful 应用本质上是基于 HTTP 协议实现的,因此可以利用标准库net/http包自己实现,当然需要针对 REST 做一定的改造:REST 的核心要求是根据不同的 HTTP 方法(method)来处理相应的资源,即"同一资源、不同方法、不同逻辑"。
REST 成熟度的三个 level
目前已经存在很多自称是 REST 的应用,其实并没有真正实现 REST。书中将这些应用根据所实现的 HTTP 方法分为几个级别,如下图所示(en/images/8.3.rest3.png):
上图展示了目前实现 REST 的三个 level:从仅实现 GET,到实现 GET 与 POST,再到完整覆盖 GET、POST、PUT、DELETE(乃至 PATCH)。需要说明的是,在应用开发时并不一定需要完全按照 RESTful 的规则实现全部方式,因为有些时候完全 RESTful 的方式未必可行——RESTful 服务要求充分利用每一个 HTTP 方法(包括DELETE和PUT),但现实中有两类典型限制:
- HTML 标准的限制:HTML 标准只能通过链接和表单支持
GET和POST。在没有 Ajax 支持的网页浏览器中,无法发出PUT或DELETE命令; - 防火墙的限制:有些防火墙会拦截 HTTP
PUT和DELETE请求。要绕过这个限制,客户端需要把实际的PUT和DELETE请求通过 POST 请求穿透过来,而 RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。
一种常用的折中方案是:在 POST 请求中增加一个隐藏字段_method来模拟PUT、DELETE等方式,但服务器端需要对它做转换。不过,在 Go 语言中完全按照 RESTful 规范来实现其实是很容易的,下面通过完整的例子来说明如何实现 RESTful 的应用设计。
基于 httprouter 的 REST 示例:完整代码
书中给出的示例代码如下(原文档见 en/08.3.md):
package main import ( "fmt" "log" "net/http" "github.com/julienschmidt/httprouter" ) 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)) }代码逐层拆解:从路由注册到方法分发
这段示例演示了如何编写一个最基本的 REST 应用。我们访问的资源是"用户"(user),针对同一资源的不同操作,通过不同的 HTTP 方法映射到不同的处理函数:
router.GET("/user/:uid", getuser)——获取指定 uid 的用户;router.POST("/adduser/:uid", adduser)——新增用户;router.DELETE("/deluser/:uid", deleteuser)——删除用户;router.PUT("/moduser/:uid", modifyuser)——修改用户。
其中:uid是路径参数(动态路由),在处理器中通过ps.ByName("uid")取出具体的参数值——这正是 REST "每个 URI 代表一种资源" 在实现层面的落地:/user/:uid这类带参数的路由模式,让同一个资源模板可以覆盖任意具体的资源实例。
代码中使用了第三方库github.com/julienschmidt/httprouter。在本书前面的章节(如第 13.2 节 自定义路由器)介绍过如何实现自定义的路由器,而httprouter恰恰实现了自定义路由和方便的路由规则映射——它的核心价值在于:将 HTTP 方法(GET/POST/PUT/DELETE)与 URL 路径绑定到特定的处理器函数,这正是实现 RESTful 架构最关键的"方法分发"能力。相比之下,Go 标准库默认的DefaultServeMux(即http.Handle/http.HandleFunc注册的默认路由器,见 en/03.4.md)存在两个与 REST 直接相关的局限:
- 不支持带参数的动态路由,如
the/user/:UID这类模式; - 对 REST 的支持不够好——访问方法无法被限制,例如用户访问
/foo时,GET、POST、DELETE、HEAD 等 HTTP 方法都可能被接受。
而httprouter通过方法化的路由注册(router.GET(...)、router.POST(...)、router.PUT(...)、router.DELETE(...))彻底解决了这两个问题:同一个路径模板可以按不同方法注册不同的处理器,未注册的方法将不会被路由,这也正是 REST 要求"对同一资源的不同方法实现不同逻辑"的天然映射。
运行与验证
将上述代码保存为main.go,通过go get github.com/julienschmidt/httprouter获取依赖后运行go run main.go,服务会监听:8080端口。此时可以用curl直接验证 REST 接口:
# 获取资源(GET) curl http://localhost:8080/user/123 # 输出: you are get user 123 # 新建资源(POST) curl -X POST http://localhost:8080/adduser/456 # 输出: you are add user 456 # 更新资源(PUT) curl -X PUT http://localhost:8080/moduser/789 # 输出: you are modify user 789 # 删除资源(DELETE) curl -X DELETE http://localhost:8080/deluser/321 # 输出: you are delete user 321这个例子也印证了 REST 的本质:REST 就是根据不同的 method 访问同一个资源的时候实现不同的逻辑处理。实际工程中,处理器内部通常会进一步调用数据库层完成真实的增删改查(可参考本书第 5 章 数据库 的相关实现),并基于Accept/Content-Type头协商 JSON 或 XML 等表现层格式(第 7 章 JSON 与 XML 提供了 Go 侧的处理细节)。
总结
REST 是一种架构风格,它汲取了 WWW 的成功经验:无状态、以资源为中心、充分利用 HTTP 协议和 URI 协议、提供统一的接口定义,这使得它作为一种 Web 服务设计方法而广泛流行。在某种意义上,通过强调 URI 和 HTTP 等早期 Internet 标准,REST 是对大型应用程序服务器时代之前 Web 方式的回归,并为大型、可扩展的 Web 应用铺平了道路。
目前 Go 对 REST 的支持还比较简单直接:通过实现自定义的路由规则,为不同的 method 实现不同的 handler,即可实现 REST 的架构。无论是使用httprouter这类第三方库,还是像本书第 13 章那样从零构建自己的路由与控制器,核心思路始终一致——以 URI 标识资源、以 HTTP 方法表达操作、以无状态设计支撑水平扩展。
延伸阅读
- 本章目录:第 8 章 Web 服务
- 上一节:WebSocket
- 下一节:RPC
- 全书目录:preface.md
- 相关主题:自定义路由器实现 13.2 自定义路由
- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
相关推荐
HttpRouter 源码级实战指南:基于压缩基数树的高性能 Go HTTP 请求路由器
HttpRouter 源码级实战指南:基于压缩基数树的高性能 Go HTTP 请求路由器 导读 HttpRouter 是一个轻量级、高性能的 Go HTTP 请
后端深入解析 Kiota HTTP Library for Go:基于 net/http 的请求适配器与中间件管道实战指南
深入解析 Kiota HTTP Library for Go:基于 net/http 的请求适配器与中间件管道实战指南 本指南围绕 OpenShift 测试套件
测试云原生质量保障OpenCloud 中的 Go HTTP/2 实现解析:golang.org/x/net/http2 的双实现架构与 Go 1.27 标准库迁移
OpenCloud 中的 Go HTTP/2 实现解析:golang.org/x/net/http2 的双实现架构与 Go 1.27 标准库迁移 导读 本篇文章
后端微服务存储认证鉴权
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考