为你自己学Go
  • README
  • PART01.Web框架概览
    • 1.01 Web框架概览:学习路线
    • 1.02 Web框架概览-Beego框架分析
    • 1.03 Web框架概览-GIN框架分析
    • 1.04 Web框架概览-Iris框架分析
    • 1.05 Web框架概览-Echo框架分析
  • PART02.Server
    • 2.01 Server详解与面试要点
  • PART03.路由树
    • 3.01 路由树-Beego&GIN&Echo实现与设计总结
    • 3.02 路由树-全静态匹配
    • 3.03 路由树-TDD起步
    • 3.04 路由树-静态匹配测试用例
    • 3.05 路由树-静态匹配之路由查找
    • 3.06 路由树-静态匹配之集成Server
    • 3.07 路由树-通配符匹配之路由注册
    • 3.08 路由树-通配符匹配之路由查找与测试
    • 3.09 路由树-参数路径之基本注册和查找
    • 3.10 路由树-参数路径之校验
    • 3.11 路由树-参数路径之参数值
    • 3.12 路由树-总结与面试要点
  • PART04.课后复习
    • 4.01 课后复习-Server
    • 4.02 课后复习-Route
  • PART05.Context
    • 5.01 Context-简介
    • 5.02 Context-Beego Context设计分析
    • 5.03 Context-Gin Context设计分析
    • 5.04 Context-Echo和Iris的Context设计分析
    • 5.05 Context-处理输入输出总结
    • 5.06 Context-处理输入之Body输入
    • 5.07 Context-处理输入之表单输入
    • 5.08 Context-处理输入之查询参数、路径参数和StringValue
    • 5.09 Context-处理输出
    • 5.10 Context-总结与面试要点
  • PART06.AOP
    • 6.01 AOP简介与不同框架设计概览
    • 6.02 AOP设计方案-Middleware
  • PART07.Middleware
    • 7.01 Middleware-AccessLog
    • 7.02 Middleware-Trace简介和OpenTelemetry
    • 7.03 Middleware-OpenTelemetry测试
    • 7.04 Middleware-OpenTelemetry总结
    • 7.05 Prometheus详解
    • 7.06 Middleware-Prometheus
    • 7.07 Middleware-错误页面
    • 7.08 Middleware-从panic中恢复
    • 7.09 Middleware总结和面试
  • PART08.Review
    • 8.01 课后复习-AOP
    • 8.02 课后复习-Context
    • 8.03 课后复习-Middleware-AccessLog
  • PART09.Appendix
    • 附录1.责任链模式
    • 附录2.生成器模式
    • 附录3.函数选项模式
  • xiaochengxu
    • 01.原力去水印
    • 02.KeePass密码管理:安全轻松的管理您的密码
Powered by GitBook
On this page
  • PART1. 对Body的输入进行JSON反序列化
  • PART2. 处理表单输入
  • PART3. 处理查询参数
  • 3.1 基本实现
  • 3.2 缓存查询参数
  • 3.3 无法区分key不存在的情况与key对应的参数值是空字符串的情况
  • PART4. 处理路径参数
  • PART5. 返回不同数据类型的输入
  • 5.1 定义ReqValue结构体
  • 5.2 定义各种类型转换的方法
  • 5.3 context的各个处理请求参数的方法中返回ReqValue
  • 5.4 测试用例
  1. PART08.Review

8.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 9 months ago