解析JSON请求
到目前为止,我们一直在研究如何从我们的API中创建和发送JSON响应,在本文中,我们将从另一方面探索,讨论如何读取和解析来自客户端的JSON请求。
为了帮助说明这一点,我们将从POST /v1/movies接口和之前设置的createMovieHandler上开始工作。
Method | URL | Handler | 动作 |
---|---|---|---|
GET | /v1/healthcheck | healthcheckHanlder | 查询应用程程序信息 |
POST | /v1/movies | createMovieHandler | 创建新的电影 |
GET | /v1/movies/:id | showMovieHandler | 查询特定电影详情 |
当客户端调用这个接口时,我们希望它们提供一个JSON请求体,其中包含想要在我们的系统中创建的电影的数据。例如,如果客户端想要为电影Moana添加一条记录到我们的API中,会发送一个类似于这样的请求体:
{
"title": "Moana",
"year": 2016,
"runtime": 107,
"genres":
[
"animation",
"adventure"
]
}
现在,我们只关注处理这个JSON请求体的读取、解析和验证。接下来你将学习:
- 如何使用encoding/json包读取请求体并将其反序列化为本地Go对象。
- 如何处理来自客户端的错误请求和无效的JSON,并返回清晰的、可操作的错误消息。
- 如何创建可重用的辅助程序来验证数据,以确保数据符合业务规则。
- 控制和定制JSON解码方式的不同技术。
JSON解码(反序列化)
和JSON编码一样,有两种方式可以用于将JSON解码为Go对象:使用json.Decoder类型和json.Unmarshal()函数。
这两种方法各有优缺点,但为了从HTTP请求体解码JSON,使用JSON.Decoder通常是最好的选择。它比json.Unmarshal()更高效,需要更少的代码,并提供了一些有用的设置,您可以使用这些设置来调整其行为。
用代码说明json.Decoder是如何工作会更简单,所以让我们直接进入代码,更新createMovieHandler处理函数:
File: cmd/api/movies.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"greenlight.alexedwards.net/internal/data"
)
func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
//申明一个匿名结构体来接收HTTP请求体中对JSON内容,注意结构体中字段和类型与之前创建movie结构体只包含部分字段
//该结构体定义类型用于接收http请求,并解码为Go对象。
var input struct {
Title string `json:"title"`
Year int32 `json:"year"`
Runtime int32 `json:"runtime"`
Genres []string `json:"genres"`
}
//初始化json.Decoder实例,从http请求body中读取请求内容,然后使用Decode()方法将内容解析为input结构体。
//注意Decoder函数接收对是指针类型,如果解析错误就调用errorResponse()帮助函数返回400错误给客户端。
err := json.NewDecoder(r.Body).Decode(&input)
if err != nil {
app.errorResponse(w, r, http.StatusBadRequest, err.Error())
return
}
//将解析后对input结构体写入HTTP响应,返回给客户端
fmt.Fprintf(w, "%+v\n", input)
...
}
关于这段代码,有一些重要的地方需要指出:
- 当调用Decoder()必须传入一个非nil指针作为解析对象的存储位置。如果传入的不是指针,运行时将返回json.InvalidUnmarshalError错误。
- 如果传入的是结构体,像上面代码中的那样,结构体字段必须首字母大写。和编码一样,它们需要被导出,这样才能使它们对encoding/json包是可见的。
- 当将JSON对象解码为结构体时,JSON中的键/值对将基于结构体标签名映射到结构体字段。如果没有匹配的结构体标签,Go将试图将对应的JSON值编码到匹配对结构体字段中,不区分大小写的匹配)。任何不能成功映射到结构体字段的JSON键/值对都将被忽略。
- 在r.Body被读取之后,没有必要关闭它。这将由Go的http.Server自动处理。
好吧,我们来试试。
启动应用程序,然后打开第二个终端窗口,向POST /v1/ movies发出请求,其中包含一些movie数据。你应该会看到类似这样的响应:
#创建一BODY变量,包含要发送对JSON内容
$ BODY='{"title":"Moana","year":2016,"runtime":107, "genres":["animation","adventure"]}'
#使用-d命令行参数将BODY内容发送给服务端
$ curl -i -d "$BODY" localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 17:13:46 GMT Content-Length: 65
Content-Type: text/plain; charset=utf-8
{Title:Moana Year:2016 Runtime:107 Genres:[animation adventure]}
太棒了!似乎很有效。从响应数据可以看出,我们在请求体中提供的值已经被解码到input结构体的对应字段中。
零值
让我们快速看一下如果我们在JSON请求体中忽略特定的键/值对会发生什么。例如,在JSON中创建一个没有year字段的请求,如下所示:
$ BODY='{"title":"Moana","runtime":107, "genres":["animation","adventure"]}'
$ curl -d "$BODY" localhost:4000/v1/movies
{Title:Moana Year:0 Runtime:107 Genres:[animation
正如您可能已经猜到的,当我们这样做时,输入结构中的Year字段将保留其零值(碰巧是0,因为Year字段是一个int32类型)。
这就引出了一个有趣的问题:如何区分客户端不提供键/值对和提供键/值对但故意将其设置为零的情况?例如:
$ BODY='{"title":"Moana","year":0,"runtime":107, "genres":["animation","adventure"]}'
$ curl -d "$BODY" localhost:4000/v1/movies
{Title:Moana Year:0 Runtime:107 Genres:[animation adventure]}
尽管HTTP请求不同,但最终结果是相同的,并且如何区分这两种场景并不是很明显。我们后面再回到这个话题,但现在,只需要了解这个特殊情况就够了。
附加内容
解码支持的目标类型
值得一提的是,某些JSON类型只能成功解码为某些Go类型。例如,如果你有JSON字符串“foo”,它可以被解码成一个Go字符串,但试图将其解码成一个Go int或bool将导致运行时错误(我们将在下一节中演示)。
下表提供了不同JSON类型支持解码为对应的GO类型:
JSON 类型 | 支持的Go类型 |
---|---|
JSON boolean | bool |
JSON string | string |
JSON number | int, uint, float*, rune |
JSON array | array, slice |
JSON object | struct, map |
使用json.Unmarshal函数
正如我们在本节开始时提到的,也可以使用json.Unmarshal()函数来解码HTTP请求体。
例如,你可以像这样在处理程序中使用它:
func (app *application) exampleHandler(w http.ResponseWriter, r *http.Request) {
var input struct {
Foo string `json:"foo"`
}
//使用io.ReadAll()读取整个HTTP请求body内容到[]byte切片中
body, err := io.ReadAll(r.Body)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
//使用json.Unmarshal()函数将切片中的JSON解码到input结构体。再次说明使用的参数是指针。
err = json.Unmarshal(body, &input)
if err != nil {
app.errorResponse(w, r, http.StatusBadRequest, err.Error())
}
fmt.Fprintf(w, "%+v\n", input)
...
}
使用这种方法很简单。但没有我们之前提到的json.Decoder方法中的优点。
不仅代码稍微更冗长,而且效率也更低。如果我们对这个特定用例的相对性能进行基准测试,可以看到使用json. unmarshal()比json.Decoder多损耗80%的内存(B/op)。以及稍微慢一点(ns/op)。
网友评论