使用Go构建RESTful的JSON API

原文地址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 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。

增加一个路由

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

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

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安装所有未安装的依赖包。

译者注:

也可以使用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

时间: 2024-11-10 00:22:52

使用Go构建RESTful的JSON API的相关文章

spring boot 1.5.4 集成Swagger2构建Restful API(十八)

上一篇博客地址:springboot 1.5.4 整合rabbitMQ(十七) 1      Spring Boot集成Swagger2构建RESTful API文档 1.1  Swagger2简介 Swagger2官网:http://swagger.io/ 由于Spring Boot能够快速开发.便捷部署等特性,相信有很大一部分Spring Boot的用户会用来构建RESTful API.而我们构建RESTful API的目的通常都是由于多终端的原因,这些终端会共用很多底层业务逻辑,因此我们会

使用ASP.NET Core 3.x 构建 RESTful API P6 状态和路由

使用ASP.NET Core 3.x 构建 RESTful API P6 状态和路由 HTTP状态路由 在 .Net Core Web API 项目中,Controller 层是对外层,所以在 Controller 层之下的其它层(如:业务逻辑层,数据库访问层)是如何运作的,与 Controller层无关,所以针对业务结果,在 Controller 层对外表述的时候,我们需要根据也业务结果给出,具体的 HTTP 状态码. 分析一个 Action 方法,此 Action 存在于 Companies

Spring MVC中使用 Swagger2 构建Restful API

1.maven依赖 <!-- 构建Restful API --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.6.0</version> </dependency> <dependency> <groupId>io.spr

gRPC helloworld service, RESTful JSON API gateway and swagger UI

概述 本篇博文完整讲述了如果通过 protocol buffers 定义并启动一个 gRPC 服务,然后在 gRPC 服务上提供一个 RESTful JSON API 的反向代理 gateway,最后通过 swagger ui 来提供 RESTful JSON API 的说明,完整代码 helloworld_restful_swagger. Helloworld gRPC Service 参考 gRPC Quick Start for Python. Install gRPC 安装 gRPC 运

springboot集成swagger2构建RESTful API文档

在开发过程中,有时候我们需要不停的测试接口,自测,或者交由测试测试接口,我们需要构建一个文档,都是单独写,太麻烦了,现在使用springboot集成swagger2来构建RESTful API文档,可以在访问接口上,直接添加注释 先介绍一下开发环境: jdk版本是1.8 springboot的版本是1.4.1 开发工具为 intellij idea 我们先引入swagger2的jar包,pom文件引入依赖如下: <dependency> <groupId>io.springfox&

JSON API:用 JSON 构建 API 的标准指南中文版

译文地址:https://github.com/justjavac/json-api-zh_CN 如果你和你的团队曾经争论过使用什么方式构建合理 JSON 响应格式, 那么 JSON API 就是你的 anti-bikeshedding 武器. 通过遵循共同的约定,可以提高开发效率,利用更普遍的工具,可以是你更加专注于开发重点:你的程序. 基于 JSON API 的客户端还能够充分利用缓存,以提升性能,有时甚至可以完全不需要网络请求. 下面是一个使用 JSON API 发送响应(response

企业分布式微服务云SpringCloud SpringBoot mybatis (二十一)构建restful API

引入依赖 在pom文件引入mybatis-spring-boot-starter的依赖: <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter<artifactId> <version>1.3.0</version> </dependency> 引入数据库连接依赖: <

(3)集成swagger2构建Restful API

在taosir父目录的pom.xml中进行版本管理 <swagger.version>2.8.0</swagger.version> 给taosir-api的pom.xml中添加依赖配置 <!-- swagger start --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> &

集成swagger2构建Restful API

集成swagger2构建Restful API 在pom.xml中进行版本管理 <swagger.version>2.8.0</swagger.version> 给taosir-api的pom.xml中添加依赖配置 <!-- swagger start --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</ar