# 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`:

```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`接口的实现作为入参的.两种实现方式的比对如下:

```go
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序列化方法(代码如下)?

```go
// 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`:

```go
package bindJSON

type Config struct {
	// UseNumber 反序列化JSON时是否使用Number类型
	UseNumber bool
	// DisallowUnknownFields 反序列化JSON时是否禁止未知字段
	DisallowUnknownFields bool
}

```

#### 2.2.2 控制单一HTTPServer实例

这种实现只需在`HTTPServer`结构体上增加控制这2个选项的字段即可:

```go
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`](https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L189)类型.

`json.Number`是一个字符串类型,这意味着数字在解码过程中不会失去精度.这在处理大整数和精确的小数点数值时特别有用,因为直接解码为`float64`可能会因为精度限制而丢失信息.例如,一个非常大的整数可能比`float64`能精确表示的最大整数还要大,或者一个小数可能需要比`float64`能提供的精度更高的精度.

当使用`json.Number`时,你可以稍后将这个值转换为你想要的确切数字类型,如`int64`、`float64`或者你自己的自定义数字类型,这样可以确保在转换过程中控制精度和范围.

简单来说,`UseNumber()`允许你更灵活和精确地处理JSON中的数字,防止在解码过程中出现不必要的精度损失.

#### 1.2 示例

```go
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 示例

```go
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()`,则未知字段会被解码过程中忽略掉,并且不会报错.
