【翻译】用Go开发RESTful JSON API(Making a RESTful JSON API in Go)

原创文章,转载请注明: 转载自勤奋的小青蛙
本文链接地址: 【翻译】用Go开发RESTful JSON API(Making a RESTful JSON API in Go)

goraster-1024x640

原文出处:https://thenewstack.io/make-a-restful-json-api-go/

在这篇文章中,我们不仅将介绍如何使用Go来创建RESTful JSON API,而且还将讨论好的RESTful设计。如果你以前曾经消费过一个不遵循良好设计的API,那么你最终写不好可供使用的API服务。希望在本文之后,您将更好地了解一个行为良好的API应该是什么样的。

什么是JSON API?

在JSON之前,有XML。使用XML和JSON两者,毫无疑问,JSON是明确的赢家。我不会深入讨论一个JSON API的概念,因为它具有很详细的说明在这个网站上:jsonapi.org

一个简单的Web Server

RESTful服务首先从根本上成为一个Web服务。下面是一个真正基本的Web服务器,通过简单地输出请求url来响应任何请求:

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

}

运行此示例将启动端口8080上的服务器,并可以访问 http://localhost:8080

访问效果如下图所示:

QQ截图20170418130731

添加路由功能

Go的标准库也附带了路由器,但我发现大多数人对它的工作感到困惑。我在项目中使用了几个第三方路由器。最值得注意的是,我使用了Gorilla Web Toolkitmux路由器

另外一个非常著名的路由是httprouter,作者Julien Schmidt.

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

这将从GitHub“github.com/gorilla/mux”中检索Gorilla Mux软件包.

上述示例创建一个基本路由器,添加route /并分配在处理该端点时要运行的Index处理程序。您还会注意到,如果你想访问 http://localhost:8080/foo由于没有定义路由,它将没有任何响应。目前的代码只能响应 http://localhost:8080/

创建一些基本路由

现在我们有路由器到位,现在是创建更多路由的时候了。 我们假设我们要创建一个基本的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)
}

我们现在已经添加了两个端点(或路由)。

一个是todo程序的首页路由: http://localhost:8080/todos

另一个是todo程序的展示路由:http://localhost:8080/todos/{todoId}

这是RESTful设计的开始。

请查看todo程序的展示路由:http://localhost:8080/todos/{todoId} ,我们可以通过给路由传入 id 然后获取对应id的数据记录。

一个基础的Model

现在我们有了路由,现在是创建一个基本的Todo模型,我们可以发送和检索数据。在Go中,结构体通常用作您的模型。许多其他语言也一样使用类来定义模型。

package main

import "time"

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

type Todos []Todo

请注意,在最后一行,我们创建另一个类型,称为Todos,它是Todo的切片(有序集合)。你会看到这在哪里变得很有用。

返回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的静态片段(模拟数据)来发送给客户端。打开浏览器访问:,你应该得到以下响应:

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

一个更好的Model

对于任何经验的开发人员,很容易发现一个问题,json字符串,是建议不该出现大写字母的,所以,为了修改全部键名为小写,我们使用下面的model定义:

package main

import "time"

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

type Todos []Todo

通过添加结构标签,您可以准确控制如何将结构体编组为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))
}

更加全面的路由

作为我们重构的一部分,我们创建了更加全面的路由,Routes.go 文件中的路由包含了更多的路由信息。比如我们现在可以指定请求路由的方法,比如GET, POST, DELETE等等。

web日志

在重构过程中,我发现在Handlers.go文件中,我们是非常容易加上一些额外的功能,比如日志功能。像大多数流行的服务器提供日志输出那样,我们可以用Go来实现一个日志功能。当然在Go的标准库里是没有日志库,我们需要自己实现。

我将创建一个文件 logger.go 并且加入下面的代码:

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

这是Go中非常标准的有效的输出log方法,我们将把我们的处理程序传递给这个函数,然后它将使用记录和定时功能包装所传递的处理程序。

接下来,我们将需要在我们的路由中使用该方法。

使用Log装饰器

要应用装饰器,当我们创建路由器时,我们将通过更新我们的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
}

然后,当我们访问 http://localhost:8080/todos 的时候,你将会看到下面的log输出:

route_log

但是我们发现这个路由文件太大了,代码又很乱,继续重构,我们把Routes.go文件拆分为以下两个文件:

  • Routes.go
  • Router.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
}

那么现在我们便有了很不错的模板了。现在我们重新访问我们的网站,我们添加一些任务,然后我们在额外的添加一些代码。

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

上面代码我们做了两件事情。第一,我们输出了我们的content type,告诉客户端我们输出的是json文本。第二我们明确的设置了http的状态码。

Go的net / http服务器会尝试猜测我们的输出内容类型(但是并不总是准确的),但是由于我们明确地知道类型,所以我们应该始终设置它们。

那么,涉及到数据库操作呢?

显然,如果我们要创建一个RESTful API,我们需要采用持久化方法,比如数据库(mysql, mongodb...)然而,这超出了本文的范围,所以我们将简单地创建一个非常粗糙的(而不是线程安全的)模拟数据库。

创建一个文件: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属性,这样子方便我们进行CRUD的操作。

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

更新ToDo显示方法

要使用数据库,我们现在需要通过修改以下函数来检索Todo中的数据:

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

上面做的都是显示Todo的方法,下面我们要增加一个插入Todo的方法。

POST JSON方法

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

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

然后紧接着创建我们的处理方法:TodoCreate

在Handlers.go中增加该方法:

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

这个方法主要做了如下工作:

我们首先做的是打开请求的正文。请注意,我们使用io.LimitReader。这是防止您的服务器上的恶意攻击的好方法。想象一下,如果有人想给你发送500GB的json!

在我们阅读了消息体之后,我们再将它分配到我们的Todo结构体上。如果失败了,我们将做正确的事情,不仅仅是以适当的状态代码422进行响应,而且我们也会在json字符串中发回错误。这样可以让客户了解不仅出现问题,而且我们有能力沟通出什么问题。

最后,如果一切顺利,我们发回201的状态码,这意味着实体成功创建。我们还发回我们创建的实体的json表示,因为它包含客户端可能需要下一步的id。

测试发送一些json

现在我们用模拟的数据库以及我们创建的路由来测试下。我使用curl通过以下命令来执行:

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

如果访问网站的话,我们会看到新增加的数据过来了:

goserver_adddata

 

我们未涉及到的内容

虽然我们是一个开始,但还有很多事要做。我们没有解决的事情是:

  • api版本控制。比如我们需要增加/api/v1/, /api/v2/等路由
  • 权限认证。除非这是一个免费/公开的API,否则我们可能需要一些身份验证。我推荐使用JSON Web Token

还有一些什么呢?

与所有项目一样,从小到大过程中慢慢就会失控。如果我要把它提升到一个新的水平,并且准备好生产,这些只是一些额外的事情:

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

我可以获取本博客的源码么?

源码地址:

CSDN:http://download.csdn.net/detail/yuan8222/9818336

github:https://github.com/corylanou/tns-restful-json-api

总结

对我来说最重要的是记住要建立一个负责任的API。发送正确的状态代码,头部标识等对于广泛采用API至关重要。我希望这篇文章可以让你开始自己的API!

原创文章,转载请注明: 转载自勤奋的小青蛙
本文链接地址: 【翻译】用Go开发RESTful JSON API(Making a RESTful JSON API in Go)

文章的脚注信息由WordPress的wp-posturl插件自动生成



|2|left
打赏

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: