为你自己学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. 处理查询参数
  • 1.1 问题1:多次调用是否会重复解析?
  • 1.2 问题2:无法区分给定的key到底是不存在,还是存在但值为空?
  • 1.3 问题3:是否需要提供返回其他数据类型的API?
  • PART2. 处理路径参数
  • 2.1 问题1:是否需要提供返回其他数据类型的API?
  • PART3. 返回不同数据类型的输入
  • 3.1 定义用于接收来自Context输入的类型
  • 3.2 修改Context中处理各部分输入的方法的返回值
  • 3.2.2 Context.QueryValue()方法
  • 3.2.3 Context.PathValue()方法
  • 3.3 新增将输入参数转换为对应类型表达的方法
  • 3.3.3 StringValue.AsFloat64()
  • 3.4 在handleFunc中使用
  1. PART05.Context

5.08 Context-处理输入之查询参数、路径参数和StringValue

PART1. 处理查询参数

本部分工程结构如下:

(base) yanglei@bogon 04-queryValue % tree ./
./
├── context.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go

0 directories, 11 files

查询参数:即就是指在URL问号之后的部分.

例如:http://localhost:8081/form?name=xiaoming&age=18.那么查询参数即为:

  • name=xiaoming

  • age=18

在之前处理表单处理的课程中我们可知,调用http.Request.ParseForm()方法,可以找到这部分参数值.

乍一看这个需求实现起来还是比较简单的:

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	return c.Req.URL.Query().Get(key), nil
}

1.1 问题1:多次调用是否会重复解析?

package queryValue

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

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	return c.queryValues.Get(key), nil
}

注:

  1. 此处删除了和本节课无关的代码

  2. 这里的设计类似GIN框架,引入了一个查询参数缓存.这个缓存是不存在所谓的缓存失效或缓存不一致问题的,因为对于Web框架而言,请求收到之后,请求参数就是确切无疑不会再变的

1.2 问题2:无法区分给定的key到底是不存在,还是存在但值为空?

但是这个实现有一个问题:无法区分给定的key到底是不存在,还是存在但值为空

举例说明:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/queryValue", queryValue)
	http.ListenAndServe(":8091", nil)
}

func queryValue(w http.ResponseWriter, r *http.Request) {
	// 获取查询字符串中的name参数
	name := r.URL.Query().Get("name")
	fmt.Fprintf(w, "name %s", name)
}

请求URL为127.0.0.1:8091/queryValue时,结果如下:

请求URL为127.0.0.1:8091/queryValue?name=时,结果如下:

package queryValue

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

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	values, ok := c.queryValues[key]
	if !ok {
		return "", errors.New("web绑定错误: 查询参数中不存在键: " + key)
	}

	return values[0], nil
}

这里可能会有人问:为什么访问这个map之前不需要判空?

如果再严格一些,可以再加一个对c.queryValues长度是否为0的判断,以便判断是否存在查询参数:

package queryValue

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

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	if len(c.queryValues) == 0 {
		return "", errors.New("web绑定错误: 无任何查询参数")
	}

	values, ok := c.queryValues[key]
	if !ok {
		return "", errors.New("web绑定错误: 查询参数中不存在键: " + key)
	}

	return values[0], nil
}

1.3 问题3:是否需要提供返回其他数据类型的API?

对于查询参数的读取,也面临着和读取表单参数一样的问题:是否需要提供返回其他数据类型的API?

例如:

package queryValue

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

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	if len(c.queryValues) == 0 {
		return "", errors.New("web绑定错误: 无任何查询参数")
	}

	values, ok := c.queryValues[key]
	if !ok {
		return "", errors.New("web绑定错误: 查询参数中不存在键: " + key)
	}

	return values[0], nil
}

// QueryValueAsInt64 获取查询字符串中给定键的值并返回其int64表示
func (c *Context) QueryValueAsInt64(key string) (intValue int64, err error) {
	value, err := c.QueryValue(key)
	if err != nil {
		return 0, err
	}
	return strconv.ParseInt(value, 10, 64)
}

同样问题:如果需要提供,那就还要提供很多其他数据类型的API.

这里的答案同样是不需要的.后续我们统一解决这个问题.

PART2. 处理路径参数

本章工程结构如下:

(base) yanglei@bogon 05-pathValue % tree ./
./
├── context.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go

0 directories, 11 files

这里基本上和前面的表单、查询参数没太大的区别

package pathValue

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

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

// PathValue 获取路径参数中给定键的值
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)
	}

	return value, nil
}

注:

  1. 此处删除了和本节课无关的代码

2.1 问题1:是否需要提供返回其他数据类型的API?

对于路径参数的读取,也面临着和查询参数、读取表单参数一样的问题:是否需要提供返回其他数据类型的API?

例如:

package pathValue

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

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

// PathValue 获取路径参数中给定键的值
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)
	}

	return value, nil
}

// PathValueAsInt64 获取路径参数中给定键的值并返回其int64表示
func (c *Context) PathValueAsInt64(key string) (intValue int64, err error) {
	value, err := c.PathValue(key)
	if err != nil {
		return 0, err
	}

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

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

本章工程结构如下:

(base) yanglei@bogon 06-stringValue % tree ./
./
├── context.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go

0 directories, 11 files

3.1 定义用于接收来自Context输入的类型

新建文件stringValue.go:

package stringValue

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

整体的思路就是:Context中处理各部分输入的方法,不再返回(string, err),而是返回一个StringValue类型的实例(注意不是指针,因为我们认为这个实例不需要被修改).后续所有类型转换的方法,均在StringValue类型上实现.换言之,StringValue类型的职责为:将各部分输入的值转换为对应的类型.

3.2 修改Context中处理各部分输入的方法的返回值

此处各方法不再返回(string, err),而是返回一个StringValue类型的实例.

3.2.1 Context.FormValue()方法

package stringValue

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

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

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

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

3.2.2 Context.QueryValue()方法

package stringValue

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

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (stringValue StringValue) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	if len(c.queryValues) == 0 {
		return StringValue{err: errors.New("web绑定错误: 无任何查询参数")}
	}

	values, ok := c.queryValues[key]
	if !ok {
		return StringValue{err: errors.New("web绑定错误: 查询参数中不存在键: " + key)}
	}

	return StringValue{value: values[0]}
}

3.2.3 Context.PathValue()方法

package stringValue

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

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

// PathValue 获取路径参数中给定键的值
func (c *Context) PathValue(key string) (stringValue StringValue) {
	if c.PathParams == nil {
		return StringValue{err: errors.New("web绑定错误: 无任何路径参数")}
	}

	value, ok := c.PathParams[key]
	if !ok {
		return StringValue{err: errors.New("web绑定错误: 路径参数中不存在键: " + key)}
	}

	return StringValue{value: value}
}

3.3 新增将输入参数转换为对应类型表达的方法

3.3.1 StringValue.AsInt64()

package stringValue

import "strconv"

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

// AsInt64 将承载的值转换为int64类型
func (s StringValue) AsInt64() (value int64, err error) {
	if s.err != nil {
		return 0, s.err
	}

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

3.3.2 StringValue.AsUint64()

package stringValue

import "strconv"

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

// AsUint64 将承载的值转换为uint64类型
func (s StringValue) AsUint64() (value uint64, err error) {
	if s.err != nil {
		return 0, s.err
	}

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

3.3.3 StringValue.AsFloat64()

package stringValue

import "strconv"

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

// AsFloat64 将承载的值转换为float64类型表示
func (s StringValue) AsFloat64() (value float64, err error) {
	if s.err != nil {
		return 0, s.err
	}

	return strconv.ParseFloat(s.value, 64)
}

3.4 在handleFunc中使用

新建文件context_test.go:

package stringValue

import (
	"fmt"
	"testing"
)

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

	handleFunc := func(ctx *Context) {
		// 获取路径参数
		id, err := ctx.PathValue("id").AsInt64()
		if err != nil {
			ctx.Resp.WriteHeader(400)
			ctx.Resp.Write([]byte("id输入不正确: " + err.Error()))
			return
		}

		ctx.Resp.Write([]byte(fmt.Sprintf("hello %d", id)))
	}

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

尝试请求:

这样设计的好处在于这一行代码:ctx.PathValue("id").AsInt64()

这种链式调用,有2个优点:

  • 对于框架使用者而言,是非常方便的

  • 对于框架设计者而言,每个类的职责非常清晰,保持Context的简约

同时,由于返回的都是结构体,因此可以保证在大多数的情况下,不会发生内存逃逸

Last updated 9 months ago

方法调用了函数,而函数又调用了函数.实际上真正实现解析查询参数的正是这个函数.换言之,我们每一次调用http.Ruquest.URL.Query()方法,都要重新解析一次.因此我们要考虑把查询参数缓存起来.

再次提醒,类型的本质是map[string][]string.因此只需要对我们的Context.queryValues做一个判断取值是否成功的操作,即可判断出到底是key到底是不存在,还是存在但值为空.

这是因为在函数中,已经对返回值进行了初始化,因此上述代码中拿到的c.queryValues必然不为空,就算没有任何查询参数,其值也是一个长度为0的map[string][]string.

这种设计思路来源于GO原生的database/sql包中的结构体.可以看到方法,思路和我们设计StringValue时的将输入参数转换为对应类型表达的方法是相同的.

http.Request.URL.Query()
url.ParseQuery()
url.ParseQuery()
url.parseQuery()
url.parseQuery()
url.Values
url.ParseQuery()
sql.Row
sql.Row.Scan()
无查询参数
有查询参数但值为空
正确的请求参数
错误的请求参数