使用Go构建RESTful的JSON API

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

JSON API是啥?

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

Sponsor Note

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

一个基本的Web Server

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

package main

import (
        "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。spring

增长一个路由

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

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

package main

import (
        "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安装全部未安装的依赖包。api

译者注:

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

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

建立更多的基本路由

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

package main

import (
    "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,分别是:

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

基本样式

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

package main

import “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 main

import "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 main

import (
    "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 main

import (
    "net/http"

    "github.com/gorilla/mux"
)

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

type Routes []Route

func 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 main

import "time"

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

type Todos []Todo

main.go

package main

import (
        "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 main

import (
    "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 main

import "net/http"

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

type Routes []Route

var routes = Routes{
    Route{
        "Index",
        "GET",
        "/",
        Index,
    },
    Route{
        "TodoIndex",
        "GET",
        "/todos",
        TodoIndex,
    },
    Route{
        "TodoShow",
        "GET",
        "/todos/{todoId}",
        TodoShow,
    },
}

router.go

package main

import (
    "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 main

import "fmt"

var currentId int

var todos Todos

// Give us some seed data
func 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 main

import "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/todos
Now, 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