5.06 Context-处理输入之Body输入

本节课工程结构如下:

(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.UseNumber()

  • 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()选项的方法,那么就意味着所有用户在调用时都需要传递useNumberdisallowUnknownFields这两个实参.而实际上还是刚才那句话:对于绝大多数用户来说,他们不会尝试控制这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时,你可以稍后将这个值转换为你想要的确切数字类型,如int64float64或者你自己的自定义数字类型,这样可以确保在转换过程中控制精度和范围.

简单来说,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.DecoderDisallowUnknownFields()方法的作用是设置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(),则未知字段会被解码过程中忽略掉,并且不会报错.

Last updated