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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://go.sai.show/part05.context/5.06-context-chu-li-shu-ru-zhi-body-shu-ru.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
