本节课工程结构如下:
(base) yanglei@yuanhong 02-bindJSON % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go
0 directories, 9 files
PART1. Body输入JSON的反序列化
JSON作为最为常见的输入格式,可以率先支持.其余的类似于XML或者protobuf都可以按照类似的思路支持.
其实这里就是实现一个将请求体中的JSON反序列化到一个给定的结构体实例上,并没有什么复杂的逻辑.
context.go
:
package bindJSON
import (
"encoding/json"
"errors"
"net/http"
)
// Context HandleFunc的上下文
type Context struct {
// Req 请求
Req *http.Request
// Resp 响应
Resp http.ResponseWriter
// PathParams 路径参数名值对
PathParams map[string]string
}
// BindJSON 绑定请求体中的JSON到给定的实例(这里的实例不一定是结构体实例,还有可能是个map)上
func (c *Context) BindJSON(target any) error {
if target == nil {
return errors.New("web绑定错误: 给定的实例为空")
}
if c.Req.Body == nil {
return errors.New("web绑定错误: 请求体为空")
}
decoder := json.NewDecoder(c.Req.Body)
return decoder.Decode(target)
}
这里需要说明的是,也可以使用json.Unmarshal()
来完成反序列化,但是相比于这个实现,多了一个步骤:需要将http.Request.Body
使用io.ReadAll()
将其内容读取为一个[]byte
.因为json.Unmarshal()
是不支持直接使用io.Reader
接口的实现作为入参的.两种实现方式的比对如下:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type User struct {
Id int `json:"id"`
}
func main() {
http.HandleFunc("/unmarshal", unmarshalHandle)
http.HandleFunc("/decoder", decoderHandle)
http.ListenAndServe(":8091", nil)
}
func unmarshalHandle(w http.ResponseWriter, r *http.Request) {
byteSlice, _ := io.ReadAll(r.Body)
err := json.Unmarshal(byteSlice, &User{})
if err != nil {
fmt.Fprintf(w, "decode failed: %v", err)
return
}
afterRead, _ := io.ReadAll(r.Body)
fmt.Fprintf(w, "after read: %s", string(afterRead))
}
func decoderHandle(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&User{})
if err != nil {
fmt.Fprintf(w, "decode failed: %v", err)
return
}
afterRead, _ := io.ReadAll(r.Body)
fmt.Fprintf(w, "after read: %s", string(afterRead))
}
很明显看到使用json.Unmarshal()
反序列化的实现多了一个读取的步骤.而这是没有太大意义的.
PART2. JSON输入控制选项
2.1 实现
在JSON的反序列化过程中,有2个选项(这2个选项的具体功能与示例见附录部分):
json.Decoder.DisallowUnknownFields()
那么问题来了,我们是否还要提供一个带有选项的JSON序列化方法(代码如下)?
// BindJSONOpt 绑定请求体中的JSON到给定的实例(这里的实例不一定是结构体实例,还有可能是个map)上
// 同时支持指定是否使用Number类型,以及是否禁止未知字段
func (c *Context) BindJSONOpt(target any, useNumber bool, disallowUnknownFields bool) error {
if target == nil {
return errors.New("web绑定错误: 给定的实例为空")
}
if c.Req.Body == nil {
return errors.New("web绑定错误: 请求体为空")
}
decoder := json.NewDecoder(c.Req.Body)
if useNumber {
decoder.UseNumber()
}
if disallowUnknownFields {
decoder.DisallowUnknownFields()
}
return decoder.Decode(target)
}
2.2 使用者的需求
严谨地讲,如果用户真的有这种需求,那么他可能需要的是:
2.2.1 控制整个应用
这种类似给整个框架提供一个配置.大致实现如下:
config.go
:
package bindJSON
type Config struct {
// UseNumber 反序列化JSON时是否使用Number类型
UseNumber bool
// DisallowUnknownFields 反序列化JSON时是否禁止未知字段
DisallowUnknownFields bool
}
2.2.2 控制单一HTTPServer实例
这种实现只需在HTTPServer
结构体上增加控制这2个选项的字段即可:
package bindJSON
// HTTPServer HTTP服务器
type HTTPServer struct {
router
// useNumber 反序列化JSON时是否使用Number类型
useNumber bool
// disallowUnknownFields 反序列化JSON时是否禁止未知字段
disallowUnknownFields bool
}
2.2.3 控制特定路径
例如针对所有在/user/
这个路径下进行的JSON反序列化操作,允许(拒绝)使用Number类型或允许(拒绝)出现结构体中未定义的字段
2.2.4 控制特定路由
例如针对/user/details
路由进行的JSON反序列化操作,允许(拒绝)使用Number类型或允许(拒绝)出现结构体中未定义的字段
2.3 结论
结论:在反序列化JSON时,完全不需要提供支持控制UseNumber()
和DisallowUnknownFields()
的API.
理由:对于绝大多数用户来说,他们不会尝试控制这2个选项.即使真的有这个需求,我们上述实现的Context.BindJSON()
逻辑较为简单,且代码量不大,完全可以让框架的使用者照抄这个方法,然后自行实现一个功能和上文中实现的Context.BindJSONOpt()
方法相同的方法.
如果Context.BindJSON()
被设计为支持提供控制UseNumber()
和DisallowUnknownFields()
选项的方法,那么就意味着所有用户在调用时都需要传递useNumber
和disallowUnknownFields
这两个实参.而实际上还是刚才那句话:对于绝大多数用户来说,他们不会尝试控制这2个选项.
记住,设计中间件时,要解决大部分人的需求.这里所谓的"大部分人的需求",其实就是根据自己的使用经验去猜测用户会如何使用自己的中间件,最终得出的一个结论.
在设计API时,要控制住一个"度".换言之,如果用户有一些小众的需求,不是不能支持,而是要在实现小众需求不影响核心的前提下实现这些小众的需求.
更不能让大多数人为小部分人付出代价.因为有些小众的需求实现起来会非常耗时,实现这种需求的代码就不要放到主流程代码中,将这些实现小众需求的代码挪出来.或者说,如果为了支持一个小众的需求,反而会影响到大部分主流用户的使用,那么就不要支持这个小众的需求.
更进一步地讲:如果一个小众需求,用户可以自己解决,那么就不要在框架核心上支持.要克制自己!
附录
附录1:json.Decoder.UseNumber()
1.1 功能说明
在Go语言中的encoding/json
包中,json.Decoder
类型的UseNumber()
方法是用来指导Decoder
在解码JSON数据时如何处理数字.默认情况下,当Decoder
遇到一个数字时,它会将该数字解码为float64
.但是,如果UseNumber()
被调用,Decoder
将代替将数字解码为json.Number
类型.
json.Number
是一个字符串类型,这意味着数字在解码过程中不会失去精度.这在处理大整数和精确的小数点数值时特别有用,因为直接解码为float64
可能会因为精度限制而丢失信息.例如,一个非常大的整数可能比float64
能精确表示的最大整数还要大,或者一个小数可能需要比float64
能提供的精度更高的精度.
当使用json.Number
时,你可以稍后将这个值转换为你想要的确切数字类型,如int64
、float64
或者你自己的自定义数字类型,这样可以确保在转换过程中控制精度和范围.
简单来说,UseNumber()
允许你更灵活和精确地处理JSON中的数字,防止在解码过程中出现不必要的精度损失.
1.2 示例
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"math/big"
)
var (
jsonBlob = []byte(`{"int_max":9223372036854775807}`)
)
func main() {
noUseNumber()
useNumber()
}
// noUseNumber 不使用`UseNumber`选项进行JSON反序列化
func noUseNumber() {
data := make(map[string]any)
if err := json.Unmarshal(jsonBlob, &data); err != nil {
log.Fatal(err)
}
// 输出一个浮点数 这个浮点数可能会出现精度丢失的现象
fmt.Println(data["int_max"])
}
// useNumber 使用`UseNumber`选项进行JSON反序列化
func useNumber() {
data := make(map[string]json.Number)
decoder := json.NewDecoder(bytes.NewReader(jsonBlob))
decoder.UseNumber()
if err := decoder.Decode(&data); err != nil {
log.Fatal(err)
}
fmt.Println(data["int_max"])
// 将这个json.Number类型的值安全的转换为更精确的数字类型
intValue, ok := new(big.Int).SetString(data["int_max"].String(), 10)
if !ok {
log.Fatal("Big int conversion failed")
}
// 输出一个精确的大整数
fmt.Printf("The big int is: %d\n", intValue)
}
运行结果:
(base) yanglei@yuanhong 8-useNumber % go run useNumber.go
9.223372036854776e+18
9223372036854775807
The big int is: 9223372036854775807
附录2:json.Decoder.DisallowUnknownFields()
2.1 功能说明
在Go语言中,json.Decoder
的DisallowUnknownFields()
方法的作用是设置Decoder
在解码JSON数据时,不允许出现结构体中未定义的字段.如果设置了这个方法,当Decoder
遇到结构体中没有定义的字段时,它将返回一个错误.
这个方法对于确保JSON数据的格式严格符合预期的结构体非常有用,它可以防止因为JSON中的意外字段而导致的潜在错误,并确保数据的解码不会静默忽略任何字段.
2.2 示例
package main
import (
"encoding/json"
"log"
"strings"
)
var jsonStr = `{"knownField":"value", "unknownField":"should cause error"}`
type MyStruct struct {
KnownField string `json:"knownField"`
}
func main() {
noDisallowUnknownFields()
disallowUnknownFields()
}
// noDisallowUnknownFields 不使用`DisallowUnknownFields`选项进行JSON反序列化
func noDisallowUnknownFields() {
myStruct := &MyStruct{}
err := json.Unmarshal([]byte(jsonStr), myStruct)
if err != nil {
log.Fatal("Unmarshal error:", err)
}
log.Printf("Unmarshal success: %+v\n", myStruct)
}
// disallowUnknownFields 使用`DisallowUnknownFields`选项进行JSON反序列化
func disallowUnknownFields() {
myStruct := &MyStruct{}
decoder := json.NewDecoder(strings.NewReader(jsonStr))
decoder.DisallowUnknownFields()
err := decoder.Decode(myStruct)
if err != nil {
// 这里将输出错误 因为JSON中包含了MyStruct没有定义的unknownField
log.Fatal("Decode error:", err)
}
log.Printf("Decode success: %+v\n", myStruct)
}
运行结果:
(base) yanglei@yuanhong 9-disallowUnknownFields % go run disallowUnknownFields.go
2023/11/16 00:19:54 Unmarshal success: &{KnownField:value}
2023/11/16 00:19:54 Decode error:json: unknown field "unknownField"
exit status 1
在这个例子中,尝试解码包含未知字段unknownField
的JSON字符串将会失败,并返回一个错误,因为MyStruct
结构体中只定义了KnownField
字段.如果你没有调用DisallowUnknownFields()
,则未知字段会被解码过程中忽略掉,并且不会报错.