Go语言RESTful JSON API建立

Go语言开发RESTFul JSON API

RESTful API在Web项目开发中普遍使用,本文针对Go语言如何一步步实现RESTful JSON API进行讲解, 另外也会涉及到RESTful设计方面的话题。 html

也许咱们以前有使用过各类各样的API, 当咱们遇到设计很糟糕的API的时候,简直感受崩溃至极。但愿经过本文以后,能对设计良好的RESTful API有一个初步认识。git

JSON API是什么?

JSON以前,不少网站都经过XML进行数据交换。若是在使用过XML以后,再接触JSON, 毫无疑问,你会以为世界多么美好。这里不深刻JSON API的介绍,有兴趣能够参考jsonapigithub

基本的Web服务器

从根本上讲,RESTful服务首先是Web服务。 所以咱们能够先看看Go语言中基本的Web服务器是如何实现的。下面例子实现了一个简单的Web服务器,对于任何请求,服务器都响应请求的URL回去。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))
}

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

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

运行上面的基本web服务,就能够直接经过浏览器访问http://localhost:8080来访问。编程

> go run basic_server.go

添加路由

虽然标准库包含有router, 可是我发现不少人对它的工做原理感受很困惑。 我在本身的项目中使用过各类不一样的第三方router库。 最值得一提的是Gorilla Web ToolKit的mux router。json

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

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))
}

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

> go get github.com/gorilla/mux

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

若是你足够细心,你会发现以前的基本web服务访问http://localhost:8080/abc能正常响应: 'Hello, "/abc"', 可是在添加了路由以后,就只能访问http://localhost:8080了。 缘由很简单,由于咱们只添加了对"/"的解析,其余的路由都是无效路由,所以都是404。

建立一些基本的路由

既然咱们加入了路由,那么咱们就能够再添加更多路由进来了。

假设咱们要建立一个基本的ToDo应用, 因而咱们的代码就变成下面这样:

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)
}

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

这就是RESTful API设计的开始。

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

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

基本模型

路由如今已经就绪,是时候建立Model了,能够用model发送和检索数据。在Go语言中,model可使用结构体来实现,而其余语言中model通常都是使用类来实现。

package main

import (
    "time"
)

type Todo struct {
    Name      string
    Completed bool
    Due       time.Time
}

type Todos []Todo

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

稍后你就会看到这样会变得很是有用。

返回一些JSON

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

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

// ...

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

    json.NewEncoder(w).Encode(todos)
}

// ...

如今咱们建立了一个静态的Todos分片来响应客户端请求。注意,若是你请求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"
    }
]

更好的Model

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

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

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

拆分代码

到目前为止,咱们全部代码都在一个文件中。显得杂乱, 是时候拆分代码了。咱们能够将代码按照功能拆分红下面多个文件。

咱们准备建立下面的文件,而后将相应代码移到具体的代码文件中:

  • main.go: 程序入口文件。
  • handlers.go: 路由相关的处理器。
  • routes.go: 路由。
  • todo.go: todo相关的代码。
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)
}
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,
    },
}
package main

import "time"

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

type Todos []Todo
package main

import (
    "log"
    "net/http"
)

func main() {

    router := NewRouter()

    log.Fatal(http.ListenAndServe(":8080", router))
}

更好的Routing

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

输出Web日志

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

首先咱们须要有对web请求打日志的能力,就像不少流行web服务器那样的。 在Go语言中,标准库里边没有web日志包或功能, 所以咱们须要本身建立。

package logger

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),
        )
    })
}

上面咱们定义了一个Logger函数,能够给handler进行包装修饰。

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

应用Logger修饰器

要应用Logger修饰符, 咱们能够建立router, 咱们只须要简单的将咱们全部的当前路由都包到其中, NewRouter函数修改以下:

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
}

如今再次运行咱们的程序,咱们就能够看到日志大概以下:

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

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

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

  • routes.go
  • router.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,
    },
}
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
}

另外再承担一些责任

到目前为止,咱们已经有了一些至关好的样板代码(boilerplate), 是时候从新审视咱们的处理器了。咱们须要稍微多的责任。 首先修改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)
    }
}

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

Go语言的net/http服务器会尝试为咱们猜想输出内容类型(然而并非每次都准确的), 可是既然咱们已经确切的知道响应类型,咱们老是应该本身设置它。

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

很明显,若是咱们要建立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

咱们建立了模拟数据库,咱们使用并赋予id, 所以咱们相应的也须要更新咱们的Todo结构体。

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

要使用数据库,咱们须要在TodoIndex中检索数据。修改代码以下:

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)
    }
}

POST JSON

到目前为止,咱们只是输出JSON, 如今是时候进入存储一些JSON了。

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

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

Create路由

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。 注意咱们使用io.LimitReader。这样是保护服务器免受恶意攻击的好方法。假设若是有人想要给你服务器发送500GB的JSON怎么办?

咱们读取body之后,咱们解构Todo结构体。 若是失败,咱们做出正确的响应,使用恰当的响应码422, 可是咱们依然使用json响应回去。 这样能够容许客户端理解有错发生了, 并且有办法知道到底发生了什么错误。

最后,若是全部都经过了,咱们就响应201状态码,表示请求建立的实体已经成功建立了。 咱们一样仍是响应回表明咱们建立的实体的json, 它会包含一个id, 客户端可能接下来须要用到它。

POST一些JSON

咱们如今有了伪repo, 也有了create路由,那么咱们须要post一些数据。 咱们使用curl经过下面的命令来达到这个目的:

curl -H "Content-Type: application/json" -d '{"name": "New Todo"}' http://localhost:8080/todos

若是你再次经过http://localhost:8080/todos访问,大概会获得下面的响应:

[
    {
        "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, 结果彻底改变了怎么办? 可能咱们须要在咱们的路由开头加上/v1/prefix?
  • 受权: 除非这些都是公开/免费API, 咱们可能还须要受权。 建议学习JSON web tokens的东西。

eTag - 若是你正在构建一些须要扩展的东西,你可能须要实现eTag。

还有什么?

对于全部项目来讲,开始都很小,可是很快就变得失控了。可是若是咱们想要将它带到另一个层次, 让他生产就绪, 还有一些额外的事情须要作:

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

源代码

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

总结

对我来讲,最重要的,须要记住的是咱们要创建一个负责任的API。 发送适当的状态码,header等,这些是API普遍采用的关键。我但愿本文能让你尽快开始本身的API。

参考连接

相关文章
相关标签/搜索