# 8.02 课后复习-Context

## PART1. 对Body的输入进行JSON反序列化

`context.go`:

```go
package web

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()`,是因为用这个API多一步将`io.Reader`接口转换为`[]byte`的过程:

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
)

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
}

// JSONUnmarshal 绑定请求体中的JSON数据到给定的目标对象上 这个目标对象可能是某个结构体的实例 也有可能是个map
// Tips: 该方法与BindJSON方法的区别在于该方法使用了 json.Unmarshal 方法
func (c *Context) JSONUnmarshal(target any) error {
	if target == nil {
		return errors.New("web绑定错误: 给定的实例为空")
	}

	if c.Req.Body == nil {
		return errors.New("web绑定错误: 请求体为空")
	}

	bytes, err := io.ReadAll(c.Req.Body)
	if err != nil {
		return err
	}

	return json.Unmarshal(bytes, target)
}
```

## PART2. 处理表单输入

1. 无论你后续准备使用`http.Request.Form`还是`http.Request.PostForm`,都得先调用`http.ParseForm()`方法解析表单数据,这俩字段才有值
2. `http.Request.FormValue()`方法内部虽然会调用`http.ParseForm()`方法解析表单数据,但是这个方法不抛出error,所以最好事前手动调用一次
3. 我在复习时,将检测给定的key是否存在也作为了一个判断错误的条件

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
)

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
}

// FormValue 获取表单中给定的key对应的值
func (c *Context) FormValue(key string) (value string, err error) {
	err = c.Req.ParseForm()
	if err != nil {
		return "", errors.New("web绑定错误: 解析表单失败: " + err.Error())
	}

	_, ok := c.Req.Form[key]
	if !ok {
		return "", errors.New("web绑定错误: 表单中没有给定的key: " + key)
	}

	return c.Req.FormValue(key), nil
}
```

## PART3. 处理查询参数

### 3.1 基本实现

`context.go`:

```gopackage

import (
	"encoding/json"
	"errors"
	"net/http"
)

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
}

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	return c.Req.URL.Query().Get(key), nil
}
```

### 3.2 缓存查询参数

多次调用`http.Request.URL.Query()`会重复解析URL,因此需要将查询参数缓存起来

注意,这里缓存参数时,不要使用`map[string][]string`的数据类型,虽然它和`url.Values`是同一种类型,因为后边的代码写的会非常难受:

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
)

// Context HandleFunc的上下文
type Context struct {
	Req        *http.Request       // Req 请求
	Resp       http.ResponseWriter // Resp 响应
	PathParams map[string]string   // PathParams 路径参数名值对
	queryValue map[string][]string // queryValue 查询参数名值对
}

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	values, ok := c.queryValue[key]
	if !ok {
		return "", errors.New("web绑定错误: URL中没有给定的key: " + key)
	}

	if len(values) == 0 {
		return "", errors.New("web绑定错误: URL中给定的key对应的值为空")
	}

	return values[0], nil
}
```

就保持和API一致,还使用`url.Values`即可.原因有2个:

1. 这里将`queryValue`设置成私有字段,意味着它的作用域不会离开`web`包,所以它和框架使用者的代码是隔离的,只能通过`Context. QueryValue()`方法来访问`queryValue`中的K-V(确切的说只能访问V)
2. 后续通过Key查找Value时,`url.Values`类型是有API的(当然最终的版本并没有使用这个API),而如果使用`map[string][]string`,则必须自行判断:
   * key是否存在
   * key对应的value是否为空slice

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

// Context HandleFunc的上下文
type Context struct {
	Req        *http.Request       // Req 请求
	Resp       http.ResponseWriter // Resp 响应
	PathParams map[string]string   // PathParams 路径参数名值对
	queryValue url.Values          // queryValue 查询参数名值对
}

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	return c.queryValue.Get(key), nil
}
```

这里需要注意的是,这个缓存是不存在失效和不一致的问题的.因为一旦服务端接收到一个请求,那么这个请求的参数就是固定的了

### 3.3 无法区分key不存在的情况与key对应的参数值是空字符串的情况

注意`url.Values.Get()`方法的返回值类型为string,所以即使没有找到key对应的查询参数,它依然返回一个空字符串.所以其实到底用`url.Values`还是`map[string][]string`其实区别并不大

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

// Context HandleFunc的上下文
type Context struct {
	Req        *http.Request       // Req 请求
	Resp       http.ResponseWriter // Resp 响应
	PathParams map[string]string   // PathParams 路径参数名值对
	queryValue url.Values          // queryValue 查询参数名值对
}

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	values, ok := c.queryValue[key]
	if !ok {
		return "", errors.New("web绑定错误: URL中没有给定的key: " + key)
	}

	if len(values) == 0 {
		return "", errors.New("web绑定错误: URL中给定的key没有对应的值: " + key)
	}

	return values[0], nil
}
```

## PART4. 处理路径参数

路径参数在查找路由树时就已经存起来了,这里只需要取就行

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

// Context HandleFunc的上下文
type Context struct {
	Req        *http.Request       // Req 请求
	Resp       http.ResponseWriter // Resp 响应
	PathParams map[string]string   // PathParams 路径参数名值对
	queryValue url.Values          // queryValue 查询参数名值对
}

// PathValue 获取路径参数中给定的key对应的值
func (c *Context) PathValue(key string) (value string, err error) {
	if c.PathParams == nil {
		return "", errors.New("web绑定错误: 路径参数为空")
	}

	value, ok := c.PathParams[key]
	if !ok {
		return "", errors.New("web绑定错误: 路径中没有给定的key: " + key)
	}

	return value, nil
}
```

## PART5. 返回不同数据类型的输入

思路:定义一个结构体,该结构体用于将string类型的输入转换为不同的类型;以上所有方法不再直接返回string类型的值,而是返回一个该结构体idea实例.**注意这里返回的是实例而非指针,因为这个结构体中的属性不该被修改,换言之该结构体是"只读"的**.

### 5.1 定义ReqValue结构体

`reqValue.go`:

```go
package web

import "strconv"

// ReqValue 用于承载来自请求中各部分输入的值 并提供统一的类型转换API
type ReqValue struct {
	value string // value 来自请求中不同部分的值 以string类型表示
	err   error  // err 承载接收请求中的参数时出现的错误
}
```

### 5.2 定义各种类型转换的方法

`reqValue.go`:

```go
package web

import "strconv"

// ReqValue 用于承载来自请求中各部分输入的值 并提供统一的类型转换API
type ReqValue struct {
	value string // value 来自请求中不同部分的值 以string类型表示
	err   error  // err 承载接收请求中的参数时出现的错误
}

// AsInt64 将ReqValue中的值转换为int64类型
func (r ReqValue) AsInt64() (value int64, err error) {
	if r.err != nil {
		return 0, r.err
	}

	return strconv.ParseInt(r.value, 10, 64)
}

// AsUint64 将ReqValue中的值转换为uint64类型
func (r ReqValue) AsUint64() (value uint64, err error) {
	if r.err != nil {
		return 0, r.err
	}

	return strconv.ParseUint(r.value, 10, 64)
}

// AsFloat64 将ReqValue中的值转换为float64类型
func (r ReqValue) AsFloat64() (value float64, err error) {
	if r.err != nil {
		return 0, r.err
	}

	return strconv.ParseFloat(r.value, 64)
}
```

### 5.3 context的各个处理请求参数的方法中返回ReqValue

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

// Context HandleFunc的上下文
type Context struct {
	Req        *http.Request       // Req 请求
	Resp       http.ResponseWriter // Resp 响应
	PathParams map[string]string   // PathParams 路径参数名值对
	queryValue url.Values          // queryValue 查询参数名值对
}

// 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)
}

// FormValue 获取表单中给定的key对应的值
func (c *Context) FormValue(key string) (value ReqValue) {
	err := c.Req.ParseForm()
	if err != nil {
		return ReqValue{err: err}
	}

	_, ok := c.Req.Form[key]
	if !ok {
		return ReqValue{err: errors.New("web绑定错误: 表单中没有给定的key: " + key)}
	}

	return ReqValue{value: c.Req.FormValue(key)}
}

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value ReqValue) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	values, ok := c.queryValue[key]
	if !ok {
		return ReqValue{err: errors.New("web绑定错误: URL中没有给定的key: " + key)}
	}

	if len(values) == 0 {
		return ReqValue{err: errors.New("web绑定错误: URL中给定的key没有对应的值: " + key)}
	}

	return ReqValue{value: values[0]}
}

// PathValue 获取路径参数中给定的key对应的值
func (c *Context) PathValue(key string) (value ReqValue) {
	if c.PathParams == nil {
		return ReqValue{err: errors.New("web绑定错误: 路径参数为空")}
	}

	val, ok := c.PathParams[key]
	if !ok {
		return ReqValue{err: errors.New("web绑定错误: 路径中没有给定的key: " + key)}
	}

	return ReqValue{value: val}
}
```

### 5.4 测试用例

`context_test.go`:

```go
package web

import (
	"fmt"
	"net/http"
	"testing"
)

func Test_Context(t *testing.T) {
	server := &HTTPServer{router: newRouter()}

	handleFunc := func(c *Context) {
		id, err := c.PathValue("id").AsInt64()
		if err != nil {
			c.Resp.WriteHeader(http.StatusBadRequest)
			_, _ = c.Resp.Write([]byte("id输入不正确: " + err.Error()))
		}

		_, _ = c.Resp.Write([]byte(fmt.Sprintf("id: %d", id)))
	}

	server.GET("/order/:id", handleFunc)
	_ = server.Start(":8091")
}
```


---

# 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/part08.review/8.02-ke-hou-fu-xi-context.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.
