程序员社区

【Go Web开发】格式化和封装响应

到目前为止,我们一直在使用Firefox向API服务发送请求,由于内置的JSON查看器提供了“输出格式化”,这使得JSON响应易于阅读。

但是如果您尝试使用curl发起请求,将看到实际的JSON响应数据都在一行中,没有空格。

$ curl localhost:4000/v1/healthcheck
{"environment":"development","status":"available","version":"1.0.0"}

$ curl localhost:4000/v1/movies/123
{"id":123,"title":"Casablanca","runtime":102,"genres":["drama","romance","war"],"version":1}

通过使用json.MarshalIndent()函数来编码响应数据,而不是使用常规的json.Marshal()函数,可以使这些内容更容易在终端中查看。自动将空格符添加到JSON输出中,每个元素放在单独的行,并在每个行前面加上可选的前缀和缩进字符。

我们更新writeJSON()助手来使用下面的代码:

File: cmd/api/helpers.go


package main

...

func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, headers http.header) error {
    //使用json.MarshalIndent()函数,可以在JSON输出中添加空格。这里使用""作为前缀,"\t"格式化元素
    js, err := json.MarshalIndent(data, "", "\t")
    if err != nil {
        return err
    }
    js = append(js, '\n')

    for key, value := range headers {
        w.Header()[key] = value
    }
  
    w.Header.Set("ContentType", "application/json")
    w.WriteHeader(status)
    w.Write(js)

    return nil
}

如果你重新启动API服务器并尝试从你的终端再次发出相同的请求,将会收到一些类似于以下内容,包含格式化空格的JSON响应:

$ curl -i localhost:4000/v1/healthcheck
{
    "environment": "development",
    "status": "available",
    "version": "1.0.0",
}

$ curl localhost:4000/v1/movies/123
{
    "id": 123,
    "title": "casablanca",
    "runtime": 102,
    "genres": [
        "drama",
        "romance",
        "war"
    ],
    "version": 1
}

性能比价

虽然从可读性和用户体验的角度来看,使用json.MarshalIndent()是更适合,但不幸的是,它不是免费的。除了响应的总字节数稍微大一点之外,Go为添加空格所做的额外工作对性能有显著的影响。

下面的基准测试使用gist中的代码演示json.Marshal()和json.MarshalIndent()的相对性能。

【Go Web开发】格式化和封装响应插图

在这些基准测试中,我们可以看到json.MarshalIndent()的运行时间比json.Marshal()长65%,使用的内存比json.Marshal()多30%,还多两次堆分配。这些数据会根据编码内容而变化,但根据我的经验,它们反映了性能影响。

但在大多数应用程序中这点性能不会造成担心。实际上,我们谈论的只是千分之一毫秒的时间——为了提高响应的可读性是值得这样做的。但是,如果你的API在资源非常受限的环境中运行,或者需要管理大级别的流量,那么这是需要注意的,您可能更倾向于使用json.Marshal()。

注意:JSON.marshalindent()的工作原理是像调用JSON.marshal()一样,然后通过独立的JSON.indent()函数为JSON添加空格。还有一个反向函数JSON.compact(),您可以使用它从JSON中删除空格。

封装响应

接下来,我们更新响应,使JSON数据始终包含在父JSON对象中。类似于:

{
    "movie":
    {
        "id": 123,
        "title": "Casablanca",
        "runtime": 102,
        "genres":
        [
            "drama",
            "romance",
            "war"
        ],
        "version": 1
    }
}

注意到电影对象是如何嵌套在键“movie”下面的,而不是本身作为顶层JSON对象。

严格来说,这样封装响应数据是没有必要的,是否选择这样做在一定程度上取决于你的编码风格和品味。但也有一些切实的好处:

1、在JSON的顶层包含一个键名(如“movie”)有助于使响应更具文档化。对于任何从上下文中看到响应的人来说,理解响应数据与什么相关更容易一些。

2、它降低了客户端出错的风险,因为这样做会避免把一个响应当作其他内容来处理。为了获得数据,客户端必须通过“movie”键显式地引用它。

3、如果我们总是封装API返回的数据,那么我们就减轻了在旧浏览器中返回JSON数组作为响应可能出现的安全漏洞。

我们可以使用一些技术来封装API响应,为了使事情简单,通过创建一个自定义的envelope map与底层类型map[string]interface{}来实现。

接下来将说明怎么封装响应:

从更新cmd/api/helper.go开始,如下所示:

File: cmd/api/helpers.go


package main

...

//定义envelop类型
type envelope map[string]interface{}

//将data参数类型改为envelope
func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.header) error {
    js, err := json.MarshalIndent(data, "", "\t")
    if err != nil {
        return err
    }

    js = append(js, '\n')

    for key, value := range headers {
        w.Header()[key] = value
    }

    w.Header.Set("ContentType", "application/json")
    w.WriteHeader(status)
    w.write(js)

    return nil
}

然后,我们需要更新showMovieHandler,以创建包含电影数据的envelope实例,并将其传递给writeJSON()函数,而不是直接传递电影结构体实例,如下所示:

File: cmd/api/movies.go


package main

...

func (app *application) showMovieHandler(w http.ResponseWriter, r http.Request) {
    id, err := app.readIDParam(r)
    if err != nil {
        http.NotFound(w, r)
        return
    }

    movie := data.Movie{
        ID:        id,
        CreateAt:  time.Now(),
        Title:     "Casablanca",
        Runtime:   102,
        Genres:    []string{"drama", "romance", "war"},
        Version:   1,
    }

    //创建envelope{"movie": movie}实例,然后传给writeJSON函数,而不是直接传movie结构体实例
    err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
    if err != nil {
        app.logger.Println(err)
        http.Error(w, "the server encountered a problem and could not process your request", http.StatusInternalServerError)
    }
}

还需要更新healthcheckHandler中的代码,以便它也将envelope类型传递给writeJSON()函数:

File: cmd/api/healthcheck.go


package main

...

func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
    //申明envelope类型包含响应数据。注意,我们构造它的方式意味着环境和版本数据现在将嵌套在JSON响应的system_info键下。
    env := envelope{
        "status": "available",
        "system_info": map[string]string{
            "environment": app.config.env,
            "version": version,
        },
    }
    err := app.writeJSON(w, http.StatusOK, env, nil)
    if err != nil {
        app.logger.Println(err)
        http.Error(w, "The server encountered a problem and could not process your request", http.StatusInternalServerError)
    }
}

好了,我们来试试这些修改。重新启动服务器,然后使用curl向API接口发出请求。您现在应该得到如下格式的响应。

$ curl localhost:4000/v1/movies/123
{
    "movie":
    {
        "id": 123,
        "title": "Casablanca",
        "runtime": 102,
        "genres":
        [
            "drama",
            "romance",
            "war"
        ],
        "version": 1
    }
}

$ curl localhost:4000/v1/healthcheck
{
    "status": "available",
    "system_info":
    {
        "environment": "development",
        "version": "1.0.0"
    }
}

附加内容

HTTP响应结构

要强调下构造JSON响应没有唯一正确或错误方法。有一些流行的格式,如JSON:API和jsend,您可能希望遵循或使用它们来获得灵感,但这当然不是必需的,大多数API不遵循这些格式。

无论你做什么,考虑格式化和在不同的API接口之间保持清晰和一致的响应结构都是很有必要的——特别是当它们需要对外开放。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 【Go Web开发】格式化和封装响应

一个分享Java & Python知识的社区