前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言开发RESTFul JSON API

Go语言开发RESTFul JSON API

作者头像
李海彬
发布2018-07-26 09:58:47
2.6K0
发布2018-07-26 09:58:47
举报
文章被收录于专栏:Golang语言社区Golang语言社区

也许我们之前有使用过各种各样的API, 当我们遇到设计很糟糕的API的时候,简直感觉崩溃至极。希望通过本文之后,能对设计良好的RESTful API有一个初步认识。

JSON API是什么?

JSON之前,很多网站都通过XML进行数据交换。如果在使用过XML之后,再接触JSON, 毫无疑问,你会觉得世界多么美好。这里不深入JSON API的介绍,有兴趣可以参考jsonapi。

基本的Web服务器

从根本上讲,RESTful服务首先是Web服务。 因此我们可以先看看Go语言中基本的Web服务器是如何实现的。下面例子实现了一个简单的Web服务器,对于任何请求,服务器都响应请求的URL回去。

代码语言:javascript
复制
 1package main
 2import (
 3    "fmt"
 4    "html"
 5    "log"
 6    "net/http"
 7)
 8func main() {
 9    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
10        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
11    })
12    log.Fatal(http.ListenAndServe(":8080", nil))
13}

上面基本的web服务器使用Go标准库的两个基本函数HandleFunc和ListenAndServe。

代码语言:javascript
复制
1func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
2    DefaultServeMux.HandleFunc(pattern, handler)
3}
4func ListenAndServe(addr string, handler Handler) error {
5    server := &Server{Addr: addr, Handler: handler}
6    return server.ListenAndServe()
7}

运行上面的基本web服务,就可以直接通过浏览器访问http://localhost:8080来访问。

代码语言:javascript
复制
1> go run basic_server.go

添加路由

虽然标准库包含有router, 但是我发现很多人对它的工作原理感觉很困惑。 我在自己的项目中使用过各种不同的第三方router库。 最值得一提的是Gorilla Web ToolKit的mux router。

另外一个流行的router是来自Julien Schmidt的叫做httprouter的包。

代码语言:javascript
复制
 1package main
 2import (
 3    "fmt"
 4    "html"
 5    "log"
 6    "net/http"
 7    "github.com/gorilla/mux"
 8)
 9func main() {
10    router := mux.NewRouter().StrictSlash(true)
11    router.HandleFunc("/", Index)
12    log.Fatal(http.ListenAndServe(":8080", router))
13}
14func Index(w http.ResponseWriter, r *http.Request) {
15    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
16}

要运行上面的代码,首先使用go get获取mux router的源代码:

代码语言:javascript
复制
1> go get github.com/gorilla/mux

上面代码创建了一个基本的路由器,给请求"/"赋予Index处理器,当客户端请求http://localhost:8080/的时候,就会执行Index处理器。

如果你足够细心,你会发现之前的基本web服务访问http://localhost:8080/abc能正常响应: 'Hello, "/abc"', 但是在添加了路由之后,就只能访问http://localhost:8080了。 原因很简单,因为我们只添加了对"/"的解析,其他的路由都是无效路由,因此都是404。

创建一些基本的路由

既然我们加入了路由,那么我们就可以再添加更多路由进来了。

假设我们要创建一个基本的ToDo应用, 于是我们的代码就变成下面这样:

代码语言:javascript
复制
 1package main
 2import (
 3    "fmt"
 4    "log"
 5    "net/http"
 6    "github.com/gorilla/mux"
 7)
 8func main() {
 9    router := mux.NewRouter().StrictSlash(true)
10    router.HandleFunc("/", Index)
11    router.HandleFunc("/todos", TodoIndex)
12    router.HandleFunc("/todos/{todoId}", TodoShow)
13    log.Fatal(http.ListenAndServe(":8080", router))
14}
15func Index(w http.ResponseWriter, r *http.Request) {
16    fmt.Fprintln(w, "Welcome!")
17}
18func TodoIndex(w http.ResponseWriter, r *http.Request) {
19    fmt.Fprintln(w, "Todo Index!")
20}
21func TodoShow(w http.ResponseWriter, r *http.Request) {
22    vars := mux.Vars(r)
23    todoId := vars["todoId"]
24    fmt.Fprintln(w, "Todo Show:", todoId)
25}

在这里我们添加了另外两个路由: todos和todos/{todoId}。

这就是RESTful API设计的开始。

请注意最后一个路由我们给路由后面添加了一个变量叫做todoId。

这样就允许我们传递id给路由,并且能使用具体的记录来响应请求。

基本模型

路由现在已经就绪,是时候创建Model了,可以用model发送和检索数据。在Go语言中,model可以使用结构体来实现,而其他语言中model一般都是使用类来实现。

代码语言:javascript
复制
 1package main
 2import (
 3    "time"
 4)
 5type Todo struct {
 6    Name      string
 7    Completed bool
 8    Due       time.Time
 9}
10type Todos []Todo

上面我们定义了一个Todo结构体,用于表示待做项。 另外我们还定义了一种类型Todos, 它表示待做列表,是一个数组,或者说是一个分片。

稍后你就会看到这样会变得非常有用。

返回一些JSON

我们有了基本的模型,那么我们可以模拟一些真实的响应了。我们可以为TodoIndex模拟一些静态的数据列表。

代码语言:javascript
复制
 1package main
 2import (
 3    "encoding/json"
 4    "fmt"
 5    "log"
 6    "net/http"
 7    "github.com/gorilla/mux"
 8)
 9// ...
10func TodoIndex(w http.ResponseWriter, r *http.Request) {
11    todos := Todos{
12        Todo{Name: "Write presentation"},
13        Todo{Name: "Host meetup"},
14    }
15    json.NewEncoder(w).Encode(todos)
16}
17// ...

现在我们创建了一个静态的Todos分片来响应客户端请求。注意,如果你请求http://localhost:8080/todos, 就会得到下面的响应:

代码语言:javascript
复制
 1[
 2    {
 3        "Name": "Write presentation",
 4        "Completed": false,
 5        "Due": "0001-01-01T00:00:00Z"
 6    },
 7    {
 8        "Name": "Host meetup",
 9        "Completed": false,
10        "Due": "0001-01-01T00:00:00Z"
11    }
12]

更好的Model

对于经验丰富的老兵来说,你可能已经发现了一个问题。响应JSON的每个key都是首字母答写的,虽然看起来微不足道,但是响应JSON的key首字母大写不是习惯的做法。 那么下面教你如何解决这个问题:

代码语言:javascript
复制
1type Todo struct {
2    Name      string    `json:"name"`
3    Completed bool      `json:"completed"`
4    Due       time.Time `json:"due"`
5}

其实很简单,就是在结构体中添加标签属性, 这样可以完全控制结构体如何编排(marshalled)成JSON。

拆分代码

到目前为止,我们所有代码都在一个文件中。显得杂乱, 是时候拆分代码了。我们可以将代码按照功能拆分成下面多个文件。

我们准备创建下面的文件,然后将相应代码移到具体的代码文件中:

  • main.go: 程序入口文件。
  • handlers.go: 路由相关的处理器。
  • routes.go: 路由。
  • todo.go: todo相关的代码。
代码语言:javascript
复制
 1package main
 2import (
 3    "encoding/json"
 4    "fmt"
 5    "net/http"
 6    "github.com/gorilla/mux"
 7)
 8func Index(w http.ResponseWriter, r *http.Request) {
 9    fmt.Fprintln(w, "Welcome!")
10}
11func TodoIndex(w http.ResponseWriter, r *http.Request) {
12    todos := Todos{
13        Todo{Name: "Write presentation"},
14        Todo{Name: "Host meetup"},
15    }
16    if err := json.NewEncoder(w).Encode(todos); err != nil {
17        panic(err)
18    }
19}
20func TodoShow(w http.ResponseWriter, r *http.Request) {
21    vars := mux.Vars(r)
22    todoId := vars["todoId"]
23    fmt.Fprintln(w, "Todo show:", todoId)
24}
代码语言:javascript
复制
 1package main
 2import (
 3    "net/http"
 4    "github.com/gorilla/mux"
 5)
 6type Route struct {
 7    Name        string
 8    Method      string
 9    Pattern     string
10    HandlerFunc http.HandlerFunc
11}
12type Routes []Route
13func NewRouter() *mux.Router {
14    router := mux.NewRouter().StrictSlash(true)
15    for _, route := range routes {
16        router.
17            Methods(route.Method).
18            Path(route.Pattern).
19            Name(route.Name).
20            Handler(route.HandlerFunc)
21    }
22    return router
23}
24var routes = Routes{
25    Route{
26        "Index",
27        "GET",
28        "/",
29        Index,
30    },
31    Route{
32        "TodoIndex",
33        "GET",
34        "/todos",
35        TodoIndex,
36    },
37    Route{
38        "TodoShow",
39        "GET",
40        "/todos/{todoId}",
41        TodoShow,
42    },
43}
代码语言:javascript
复制
1package main
2import "time"
3type Todo struct {
4    Name      string    `json:"name"`
5    Completed bool      `json:"completed"`
6    Due       time.Time `json:"due"`
7}
8type Todos []Todo
9}
代码语言:javascript
复制
1package main
2import (
3    "log"
4    "net/http"
5)
6func main() {
7    router := NewRouter()
8    log.Fatal(http.ListenAndServe(":8080", router))
9}

更好的Routing

我们重构的过程中,我们创建了一个更多功能的routes文件。 这个新文件利用了一个包含多个关于路由信息的结构体。 注意,这里我们可以指定请求的类型,例如GET, POST, DELETE等等。

输出Web日志

在拆分的路由文件中,我也包含有一个不可告人的动机。稍后你就会看到,拆分之后很容易使用另外的函数来修饰http处理器。

首先我们需要有对web请求打日志的能力,就像很多流行web服务器那样的。 在Go语言中,标准库里边没有web日志包或功能, 因此我们需要自己创建。

代码语言:javascript
复制
 1package logger
 2import (
 3    "log"
 4    "net/http"
 5    "time"
 6)
 7func Logger(inner http.Handler, name string) http.Handler {
 8    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 9        start := time.Now()
10        inner.ServeHTTP(w, r)
11        log.Printf(
12            "%s\t%s\t%s\t%s",
13            r.Method,
14            r.RequestURI,
15            name,
16            time.Since(start),
17        )
18    })
19}

上面我们定义了一个Logger函数,可以给handler进行包装修饰。

这是Go语言中非常标准的惯用方式。其实也是函数式编程的惯用方式。 非常有效,我们只需要将Handler传入该函数, 然后它会将传入的handler包装一下,添加web日志和耗时统计功能。

应用Logger修饰器

要应用Logger修饰符, 我们可以创建router, 我们只需要简单的将我们所有的当前路由都包到其中, NewRouter函数修改如下:

代码语言:javascript
复制
 1func NewRouter() *mux.Router {
 2    router := mux.NewRouter().StrictSlash(true)
 3    for _, route := range routes {
 4        var handler http.Handler
 5        handler = route.HandlerFunc
 6        handler = Logger(handler, route.Name)
 7        router.
 8            Methods(route.Method).
 9            Path(route.Pattern).
10            Name(route.Name).
11            Handler(handler)
12    }
13    return router
14}

现在再次运行我们的程序,我们就可以看到日志大概如下:

代码语言:javascript
复制
2014/11/19 12:41:39 GET /todos TodoIndex 148.324us

这个路由文件太疯狂...让我们重构它吧

路由routes文件现在已经变得稍微大了些, 下面我们将它分解成多个文件:

  • routes.go
  • router.go
代码语言:javascript
复制
 1package main
 2import "net/http"
 3type Route struct {
 4    Name        string
 5    Method      string
 6    Pattern     string
 7    HandlerFunc http.HandlerFunc
 8}
 9type Routes []Route
10var routes = Routes{
11    Route{
12        "Index",
13        "GET",
14        "/",
15        Index,
16    },
17    Route{
18        "TodoIndex",
19        "GET",
20        "/todos",
21        TodoIndex,
22    },
23    Route{
24        "TodoShow",
25        "GET",
26        "/todos/{todoId}",
27        TodoShow,
28    },
29}
代码语言:javascript
复制
 1package main
 2import (
 3    "net/http"
 4    "github.com/gorilla/mux"
 5)
 6func NewRouter() *mux.Router {
 7    router := mux.NewRouter().StrictSlash(true)
 8    for _, route := range routes {
 9        var handler http.Handler
10        handler = route.HandlerFunc
11        handler = Logger(handler, route.Name)
12        router.
13            Methods(route.Method).
14            Path(route.Pattern).
15            Name(route.Name).
16            Handler(handler)
17    }
18    return router
19}

另外再承担一些责任

到目前为止,我们已经有了一些相当好的样板代码(boilerplate), 是时候重新审视我们的处理器了。我们需要稍微多的责任。 首先修改TodoIndex,添加下面两行代码:

代码语言:javascript
复制
 1func TodoIndex(w http.ResponseWriter, r *http.Request) {
 2    todos := Todos{
 3        Todo{Name: "Write presentation"},
 4        Todo{Name: "Host meetup"},
 5    }
 6    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
 7    w.WriteHeader(http.StatusOK)
 8    if err := json.NewEncoder(w).Encode(todos); err != nil {
 9        panic(err)
10    }
11}

这里发生了两件事。 首先,我们设置了响应类型并告诉客户端期望接受JSON。第二,我们明确的设置了响应状态码。

Go语言的net/http服务器会尝试为我们猜测输出内容类型(然而并不是每次都准确的), 但是既然我们已经确切的知道响应类型,我们总是应该自己设置它。

稍等片刻,我们的数据库在哪里?

很明显,如果我们要创建RESTful API, 我们需要一些用于存储和检索数据的地方。然而,这个是不是本文的范围之内, 因此我们将简单的创建一个非常简陋的模拟数据库(非线程安全的)。

我们创建一个repo.go文件,内容如下:

代码语言:javascript
复制
 1package main
 2import "fmt"
 3var currentId int
 4var todos Todos
 5// Give us some seed data
 6func init() {
 7    RepoCreateTodo(Todo{Name: "Write presentation"})
 8    RepoCreateTodo(Todo{Name: "Host meetup"})
 9}
10func RepoFindTodo(id int) Todo {
11    for _, t := range todos {
12        if t.Id == id {
13            return t
14        }
15    }
16    // return empty Todo if not found
17    return Todo{}
18}
19func RepoCreateTodo(t Todo) Todo {
20    currentId += 1
21    t.Id = currentId
22    todos = append(todos, t)
23    return t
24}
25func RepoDestroyTodo(id int) error {
26    for i, t := range todos {
27        if t.Id == id {
28            todos = append(todos[:i], todos[i+1:]...)
29            return nil
30        }
31    }
32    return fmt.Errorf("Could not find Todo with id of %d to delete", id)
33}

给Todo添加ID

我们创建了模拟数据库,我们使用并赋予id, 因此我们相应的也需要更新我们的Todo结构体。

代码语言:javascript
复制
1package main
2import "time"
3type Todo struct {
4    Id        int       `json:"id"`
5    Name      string    `json:"name"`
6    Completed bool      `json:"completed"`
7    Due       time.Time `json:"due"`
8}
9type Todos []Todo

更新我们的TodoIndex

要使用数据库,我们需要在TodoIndex中检索数据。修改代码如下:

代码语言:javascript
复制
1func TodoIndex(w http.ResponseWriter, r *http.Request) {
2    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
3    w.WriteHeader(http.StatusOK)
4    if err := json.NewEncoder(w).Encode(todos); err != nil {
5        panic(err)
6    }
7}

POST JSON

到目前为止,我们只是输出JSON, 现在是时候进入存储一些JSON了。

在routes.go文件中添加如下路由:

代码语言:javascript
复制
1Route{
2    "TodoCreate",
3    "POST",
4    "/todos",
5    TodoCreate,
6},

Create路由

代码语言:javascript
复制
 1func TodoCreate(w http.ResponseWriter, r *http.Request) {
 2    var todo Todo
 3    body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))
 4    if err != nil {
 5        panic(err)
 6    }
 7    if err := r.Body.Close(); err != nil {
 8        panic(err)
 9    }
10    if err := json.Unmarshal(body, &todo); err != nil {
11        w.Header().Set("Content-Type", "application/json; charset=UTF-8")
12        w.WriteHeader(422) // unprocessable entity
13        if err := json.NewEncoder(w).Encode(err); err != nil {
14            panic(err)
15        }
16    }
17    t := RepoCreateTodo(todo)
18    w.Header().Set("Content-Type", "application/json; charset=UTF-8")
19    w.WriteHeader(http.StatusCreated)
20    if err := json.NewEncoder(w).Encode(t); err != nil {
21        panic(err)
22    }
23}

首先我们打开请求的body。 注意我们使用io.LimitReader。这样是保护服务器免受恶意攻击的好方法。假设如果有人想要给你服务器发送500GB的JSON怎么办?

我们读取body以后,我们解构Todo结构体。 如果失败,我们作出正确的响应,使用恰当的响应码422, 但是我们依然使用json响应回去。 这样可以允许客户端理解有错发生了, 而且有办法知道到底发生了什么错误。

最后,如果所有都通过了,我们就响应201状态码,表示请求创建的实体已经成功创建了。 我们同样还是响应回代表我们创建的实体的json, 它会包含一个id, 客户端可能接下来需要用到它。

POST一些JSON

我们现在有了伪repo, 也有了create路由,那么我们需要post一些数据。 我们使用curl通过下面的命令来达到这个目的:

代码语言:javascript
复制
1curl -H "Content-Type: application/json" -d '{"name": "New Todo"}' http://localhost:8080/todos

如果你再次通过http://localhost:8080/todos访问,大概会得到下面的响应:

代码语言:javascript
复制
 1[
 2    {
 3        "id": 1,
 4        "name": "Write presentation",
 5        "completed": false,
 6        "due": "0001-01-01T00:00:00Z"
 7    },
 8    {
 9        "id": 2,
10        "name": "Host meetup",
11        "completed": false,
12        "due": "0001-01-01T00:00:00Z"
13    },
14    {
15        "id": 3,
16        "name": "New Todo",
17        "completed": false,
18        "due": "0001-01-01T00:00:00Z"
19    }
20]

我们还没有做的事情

虽然我们已经有了很好的开端,但是还有很多事情没有做:

  • 版本控制: 如果我们需要修改API, 结果完全改变了怎么办? 可能我们需要在我们的路由开头加上/v1/prefix?
  • 授权: 除非这些都是公开/免费API, 我们可能还需要授权。 建议学习JSON web tokens的东西。

eTag - 如果你正在构建一些需要扩展的东西,你可能需要实现eTag。

还有什么?

对于所有项目来说,开始都很小,但是很快就变得失控了。但是如果我们想要将它带到另外一个层次, 让他生产就绪, 还有一些额外的事情需要做:

  • 大量重构(refactoring).
  • 为这些文件创建几个包,例如一些JSON助手、修饰符、处理器等等。
  • 测试, 使得,你不能忘记这点。这里我们没有做任何测试。对于生产系统来说,测试是必须的。

源代码

https://github.com/corylanou/...

总结

对我来说,最重要的,需要记住的是我们要建立一个负责任的API。 发送适当的状态码,header等,这些是API广泛采用的关键。我希望本文能让你尽快开始自己的API。

参考链接

  • Go语言RESTful JSON API实现
  • JSON API
  • Gorilla Web Toolkit
  • httprouter
  • JSON Web Tokens
  • eTag
  • 专题首页

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JSON API是什么?
  • 基本的Web服务器
  • 添加路由
  • 创建一些基本的路由
  • 基本模型
  • 返回一些JSON
  • 更好的Model
  • 拆分代码
  • 更好的Routing
  • 输出Web日志
  • 应用Logger修饰器
  • 这个路由文件太疯狂...让我们重构它吧
  • 另外再承担一些责任
  • 稍等片刻,我们的数据库在哪里?
  • 给Todo添加ID
  • 更新我们的TodoIndex
  • POST JSON
  • Create路由
  • POST一些JSON
  • 我们还没有做的事情
  • 还有什么?
  • 源代码
  • 总结
  • 参考链接
相关产品与服务
Serverless HTTP 服务
Serverless HTTP 服务基于腾讯云 API 网关 和 Web Cloud Function(以下简称“Web Function”)建站云函数(云函数的一种类型)的产品能力,可以支持各种类型的 HTTP 服务开发,实现了 Serverless 与 Web 服务最优雅的结合。用户可以快速构建 Web 原生框架,把本地的 Express、Koa、Nextjs、Nuxtjs 等框架项目快速迁移到云端,同时也支持 Wordpress、Discuz Q 等现有应用模版一键快速创建。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档