使用Go构建RESTful的JSON API

来源:互联网 发布:慢走丝编程招聘信息 编辑:程序博客网 时间:2024/04/30 05:09

原文地址http://thenewstack.io/make-a-restful-json-api-go/
这篇文章不仅仅讨论如何使用Go构建RESTful的JSON API,同时也会讨论如何设计好的RESTful API。如果你曾经遭遇了未遵循良好设计的API,那么你最终将写烂代码来使用这些垃圾API。希望阅读这篇文章后,你能够对好的API应该是怎样的有更多的认识。

JSON API是啥?

在JSON前,XML是一种主流的文本格式。笔者有幸XML和JSON都使用过,毫无疑问,JSON是明显的赢家。本文不会深入涉及JSON API的概念,在jsonapi.org可以找到的详细的描述。

Sponsor Note

SpringOne2GX是一个专门面向App开发者、解决方案和数据架构师的会议。议题都是专门针对程序猿(媛),架构师所使用的流行的开源技术,如:Spring IO Projects,Groovy & Grails,Cloud Foundry,RabbitMQ,Redis,Geode,Hadoop and Tomcat等。

一个基本的Web Server

一个RESTful服务本质上首先是一个Web service。下面的是示例是一个最简单的Web server,对于任何请求都简单的直接返回请求链接:

package mainimport (        "fmt"        "html"        "log"        "net/http")func main() {        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {            fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))    })    log.Fatal(http.ListenAndServe(":8080", nil))}

编译执行这个示例将运行这个server,监听8080端口。尝试使用http://localhost:8080访问server。

增加一个路由

当大多数标准库开始支持路由,我发现大多数人都搞不清楚它们是如何工作的。我在项目中使用过几个第三方的router。印象最深的是Gorilla Web Toolkit中的mux router.

另一个比较流行的router是Julien Schmidt贡献的httprouter

package mainimport (        "fmt"        "html"        "log"        "net/http"        "github.com/gorilla/mux")func main() {        router := mux.NewRouter().StrictSlash(true)        router.HandleFunc("/", Index)        log.Fatal(http.ListenAndServe(":8080", router))}func Index(w http.ResponseWriter, r *http.Request) {        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))}

运行上面的示例,首先需要安装包“github.com/gorilla/mux”.可以直接使用命令go get遍历整个source code安装所有未安装的依赖包。

译者注:

也可以使用go get "github.com/gorilla/mux"直接安装包。

上面的示例创建了一个简单的router,增加了一个“/”路由,并分配Index handler响应针对指定的endpoint的访问。这是你会发现在第一个示例中还能访问的如http://localhost:8080/foo这类的链接在这个示例中不再工作了,这个示例将只能响应链接http://localhost:8080.

创建更多的基本路由

上一节我们已经有了一个路由,是时候创建更多的路由了。假设我们将要创建一个基本的TODO app。

package mainimport (    "fmt"    "log"    "net/http"    "github.com/gorilla/mux")func main() {    router := mux.NewRouter().StrictSlash(true)    router.HandleFunc("/", Index)    router.HandleFunc("/todos", TodoIndex)    router.HandleFunc("/todos/{todoId}", TodoShow)    log.Fatal(http.ListenAndServe(":8080", router))}func Index(w http.ResponseWriter, r *http.Request) {    fmt.Fprintln(w, "Welcome!")}func TodoIndex(w http.ResponseWriter, r *http.Request) {    fmt.Fprintln(w, "Todo Index!")}func TodoShow(w http.ResponseWriter, r *http.Request) {    vars := mux.Vars(r)    todoId := vars["todoId"]    fmt.Fprintln(w, "Todo show:", todoId)}

现在我们又在上一个示例的基础上增加了两个routes,分别是:

  • ToDo index route:http://localhost:8080/todos
  • ToDo show route: http://localhost:8080/todos/{todoId}

这就是一个RESTful设计的开始。注意,最后一个路由我们增加了一个名为todoId的变量。这将允许我们向route传递变量,然后获得合适的响应记录。

基本样式

有了路由后,就可以创建一些基本的TODO样式用于发送和检索数据。在一些其他语言中使用类(class)来达到这个目的,Go中使用struct。

package mainimport “time”type Todo struct {    Name        string    Completed   tool    Due         time.time}type Todos []Todo   

注:

最后一行定义的类型TodosTodo的slice。稍后你将会看到怎么使用它。

返回JSON

基于上面的基本样式,我们可以模拟真实的响应,并基于静态数据列出TodoIndex。

func TodoIndex(w http.ResponseWriter, r *http.Request) {    todos := Todos{    Todo{Name: "Write presentation"},    Todo{Name: "Host meetup"},    }    json.NewEncoder(w).Encode(todos)}

这样就创建了一个Todos的静态slice,并被编码响应用户请求。如果这时你访问http://localhost:8080/todos,你将得到如下响应:

[    {        "Name": "Write presentation",        "Completed": false,        "Due": "0001-01-01T00:00:00Z"    },    {    "Name": "Host meetup",        "Completed": false,        "Due": "0001-01-01T00:00:00Z"    }]

一个稍微好点的样式

可能你已经发现了,基于前面的样式,todos返回的并不是一个标准的JSON数据包(JSON格式定义中不包含大写字母)。虽然这个问题有那么一点微不足道,但是我们还是可以解决它:

package mainimport "time"type Todo struct {    Name      string    `json:"name"`    Completed bool      `json:"completed"`    Due       time.Time `json:"due"`}type Todos []Todo

上面的代码示例在原来的基础上增加了struct tags,这样可以指定JSON的编码格式。

文件拆分

到此我们需要对这个项目稍微做下重构。现在一个文件包含了太多的内容。我们将创建如下几个文件,并重新组织文件内容:

  • main.go
  • handlers.go
  • routes.go
  • todo.go

handlers.go

package mainimport (    "encoding/json"    "fmt"    "net/http"    "github.com/gorilla/mux")func Index(w http.ResponseWriter, r *http.Request) {    fmt.Fprintln(w, "Welcome!")}func TodoIndex(w http.ResponseWriter, r *http.Request) {    todos := Todos{        Todo{Name: "Write presentation"},        Todo{Name: "Host meetup"},    }    if err := json.NewEncoder(w).Encode(todos); err != nil {        panic(err)    }}func TodoShow(w http.ResponseWriter, r *http.Request) {    vars := mux.Vars(r)    todoId := vars["todoId"]    fmt.Fprintln(w, "Todo show:", todoId)}

routes.go

package mainimport (    "net/http"    "github.com/gorilla/mux")type Route struct {    Name        string    Method      string    Pattern     string    HandlerFunc http.HandlerFunc}type Routes []Routefunc NewRouter() *mux.Router {    router := mux.NewRouter().StrictSlash(true)    for _, route := range routes {        router.        Methods(route.Method).            Path(route.Pattern).            Name(route.Name).            Handler(route.HandlerFunc)    }    return router}var routes = Routes{    Route{        "Index",        "GET",        "/",        Index,    },    Route{        "TodoIndex",        "GET",        "/todos",        TodoIndex,    },    Route{        "TodoShow",        "GET",        "/todos/{todoId}",        TodoShow,    },}

todo.go

package mainimport "time"type Todo struct {    Name      string    `json:"name"`    Completed bool      `json:"completed"`    Due       time.Time `json:"due"`}type Todos []Todo

main.go

package mainimport (        "log"        "net/http")func main() {    router := NewRouter()    log.Fatal(http.ListenAndServe(":8080", router))}

更好的路由

上面重构的一部分就是创建了一个更详细的route文件,新文件中使用了一个struct包含了更多的有关路由的详细信息。尤其是,我们可以通过这个struct指定请求的动作,如GET、POST、DELETE等。

记录Web Log

前面的拆分文件中,我还有一个更长远的考虑。稍后你将会看到,拆分后我将能够很轻松的使用其他函数装饰我的http handlers。这一节我们将使用这个功能让我们的web能够像其他现代的网站一样为web访问请求记Log。在Go中,目前还没有一个web logging package,也没有标准库提供相应的功能。所以我们不得不自己实现一个。

在前面拆分文件的基础上,我们创建一个叫logger.go的新文件,并在文件中添加如下代码:

package mainimport (    "log"    "net/http"    "time")func Logger(inner http.Handler, name string) http.Handler {    return http.HandlerFunc(func(w http.ResponseWriter, r       *http.Request) {        start := time.Now()        inner.ServeHTTP(w, r)        log.Printf(            "%s\t%s\t%s\t%s",            r.Method,            r.RequestURI,            name,            time.Since(start),        )    })}

这样,如果你访问http://localhost:8080/todos,你将会看到console中有如下log输出。

2014/11/19 12:41:39 GET /todos  TodoIndex       148.324us

Routes file开始疯狂…继续重构

基于上面的拆分,你会发现继续照着这个节奏发展,routes.go文件将变得越来越庞大。所以我们继续拆分这个文件。将其拆分为如下两个文件:

  • router.go
  • routes.go

routes.go 回归

package mainimport "net/http"type Route struct {    Name        string    Method      string    Pattern     string    HandlerFunc http.HandlerFunc}type Routes []Routevar routes = Routes{    Route{        "Index",        "GET",        "/",        Index,    },    Route{        "TodoIndex",        "GET",        "/todos",        TodoIndex,    },    Route{        "TodoShow",        "GET",        "/todos/{todoId}",        TodoShow,    },}

router.go

package mainimport (    "net/http"    "github.com/gorilla/mux")func NewRouter() *mux.Router {    router := mux.NewRouter().StrictSlash(true)    for _, route := range routes {        var handler http.Handler        handler = route.HandlerFunc        handler = Logger(handler, route.Name)        router.            Methods(route.Method).            Path(route.Pattern).            Name(route.Name).            Handler(handler)    }    return router}

做更多的事情

现在我们已经有了一个不错的模板,是时候重新考虑我们handlers了,让handler能做更多的事情。首先我们在TodoIndex中增加两行代码。

func TodoIndex(w http.ResponseWriter, r *http.Request) {    todos := Todos{        Todo{Name: "Write presentation"},        Todo{Name: "Host meetup"},    }    w.Header().Set("Content-Type", "application/json; charset=UTF-8")    w.WriteHeader(http.StatusOK)    if err := json.NewEncoder(w).Encode(todos); err != nil {        panic(err)    }}

新增的两行代码让TodoIndex handler多做两件事。首先返回client期望的json,并告知内容类型。然后明确的设置一个状态码。

Go的net/http server在Header中没有显示的说明内容类型时将尝试为我们猜测内容类型,但是并不是总是那么准确。所以在我们知道content类型的情况下,我们应该总是自己设置类型。

等等,数据库在哪儿?

如果我们继续构造RESTful API,我们需要考虑一个地方用于存储和检索数据。但是这超出了本文所讨论的范畴,所以这里简单的实现了一个粗糙的数据存储(粗糙到甚至都没线程安全机制)。

创建一个名为repo.go的文件,代码如下:

package mainimport "fmt"var currentId intvar todos Todos// Give us some seed datafunc init() {    RepoCreateTodo(Todo{Name: "Write presentation"})    RepoCreateTodo(Todo{Name: "Host meetup"})}func RepoFindTodo(id int) Todo {    for _, t := range todos {        if t.Id == id {            return t        }    }    // return empty Todo if not found    return Todo{}}func RepoCreateTodo(t Todo) Todo {    currentId += 1    t.Id = currentId    todos = append(todos, t)    return t}func RepoDestroyTodo(id int) error {    for i, t := range todos {        if t.Id == id {            todos = append(todos[:i], todos[i+1:]...)            return nil        }    }    return fmt.Errorf("Could not find Todo with id of %d to delete", id)}

为Todo添加一个ID

现在我们已经有了一个粗糙的数据库。我们可以为Todo创建一个ID,用于标识和见识Todo item。数据结构更新如下:

package mainimport "time"type Todo struct {    Id        int       `json:"id"`    Name      string    `json:"name"`    Completed bool      `json:"completed"`    Due       time.Time `json:"due"`}type Todos []Todo

更新TodoIndex handler

数据存储在数据库后,不必在handler中生成数据,直接通过ID检索数据库即可得到相应内容。修改handler如下:

func TodoIndex(w http.ResponseWriter, r *http.Request) {    w.Header().Set("Content-Type", "application/json; charset=UTF-8")    w.WriteHeader(http.StatusOK)    if err := json.NewEncoder(w).Encode(todos); err != nil {        panic(err)    }}

Posting JSON

前面所有的API都是相应GET请求的,只能输出JSON。这节将增加一个上传和存储JSON的API。在routes.go文件中增加如下route:

Route{    "TodoCreate",    "POST",    "/todos",    TodoCreate,},

The Create endpoint

上面创建了一个新的router,现在为这个新的route创建一个endpoint。在handlers.go文件增加TodoCreate handler。代码如下:

func TodoCreate(w http.ResponseWriter, r *http.Request) {    var todo Todo    body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576))    if err != nil {        panic(err)    }    if err := r.Body.Close(); err != nil {        panic(err)    }    if err := json.Unmarshal(body, &todo); err != nil {        w.Header().Set("Content-Type", "application/json;   charset=UTF-8")        w.WriteHeader(422) // unprocessable entity        if err := json.NewEncoder(w).Encode(err); err != nil {            panic(err)        }    }    t := RepoCreateTodo(todo)    w.Header().Set("Content-Type", "application/json;   charset=UTF-8")    w.WriteHeader(http.StatusCreated)    if err := json.NewEncoder(w).Encode(t); err != nil {        panic(err)    }}

上面的代码中,首先我们获取用户请求的body。注意,在获取body时我们使用了io.LimitReader,这是一个防止你的服务器被恶意攻击的好方法。试想如果有人给你发送了一个500GB的json。

读取body后,将其内容解码到Todo struct中。如果解码失败,我们要做的事情不仅仅是返回一个‘422’这样的状态码,同时还会返回一段包含错误信息的json。这能够使客户端不仅知道有错误发生,还能了解错误发生在哪儿。

最后,如果一切顺利,我们将向客户端返回状态码201,同时我们还向客户端返回创建的实体内容,这些信息客户端在后面的操作中可能会用到。

Post JSON

所有的工作的完成后,我们就可以上传下json string测试一下了。Sample及返回结果如下所示:

curl -H "Content-Type: application/json" -d '{"name":"New Todo"}' http://localhost:8080/todosNow, if you go to http://localhost/todos we should see the following response:[    {        "id": 1,        "name": "Write presentation",        "completed": false,        "due": "0001-01-01T00:00:00Z"    },    {        "id": 2,        "name": "Host meetup",        "completed": false,        "due": "0001-01-01T00:00:00Z"    },    {        "id": 3,        "name": "New Todo",        "completed": false,        "due": "0001-01-01T00:00:00Z"    }]

我们未做的事情

现在我们已经有了一个好的开头,后面还有很多事情要做。下面是我们还未做的事情:

  • 版本控制 - 如果我们需要修改API,并且这将导致重大的更改?也许我们可以从为所有的routes添加/v1这样的前缀开始。
  • 身份认证 - 除非这是一个自由/公开的API,否则我们可能需要添加一些认证机制。建议学习JSON web tokens
  • eTags - 如果你的构建需要扩展,你可能需要实现eTags

还剩些啥?

所有的项目都是开始的时候很小,但是很快就会发展开始变得失控。如果我想把这件事带到下一个层级,并准备使其投入生产,则还有如下这些额外的事情需要做:

  • 很多的重构
  • 将这些文件封装成一些package,如JSON helpers,decorators,handlers等等。
  • 测试…是的,这个不能忽略。目前我们还没有做任何的测试,但是对于一个产品,这个是必须的。

如何获取源代码

如果你想获取本文示例的源代码,repo地址在这里:https://github.com/corylanou/tns-restful-json-api

0 0
原创粉丝点击