用Go语言写RESTful API

    REST 是 的缩写,中文翻译是 表现层状态转换 。这种软件构建风格,通过基于 HTTP 之上的一组 约束属性 ,提供万维网网络服务。其主要特点包括:

    • 统一接口 ( Uniform Interface ),资源通过一致、可预见的 URI 及请求方法来操作;
    • 无状态 ( Stateless ),请求状态由客户端维护,服务端不做保存;
    • 可缓存 ( Cacheable ),可以通过缓存提升服务性能;
    • 分层系统 ( Layered System )

    统一接口RESTful 服务最大的特点。统一接口的核心思想是,将服务抽象成各种 资源 ,并通过一套一致、可预见的 URI 以及 请求方法 ( Request Method )来操作这些资源。这样一来,掌握了一种资源的使用方法,便可延伸到其他资源上,达到举一反三的效果。

    资源可以是 单个资源 ,也可以是同种类资源组成的集合,即 资源组 。资源、资源组以及对应的操作定义如下:

    注意到,创建资源有两种不同的方式,即:

    • 针对资源组的 POST 操作;
    • 针对资源 PUT 操作;这两种操作是有区别的,主要体现在 幂等性 上。

    针对资源组的 POST 操作,数据键(或 ID )一般由服务端分配。因此,同个请求执行两次将创建两个相同的资源。换句话讲,这个操作一般 不具有幂等性

    针对资源 PUT 操作,由于客户端显式指定数据键,幂等性是可以保证的。实际上,这个操作在资源已经存在的情况下替换原有资源,在资源还未存在的情况下创建资源。

    最后,我们还可以将 RESTful 与传统的 增删改查 一一对应起来:

    表格-2 RESTful 增删改查
    操作对象URI请求方法
    创建资源( )资源组/resourcesPOST
    删除资源( )资源/resources/142DELETE
    更新资源( )资源/resources/142PUT
    查询资源( )资源/resources/142GET
    列举资源( )资源组/resourcesGET

    为了演示 RESTful 服务接口的特性,我们特地实现了一个简单的服务—— KVS 服务。

    该服务只提供一种资源,名为 。根据资源名, kv 资源组的 URI/kvskv 资源的 URI/kvs/<key>key 是资源的键。

    进入源码目录 go/restful/go-restful/kvs ,可以看到源码文件 :

    Go 开发环境下,可直接启动 KVS 服务了:

    1. $ go run kvs.go

    服务默认监听本地 8080 端口: http://127.0.0.1:8080/

    服务启动后,资源池是空的,不信发起搜索请求看看:

    1. $ curl http://127.0.0.1:8080/kvs
    2. {
    3. "result": true,
    4. "data": [],
    5. "meta": {
    6. "total": 0,
    7. "skip": 0,
    8. "count": 0
    9. },
    10. "message": ""
    11. }

    注意到,返回的数据中, data 字段是一个空的列表。换句话讲,资源池中没有任何可用的键。

    接着,我们随便用一个键(如 something )向服务器检索数据。毫不意外,服务器返回一个错误,告诉我们资源不存在:

    1. $ curl http://127.0.0.1:8080/kvs/something
    2. {
    3. "result": false,
    4. "data": null,
    5. "meta": null,
    6. "message": "resource not exists"
    7. }

    好吧,那我们创建一个资源呗:

    1. $ curl -X POST \
    2. -H 'Content-Type: application/json' \
    3. -d '{"key": "guangzhou", "name": "广州", "population": 14498400}' \
    4. http://127.0.0.1:8080/kvs
    5. {
    6. "result": true,
    7. "data": {
    8. "key": "guangzhou",
    9. "name": "广州",
    10. "population": 14498400
    11. },
    12. "meta": null,
    13. "message": ""
    14. }

    我们再接再厉,添加杭州的数据:

    艾玛呀,人口少了个零咋整?——更新呗:

    1. $ curl -X PUT \
    2. -H 'Content-Type: application/json' \
    3. -d '{"population": 9468000}' \
    4. http://127.0.0.1:8080/kvs/hangzhou
    5. {
    6. "result": true,
    7. "data": {
    8. "key": "hangzhou",
    9. "name": "杭州",
    10. "population": 9468000
    11. },
    12. "meta": null,
    13. "message": ""
    14. }

    注意到,这里我们采用 PUT 方法,数据只包括需要修正的人口字段( population )。

    好了,现在在资源池可以查询到这两个数据记录了:

    1. $ curl http://127.0.0.1:8080/kvs
    2. {
    3. "result": true,
    4. "data": [
    5. {
    6. "key": "guangzhou",
    7. "name": "广州",
    8. "population": 14498400
    9. },
    10. {
    11. "key": "hangzhou",
    12. "name": "杭州",
    13. "population": 9468000
    14. }
    15. ],
    16. "meta": {
    17. "total": 2,
    18. "skip": 0,
    19. "count": 2
    20. },
    21. "message": ""
    22. }

    根据数据键,我们可以查询出对应的数据记录:

    1. $ curl http://localhost:8080/kvs/guangzhou
    2. {
    3. "result": true,
    4. "data": {
    5. "key": "guangzhou",
    6. "name": "广州",
    7. },
    8. "meta": null,
    9. "message": ""

    数据记录不断增长,当存储资源不足时,服务将返回一个错误:

    1. $ curl -X POST \
    2. -H 'Content-Type: application/json' \
    3. -d '{"key": "suzhou", "name": "苏州", "population": 10684000}' \
    4. http://127.0.0.1:8080/kvs
    5. {
    6. "result": false,
    7. "data": null,
    8. "meta": null,
    9. "message": "out of resources"
    10. }

    KVS 是一个非常简单的服务,对于有一些编程基础的童鞋,理解起来应该毫无难度。完整源码在这查看: 。

    先定义数据结构体,通过标签定义用于 json 序列化的字段名:

    DataItem 是存储 城市信息 的结构体;ResultMeta 是存储 结果元数据 的结构体;而 Result 则是存储 请求结果 的结构体。

    go-restful 框架中,服务定义由 restful.WebService 结构维护,初始化示例如下:

    1. func NewKVService() *restful.WebService {
    2. service := new(restful.WebService)
    3. service.
    4. Path("/kvs").
    5. Consumes(restful.MIME_JSON).
    6. Produces(restful.MIME_JSON)
    7. service.Route(service.POST("").To(Create))
    8. service.Route(service.DELETE("/{key}").To(Delete))
    9. service.Route(service.PUT("/{key}").To(Update))
    10. service.Route(service.GET("/{key}").To(Retrieve))
    11. service.Route(service.GET("").To(Search))
    12. return service
    13. }

    如上述代码,构建 restful.WebService 结构,需指定 路径 ( path )以及 路由规则 。路由规则包括 3 个组成部分, 路径 ( path )、 方法 ( method )以及 处理函数

    处理函数有两个参数: 请求对象 ( restful.Request )以及 响应对象 ( restful.Response )。其中,请求对象用于获取请求信息;响应对象用于返回数据。

    创建资源需在资源组 URI ( /kvs )之上实现 POST 方法处理函数:

    1. func Create(request *restful.Request, response *restful.Response) {
    2. if len(mapping) >= SIZE_LIMIT {
    3. response.WriteEntity(&Result{
    4. Result: false,
    5. Data: nil,
    6. Message: "out of resources",
    7. })
    8. return
    9. }
    10. var item DataItem
    11. err := request.ReadEntity(&item)
    12. if err != nil {
    13. response.WriteEntity(&Result{
    14. Result: false,
    15. Data: nil,
    16. Message: err.Error(),
    17. })
    18. return
    19. }
    20. if item.Key == "" {
    21. response.WriteEntity(&Result{
    22. Result: false,
    23. Data: nil,
    24. Message: "data key missing",
    25. })
    26. return
    27. }
    28. _, ok := mapping[item.Key]
    29. if ok {
    30. response.WriteEntity(&Result{
    31. Result: false,
    32. Data: nil,
    33. Message: "resource exists",
    34. })
    35. return
    36. }
    37. mapping[item.Key] = &item
    38. response.WriteEntity(&Result{
    39. Result: true,
    40. Data: &item,
    41. })
    42. }

    • 检查资源使用情况,在资源不足时返回错误;
    • 获取请求数据,即反序列化请求数据;
    • 检查数据,当数据不符合要求时下返回错误;
    • 检查资源,在资源已存在时返回错误;
    • 创建资源并加入资源组;注意到,我们通过在请求数据中包含数据键( key 字段 )来保证该操作的 幂等性

    删除资源需要为资源 URI 实现 DELETE 方法处理函数。资源 URI 形如 /kvs/<key> ,其中 kvs 是资源名, <key> 是用于唯一标识一个资源的键(或者 ID )。

    /_src/go/restful/go-restful/kvs/kvs.go

    1. func Delete(request *restful.Request, response *restful.Response) {
    2. key := request.PathParameter("key")
    3. item, ok := mapping[key]
    4. if !ok {
    5. response.WriteEntity(&Result{
    6. Result: false,
    7. Message: "resource not exists",
    8. })
    9. }
    10. delete(mapping, key)
    11. response.WriteEntity(&Result{
    12. Result: true,
    13. Data: item,
    14. })
    15. }

    删除资源前,先检查资源是否存在。当资源不存在时,直接返回错误;当资源存在时,删除该资源并将其返回。

    更新资源需要为资源 URI 实现 PUT 方法处理函数:

    /_src/go/restful/go-restful/kvs/kvs.go

    1. func Update(request *restful.Request, response *restful.Response) {
    2. key := request.PathParameter("key")
    3. item, ok := mapping[key]
    4. if !ok {
    5. response.WriteEntity(&Result{
    6. Result: false,
    7. Message: "resource not exists",
    8. })
    9. return
    10. }
    11. err := request.ReadEntity(item)
    12. if err != nil {
    13. response.WriteEntity(&Result{
    14. Result: false,
    15. Message: err.Error(),
    16. })
    17. return
    18. }
    19. response.WriteEntity(&Result{
    20. Result: true,
    21. Data: item,
    22. })
    23. }

    更新方式由两种, 部分更新 以及 整体替换 ,该例子实现的是前者。

    查询操作需要为资源 URI 实现 GET 方法处理函数:

    /_src/go/restful/go-restful/kvs/kvs.go

    列举给定资源组内的资源,需要为资源组 URI 实现 GET 方法:

    /_src/go/restful/go-restful/kvs/kvs.go

    1. func Search(request *restful.Request, response *restful.Response) {
    2. var skip uint64 = 0
    3. var limit uint64 = 10
    4. var err error
    5. arg_skip := request.QueryParameter("skip")
    6. if arg_skip != "" {
    7. skip, err = strconv.ParseUint(arg_skip, 10, 64)
    8. if err != nil {
    9. response.WriteEntity(&Result{
    10. Result: false,
    11. Message: err.Error(),
    12. })
    13. return
    14. }
    15. }
    16. arg_limit := request.QueryParameter("limit")
    17. if arg_limit != "" {
    18. limit, err = strconv.ParseUint(arg_limit, 10, 64)
    19. if err != nil {
    20. response.WriteEntity(&Result{
    21. Result: false,
    22. Message: err.Error(),
    23. })
    24. return
    25. }
    26. }
    27. items := make([]*DataItem, 0, limit)
    28. var i uint64 = 0
    29. for _, item := range mapping {
    30. if i < skip {
    31. i += 1
    32. continue
    33. }
    34. items = append(items, item)
    35. if uint64(len(items)) >= limit {
    36. break
    37. }
    38. }
    39. response.WriteEntity(&Result{
    40. Result: true,
    41. Data: items,
    42. Meta: &ResultMeta{
    43. Total: uint64(len(mapping)),
    44. Skip: skip,
    45. Count: uint64(len(items)),
    46. },
    47. })
    48. }

    可以通过 URI 参数指定列举条件,则只返回符合条件的资源。此外,还可以为列举操作实现分页逻辑,这在资源数很多的情况下非常有用。

    注意到,我们在返回数据中,用一个名为 meta 字段存放分页信息。分页信息包括三部分:总资源数( total )、当前页资源数( count )以及当前页起点( skip )。

    小菜学编程