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. 处理表单输入
无论你后续准备使用http.Request.Form
还是http.Request.PostForm
,都得先调用http.ParseForm()
方法解析表单数据,这俩字段才有值
http.Request.FormValue()
方法内部虽然会调用http.ParseForm()
方法解析表单数据,但是这个方法不抛出error,所以最好事前手动调用一次
我在复习时,将检测给定的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个:
这里将queryValue
设置成私有字段,意味着它的作用域不会离开web
包,所以它和框架使用者的代码是隔离的,只能通过Context. QueryValue()
方法来访问queryValue
中的K-V(确切的说只能访问V)
后续通过Key查找Value时,url.Values
类型是有API的(当然最终的版本并没有使用这个API),而如果使用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()
}
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