Go RESTful
在 RESTful API 我介绍了 RESTful 架构的约束条件与最佳实践,但还没有实际上手构建过一个 RESTful 的架构服务。本文将基于 go-restful 这一轻量级框架,介绍在 Go 语言中如何实现 RESTful API,本文中所有代码参考自go-restful examples,可在我的 Github 中找到。
go-restful 并不是 Go 语言中唯一的 RESTful API 框架,beego 、gin 都属于这一范畴,go-restful 的一大优点在于其轻量性,k8s apisever 中也使用了 go-restful 框架。在深入了解 go-restful 之前,结合 RESTful API 中讨论的 REST 架构的基本原则,我们先提出一个问题,Go 语言不是已经给我们提供了原生的 net/http 包吗?我们为什么还需要一个独立的 RESTful API 框架呢, RESTful API 框架需要实现什么?
一个 RESTful API 框架应该具备以下几个元素:
- Resources:资源的定义,即
HTTP URI的定义,RESTful API 的设计围绕着 Resource 进行建模。 - Handlers:资源处理器,是资源业务逻辑处理的具体实现。
- Request Routers:资源请求路由器,完成
HTTP URIs、HTTP Request Methods和Handlers三者之间的映射与路由。 - Request Verification Schemas:
HTTP Request Body校验器,验证请求实体的合法性。 - Response View Builder:
HTTP Response Body生成器,生成合法的响应实体。 - Controllers:资源表现层状态转移控制器,每个 Resource 都有着各自的 Controller,将 Resource 自身及其所拥有的
Handlers、Request Verification Schemas以及Response View Builder进行封装,配合Request Routers完成RESTful请求的处理即响应。
基本概念
Route
Route 表示一条请求路由记录,即:Resource 的 URL Path(URI),从编程的角度可细分为 RootPath 和 SubPath。Route 包含了 Resource 的 URL Path、HTTP Method、Handler 三者之间的组合映射关系。
|
|
go-restful 内置的 RouteSelector 根据 Route 将客户端发出的 HTTP 请求路由到相应的 Handler 进行处理,Handler 具体也就是 Route 数据结构中的 Function,这个函数包含了用户的请求与返回给用户的响应。
|
|
WebService
一个 WebService 由若干个 Routes 组成,并且 WebService 内的 Routes 拥有同一个 RootPath、输入输出格式、基本一致的请求数据类型等等一系列的通用属性。通常的,我们会根据需要将一组相关性非常强的 API 封装成为一个 WebServiice,继而将 Web Application 所拥有的全部 APIs 划分若干个 Group。
|
|
Root Path
WebService 有一个 Root Path,通过 ws.Path() 方法设置,例如:/users,作为 Group 的 根。
|
|
Path 的具体实现就是设置了 rootPath 字段,而上面的 Consumes 和 Produces 则设置了 WebService 所能接收和返回的 MIME 类型,你也可以对每个 Route 单独设置。
|
|
Group 下属的 APIs 都是 RootRoute(RootPath)下属的 SubRoute(SubPath)。每个 Group 就是提供一项服务的 API 集合,每个 Group 会维护一个 Version。Group 的抽象是为了能够安全隔离的对各项服务进行敏捷迭代,当我们对一项服务进行升级时,只需要通过对特定版本号的更新来升级相关的 APIs,而不会影响到整个 Web Server。视实际情况而定,可能是若干个 APIs 分为一个 Group,也有可能一个 API 就是一个 Group。
RouteBuilder
Route 包含了 Resource 的 URL Path、HTTP Method、Handler 三者之间的组合映射关系,为了在 WebService 能够注册到这种映射关系,用户需要调用 Route() 函数,这里的 hello 则是一个典型的 RouteFunction,其参数为 Request 和 Response。
|
|
我们看看 Route() 函数的具体实现,其参数是 RouteBuilder 这种数据结构:
|
|
下面是 RouteBuilder 的数据结构,很像 Route 的数据结构,主要是为了便于收集 Route 需要用到的各种信息。
|
|
比如首先通过 GET 注册了请求的路径,因为 RouteBuilder 的函数返回都是 RouteBuilder ,所以可以链式调用下去。在后面的 To 就指定了路由的 Handler 函数。
|
|
经过这种链式调用之后,就通过 builder.Build() 函数构建了 Route 对象:
|
|
Example
至此,结合上面的内容,我们就已经可以借助 go-restful 框架实现一个最简单的 Hello World 了:
|
|
上面的代码比较简单,包含一个 hello 的 Handler,通过 ws.Route(ws.GET("/hello").To(hello)) 将其注册到 WebService,然后启动了一个 WebServer,就可以了通过 GET 方法访问了,如下所示:
Container
上一小节虽然 Work 了,但是还是有一个问题,我们通过 Go 自带的 net/http 包启动的 WebServer 是如何和我们定义的 WebService 联系起来的呢?在解答这个问题之前,我们首先来了解下 Container。
Container 表示一个 Web Server,由多个 WebServices 组成,此外还包含了若干个 Filters、一个 http.ServeMux 多路复用器以及一个 dispatch。
|
|
ServeMux
我们看到 Container 有一个 ServeMux,这就是利用 net/http 的标准多路复用器,它会将不同的请求路径注册上对应的 Handler:
|
|
而 Container 会在 addHandler函数中,将不同的路径都分发到它自己的 dispatch 函数:
|
|
那么 addHandler 是在哪里被调用的呢?这就是我们的 Add 函数
|
|
因此,如果我们创建了自己的 Container 的话,需要将 WebService 关联到这个 Container 才能生效:
|
|
因为这里的 Container 实现了 ServeHTTP 接口,所以就可以直接作为一个Handler传递给 http.Server:
|
|
DefaultContainer
还是回到 Hello World 的示例,我们并没有发现 Container 的创建啊,那是如何实现关联的呢?这就借助来 net/http 的 DefaultServeMux 和 go-restful 的 DefaultContainer:
|
|
可以看到,在 go-restful包初始化的时候,默认就会创建一个 DefaultContainer,并且将它的 ServeMux 设置为了 http.DefaultServeMux。我们通过 restful.Add(ws) 给 DefaultServeMux 注册了 WebService,也就是把 dispatch 函数注册给了 WebServer,这样就可以使用 http.DefaultServeMux 的机制调用它们了。
|
|
Dispatch
dispatch 是整个框架最关键的函数了,它作为 Container 这个 WebServer 的入口,通过 SelectRouter 将路由分发给各个 WebService,再由 WebService 分发给具体的 Handler 函数。找到对应的 WebService 和 Route 之后,就可以运行 Filters 和把 Route 的 Function 作为 Handler 了。关于 SelectRouter 的实现,将会在后面的 路由分发 介绍。
|
|
Example
经过上面的讲解,我们现在可以实现一个稍微复杂一点的 RESTful API 了:
|
|
编译运行上面这个程序,发出请求如下:
|
|
过滤器
过滤器可以动态拦截请求和响应,以及转换或使用请求和响应中包含的信息。用户可以使用过滤器来执行常规的日志记录、测量、验证、重定向、设置响应头部Header等。restful包中有三个针对请求、响应流的钩子,还可以添加过滤器。每个过滤器必须定义一个FilterFunction:go-restful 支持服务级、路由级的请求或响应过滤。开发者可以使用 Filter 来执行常规的日志记录、计量、验证、重定向、设置响应头部等工作。go-restful 提供了 3 个针对请求、响应的钩子(Hooks),此外,还可以实现自定义的 Filter。
|
|
使用如下语句传递请求/响应对到下一个过滤器或RouteFunction:
|
|
|
|
Container Filter
在注册 WebService 之前处理
|
|
在 Container 的数据结构中,我们看到了有 containerFilters 这样一个 FilterFunction 切片,上面调用的 Filter 函数是实际上就是将对应的 FulterFunction 加入到这个切片中:
|
|
Container 调用 Filter 函数就是通过 FilterChain 的 ProcessFilter 来实现的:
|
|
所有的 Filter 执行完,就去执行 Target Route 的 Function,也就是用户注册的 handler。
WebService Filter
路由 WebService 之前处理
|
|
在 WebService 的数据结构中,我们看到了有 filters 这个 FilterFunction 切片,上面调用的 Filter 函数是实际上就是将对应的 FulterFunction 加入到这个切片中:
|
|
而对于 WebService 的 Filter 函数调用,就是在 Container 的 dispatch 中实现的,见上面的代码。
Route Filter
在调用 Router 相关的函数之前处理。
|
|
Route 的 Filter 安装是通过 RouterBuilder 来实现的,之后会在 Build() 函数中将 filters 传递给 Route
|
|
对于 Route 的 Filter 函数调用,也是在 Container 的 dispatch 中实现的,见 Container 的代码。
路由分发
go-restful 支持两种路由分发器:快速路由 CurlyRouter 和 RouterJSR311。实际上,CurlyRoute 也是基于 RouterJSR311 的,相比 RouterJSR11,还支持了正则表达式和动态参数,也更加轻量级,Kubernetes ApiServer 中使用的就是这种路由。
CurlyRouter 的元素包括:请求路径(URL Path),请求参数(Parameter),输入、输出类型(Writes/Reads Model),处理函数(Handler),响应内容类型(Accept)等。
Response Encoding
如果 HTTP Request 包含了 Accept-Encoding Header,那么 HTTP Response 就必须使用指定的编码格式进行压缩。go-restful 目前支持 gzip 、deflate 这两种响应编码格式。
如果要为所有的响应启用它们:
|
|
同时,也可以通过创建一个 Filter 来实现自定义的响应编码过滤器,并将其安装到每一个 WebService 和 Route 上。
OPTIONS支持
通过安装预定义的容器过滤器,你的 WebService 可以响应 HTTP OPTIONS 请求。
|
|
CORS
通过安装 CrossOriginResourceSharing 过滤器,使 WebService 可以响应 CORS 请求。
|
|
异常处理
意想不到的事情发生。如果因为故障而不能处理请求,服务端需要通过响应告诉客户端发生了什么和为什么。因此使用HTTP状态码,更重要的是要正确的使用状态码。
- 400: Bad Request
如果路径或查询参数无效(内容或类型),那么使用http.StatusBadRequest。
- 404: Not Found
尽管URI有效,但请求的资源可能不可用。
- 500: Internal Server Error
如果应用程序逻辑无法处理请求(或编写响应),则使用http.StatusInternalServerError。
- 405: Method Not Allowed
请求的URL是有效的,但请求使用的HTTP方法(GET,PUT,POST,…)是不允许的。
- 406: Not Acceptable
请求的头部没有或设置了未知Accept Header。
- 415: Unsupported Media Type
请求的头部没有或设置了未知的Content-Type报头。
ServiceError
除了设置HTTP状态码,还应该为响应选择写适当的ServiceError消息。
Performance Options
这个包有几个选项,它们可能会影响服务的性能。重要的是要理解这些选项,正确地设置它们。
restful.DefaultContainer.DoNotRecover(false)
DoNotRecover控制是否因返回HTTP 500状态码而(恐慌)停止服务。如果设置为false,那么容器Container会恢复服务。默认值为true。
|
|
如果启用了内容编码,那么获得新gzip/zlib输出器(writer)和读入器(reader)的默认策略是使用sync.Pool。由于输出器writer是昂贵的结构,当使用预加载缓存时性能提高非常明显。你也可以注入自己的实现。
Trouble shooting
这个包可以对完整的Http请求的匹配过程和过滤器调用产生详细的日志记录。启用此功能需要你设置 restful.StdLogger的实现,例如 log.Logger:
|
|
参考资料
-
No backlinks found.