02.课后复习-Context

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

context.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:

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:

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:


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:

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:

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:

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:

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:

package web

import "strconv"

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

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

reqValue.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:

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:

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

Last updated