4.02 课后复习-Route
本节课工程结构:
(base) yanglei@yuanhong v4-rc % tree ./
./
├── context.go
├── handle_func.go
├── server.go
├── server_interface.go
└── server_test.go
0 directories, 5 files
PART1. 定义路由森林与节点
1.1 定义节点
新建文件node.go
:
package v4_rc
// node 路由树中的节点
type node struct {
// pattern 路由路径
pattern string
// children 子节点 key为子节点的路由路径 value为路径对应子节点
children map[string]*node
// HandleFunc 路由对应的处理函数
HandleFunc
}
1.2 定义路由森林
新建文件router.go
:
package v4_rc
// router 路由森林
type router struct {
// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
trees map[string]*node
}
1.3 定义添加路由的操作
router.go
:
package v4_rc
// router 路由森林
type router struct {
// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
trees map[string]*node
}
// addRoute 添加路由
func (r *router) addRoute(method string, pattern string, handle HandleFunc) {
panic("implement me")
}
PART2. 组合路由森林与Server
server_interface.go
:
package v4_rc
import "net/http"
// ServerInterface 服务器实体接口
// 用于定义服务器实体的行为
type ServerInterface interface {
// Handler 组合http.Handler接口
http.Handler
// Start 启动服务器
Start(addr string) error
}
server.go
:
package v4_rc
import (
"net"
"net/http"
)
type Server struct {
*router
}
// ServeHTTP 是http.Handler接口的方法 此处必须先写个实现 不然Server不是http.Handler接口的实现
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := &Context{
Req: r,
Resp: w,
}
s.serve(ctx)
panic("implement me")
}
// serve 查找路由树并执行匹配到的节点所对应的处理函数
func (s *Server) serve(ctx *Context) {
panic("implement me")
}
// Start 启动服务器
func (s *Server) Start(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return http.Serve(listener, s)
}
// GET 注册GET路由
func (s *Server) GET(path string, handler HandleFunc) {
s.addRoute(http.MethodGet, path, handler)
}
// POST 注册POST路由
func (s *Server) POST(path string, handler HandleFunc) {
s.addRoute(http.MethodPost, path, handler)
}
PART3. 静态路由注册
3.1 编写测试方法
3.1.1 构造路由树
router_test.go
:
package v4_rc
import (
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
}
3.1.2 断言路由森林
若2个路由森林中的路由树数量不同,则不相等
如果目标router中没有对应HTTP动词的路由树(例:目标路由森林中有GET/POST/PUT这3棵路由树,而期望路由森林中有GET/POST/DELETE这3棵路由树),则不相等
router_test.go
:
package v4_rc
import (
"fmt"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// TODO: implement me
return "", false
}
3.1.3 断言路由树
若2个节点中有一个为nil,则不相等
若2个节点的path不同,则不相等
若2个节点的子节点数量不同,则不相等
若2个节点的HandleFunc类型不同,则不相等
比对2个节点的子节点映射:
若源节点的子节点映射中,存在目标节点中没有的子节点,则不相同
注:因为上边已经比对过2个节点的子节点数量了,所以只要能通过这个步骤的比对,那么2个节点的子节点必然是一一对应的
两个path相同的节点递归比对
router_test.go
:
package v4_rc
import (
"fmt"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点的处理函数为: %v, 目标节点的处理函数为: %v", wantHandler, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
3.2 添加测试用例
router_test.go
:
package v4_rc
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user/home",
},
}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(targetRouter)
assert.True(t, ok, msg)
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点的处理函数为: %v, 目标节点的处理函数为: %v", wantHandler, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
3.3 实现addRoute方法
3.3.1 根据method查找路由树,不存在则创建
router.go
:
package v4_rc
import (
"strings"
)
// router 路由森林
type router struct {
// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
trees map[string]*node
}
func newRouter() *router {
return &router{
trees: map[string]*node{},
}
}
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
// step1. 查找路由树,不存在则创建
root, exist := r.trees[method]
if !exist {
root = &node{
path: "/",
}
r.trees[method] = root
}
}
3.3.2 对根节点做特殊处理
此处要特殊处理根节点的原因在于:后续按/
分割path后,根节点就直接被分割没了(变成"")了.所以要特殊处理根节点
router.go
:
package v4_rc
import (
"strings"
)
// router 路由森林
type router struct {
// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
trees map[string]*node
}
func newRouter() *router {
return &router{
trees: map[string]*node{},
}
}
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
// step1. 查找路由树,不存在则创建
root, exist := r.trees[method]
if !exist {
root = &node{
path: "/",
}
r.trees[method] = root
}
// step2. 在根节点上查找子节点 不存在则创建
// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
if path == "/" {
root.HandleFunc = handle
return
}
}
3.3.3 从根节点开始逐层查找目标节点,找到目标节点后添加HandleFunc
node.go
:
package v4_rc
// node 路由树中的节点
type node struct {
// path 路由路径
path string
// children 子节点 key为子节点的路由路径 value为路径对应子节点
children map[string]*node
// HandleFunc 路由对应的处理函数
HandleFunc
}
// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
if n.children == nil {
n.children = make(map[string]*node)
}
target, exist := n.children[segment]
if !exist {
// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
target = &node{
path: segment,
}
n.children[segment] = target
return target
}
// 当前节点的子节点映射中存在目标子节点 则直接返回
return target
}
router.go
:
package v4_rc
import (
"strings"
)
// router 路由森林
type router struct {
// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
trees map[string]*node
}
func newRouter() *router {
return &router{
trees: map[string]*node{},
}
}
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
// step1. 查找路由树,不存在则创建
root, exist := r.trees[method]
if !exist {
root = &node{
path: "/",
}
r.trees[method] = root
}
// step2. 在根节点上查找子节点 不存在则创建
// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
if path == "/" {
root.HandleFunc = handle
return
}
// step2.2 从根节点开始 逐层查找
target := root
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
for _, pathSegment := range pathSegments {
// 在当前节点上查找子节点
child := target.findOrCreate(pathSegment)
target = child
}
// 为目标节点创建HandleFunc
target.HandleFunc = handle
}
此时运行测试用例即可顺利通过测试
3.4 测试用例
3.4.1 对根节点的测试用例
router_test.go
:
package v4_rc
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/",
},
}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
},
}
msg, ok := wantRouter.equal(targetRouter)
assert.True(t, ok, msg)
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
3.4.2 前导/user节点的测试用例
router_test.go
:
package v4_rc
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
},
}
msg, ok := wantRouter.equal(targetRouter)
assert.True(t, ok, msg)
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
3.4.3 /order/detail节点的测试用例
为测试路由树中间有不存在的节点时,是否符合预期
router_test.go
:
package v4_rc
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
},
}
msg, ok := wantRouter.equal(targetRouter)
assert.True(t, ok, msg)
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
3.4.4 其他HTTP动词的测试用例
a. /login
router_test.go
:
package v4_rc
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodPost,
path: "/login",
},
}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
http.MethodPost: &node{
path: "/",
children: map[string]*node{
"login": &node{
path: "login",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(targetRouter)
assert.True(t, ok, msg)
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
b. /order/create
router_test.go
:
package v4_rc
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodPost,
path: "/login",
},
{
method: http.MethodPost,
path: "/order/create",
},
}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
http.MethodPost: &node{
path: "/",
children: map[string]*node{
"login": &node{
path: "login",
children: nil,
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"create": &node{
path: "create",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(targetRouter)
assert.True(t, ok, msg)
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
3.4.5 非法用例
a. path为空字符串
router.go
:
package v4_rc
import (
"strings"
)
// router 路由森林
type router struct {
// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
trees map[string]*node
}
func newRouter() *router {
return &router{
trees: map[string]*node{},
}
}
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
msg, ok := r.checkPath(path)
if !ok {
panic(msg)
}
// step1. 查找路由树,不存在则创建
root, exist := r.trees[method]
if !exist {
root = &node{
path: "/",
}
r.trees[method] = root
}
// step2. 在根节点上查找子节点 不存在则创建
// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
if path == "/" {
root.HandleFunc = handle
return
}
// step2.2 从根节点开始 逐层查找
target := root
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
for _, pathSegment := range pathSegments {
// 在当前节点上查找子节点
child := target.findOrCreate(pathSegment)
target = child
}
// 为目标节点创建HandleFunc
target.HandleFunc = handle
}
// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
if path == "" {
return "web: 路由不能为空字符串", false
}
return "", true
}
router_test.go
:
package v4_rc
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
type TestNode struct {
method string
path string
}
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由森林
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodPost,
path: "/login",
},
{
method: http.MethodPost,
path: "/order/create",
},
}
targetRouter := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
targetRouter.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
http.MethodPost: &node{
path: "/",
children: map[string]*node{
"login": &node{
path: "login",
children: nil,
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"create": &node{
path: "create",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(targetRouter)
assert.True(t, ok, msg)
}
func (r *router) equal(target *router) (msg string, ok bool) {
// step1. 比较路由森林中的路由树数量
wantLen := len(r.trees)
targetLen := len(target.trees)
if wantLen != targetLen {
msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
return msg, false
}
// step2. 比对2个路由森林中的路由树HTTP动词是否相同
for method, tree := range r.trees {
dstTree, ok := target.trees[method]
if !ok {
msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
return msg, false
}
// step3. 比对2个路由树中的结构是否相同
msg, ok = tree.equal(dstTree)
if !ok {
return msg, false
}
}
return "", true
}
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step5. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step5.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step5.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
func TestRouter_Illegal_Path(t *testing.T) {
r := newRouter()
mockHandle := func(ctx *Context) {}
nilPathFunc := func() { r.addRoute(http.MethodGet, "", mockHandle) }
assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")
}
TODO:assert.Panicsf
b. path不是以/
开头
router.go
:
// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
if path == "" {
return "web: 路由不能为空字符串", false
}
if path[0] != '/' {
return "web: 路由必须以/开头", false
}
return "", true
}
router_test.go
:
func TestRouter_Illegal_Path(t *testing.T) {
r := newRouter()
mockHandle := func(ctx *Context) {}
// 路由为空的测试用例
nilPathFunc := func() {
r.addRoute(http.MethodGet, "", mockHandle)
}
assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")
// 路由不是以`/`开头的测试用例
incorrectFirstCharacter := func() {
r.addRoute(http.MethodGet, "login", mockHandle)
}
assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")
}
c. path以/
结尾
router.go
:
// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
if path == "" {
return "web: 路由不能为空字符串", false
}
if path[0] != '/' {
return "web: 路由必须以/开头", false
}
if path != "/" && path[len(path)-1] == '/' {
return "web: 路由不能以/结尾", false
}
return "", true
}
router_test.go
:
func TestRouter_Illegal_Path(t *testing.T) {
r := newRouter()
mockHandle := func(ctx *Context) {}
// 路由为空的测试用例
nilPathFunc := func() {
r.addRoute(http.MethodGet, "", mockHandle)
}
assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")
// 路由不是以`/`开头的测试用例
incorrectFirstCharacter := func() {
r.addRoute(http.MethodGet, "login", mockHandle)
}
assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")
// 路由以`/`结尾的测试用例
incorrectLastCharacter := func() {
r.addRoute(http.MethodGet, "/login/", mockHandle)
}
assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")
}
d. path中包含连续的/
router.go
:
// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
if path == "" {
return "web: 路由不能为空字符串", false
}
if path[0] != '/' {
return "web: 路由必须以/开头", false
}
if path != "/" {
if path[len(path)-1] == '/' {
return "web: 路由不能以/结尾", false
}
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
for _, pathSegment := range pathSegments {
if pathSegment == "" {
return "web: 路由中不能出现连续的/", false
}
}
}
return "", true
}
router_test.go
:
func TestRouter_Illegal_Path(t *testing.T) {
r := newRouter()
mockHandle := func(ctx *Context) {}
// 路由为空的测试用例
nilPathFunc := func() {
r.addRoute(http.MethodGet, "", mockHandle)
}
assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")
// 路由不是以`/`开头的测试用例
incorrectFirstCharacter := func() {
r.addRoute(http.MethodGet, "login", mockHandle)
}
assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")
// 路由以`/`结尾的测试用例
incorrectLastCharacter := func() {
r.addRoute(http.MethodGet, "/login/", mockHandle)
}
assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")
// 路由中出现了连续的/
continuousSeparator := func() {
r.addRoute(http.MethodGet, "/a//b", mockHandle)
}
assert.Panicsf(t, continuousSeparator, "web: 路由不能出现多个连续的/")
}
e. 路由重复注册
e1. 根节点路由重复注册
router.go
:
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
msg, ok := r.checkPath(path)
if !ok {
panic(msg)
}
// step1. 查找路由树,不存在则创建
root, exist := r.trees[method]
if !exist {
root = &node{
path: "/",
}
r.trees[method] = root
}
// step2. 在根节点上查找子节点 不存在则创建
// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
if path == "/" {
if root.HandleFunc != nil {
msg = fmt.Sprintf("web: 路由冲突,重复注册路由 [%s]", path)
panic(msg)
}
root.HandleFunc = handle
return
}
// step2.2 从根节点开始 逐层查找
target := root
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
for _, pathSegment := range pathSegments {
// 在当前节点上查找子节点
child := target.findOrCreate(pathSegment)
target = child
}
// 为目标节点创建HandleFunc
target.HandleFunc = handle
}
router_test.go
:
func TestRouter_Illegal_Path(t *testing.T) {
r := newRouter()
mockHandle := func(ctx *Context) {}
// 路由为空的测试用例
nilPathFunc := func() {
r.addRoute(http.MethodGet, "", mockHandle)
}
assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")
// 路由不是以`/`开头的测试用例
incorrectFirstCharacter := func() {
r.addRoute(http.MethodGet, "login", mockHandle)
}
assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")
// 路由以`/`结尾的测试用例
incorrectLastCharacter := func() {
r.addRoute(http.MethodGet, "/login/", mockHandle)
}
assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")
// 路由中出现了连续`/`的测试用例
continuousSeparator := func() {
r.addRoute(http.MethodGet, "/a//b", mockHandle)
}
assert.Panicsf(t, continuousSeparator, "web: 路由不能出现多个连续的/")
// 路由重复注册的测试用例
r.addRoute(http.MethodGet, "/", mockHandle)
repeatRegisterRoute := func() {
r.addRoute(http.MethodGet, "/", mockHandle)
}
assert.Panicsf(t, repeatRegisterRoute, "web: 路由冲突,重复注册路由 [/]")
}
e2. 普通节点路由重复注册
router.go
:
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
msg, ok := r.checkPath(path)
if !ok {
panic(msg)
}
// step1. 查找路由树,不存在则创建
root, exist := r.trees[method]
if !exist {
root = &node{
path: "/",
}
r.trees[method] = root
}
// step2. 在根节点上查找子节点 不存在则创建
// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
if path == "/" {
if root.HandleFunc != nil {
msg = fmt.Sprintf("web: 路由冲突,重复注册路由 [%s]", path)
panic(msg)
}
root.HandleFunc = handle
return
}
// step2.2 从根节点开始 逐层查找
target := root
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
for _, pathSegment := range pathSegments {
// 在当前节点上查找子节点
child := target.findOrCreate(pathSegment)
target = child
}
// 为目标节点创建HandleFunc
if target.HandleFunc != nil {
msg = fmt.Sprintf("web: 路由冲突,重复注册路由 [%s]", path)
panic(msg)
}
target.HandleFunc = handle
}
router_test.go
:
func TestRouter_Illegal_Path(t *testing.T) {
r := newRouter()
mockHandle := func(ctx *Context) {}
// 路由为空的测试用例
nilPathFunc := func() {
r.addRoute(http.MethodGet, "", mockHandle)
}
assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")
// 路由不是以`/`开头的测试用例
incorrectFirstCharacter := func() {
r.addRoute(http.MethodGet, "login", mockHandle)
}
assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")
// 路由以`/`结尾的测试用例
incorrectLastCharacter := func() {
r.addRoute(http.MethodGet, "/login/", mockHandle)
}
assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")
// 路由中出现了连续`/`的测试用例
continuousSeparator := func() {
r.addRoute(http.MethodGet, "/a//b", mockHandle)
}
assert.Panicsf(t, continuousSeparator, "web: 路由不能出现多个连续的/")
// 路由重复注册的测试用例
// 根节点路由重复注册
r.addRoute(http.MethodGet, "/", mockHandle)
repeatRegisterRoute := func() {
r.addRoute(http.MethodGet, "/", mockHandle)
}
assert.Panicsf(t, repeatRegisterRoute, "web: 路由冲突,重复注册路由 [/]")
// 普通节点路由重复注册
r.addRoute(http.MethodGet, "/user/login", mockHandle)
repeatRegisterRoute = func() {
r.addRoute(http.MethodGet, "/user/login", mockHandle)
}
assert.Panicsf(t, repeatRegisterRoute, "web: 路由冲突,重复注册路由 [/user/login]")
}
PART4. 静态路由查找
4.1 编写测试函数
router_test.go
:
func TestRouter_FindRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
}
4.2 定义测试用例的类型
我们需要这个类型能够告知我们如下信息:
在给定HTTP动词和path的前提下,是否找到了节点?
在给定HTTP动词和path的前提下,找到的节点和预期的节点是否相同?
type TestCaseNode struct {
name string // name 子测试用例的名称
method string // method HTTP动词
path string // path 路由路径
isFound bool // isFound 是否找到了节点
wantNode *node // wantNode 期望的路由节点
}
4.3 定义测试的过程
step1. 判断在路由树中是否找到了节点
step2. 判断找到的节点和预期的节点是否相同
route_test.go
:
func TestRouter_FindRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
// name 子测试用例的名称
name string
// method HTTP动词
method string
// path 路由路径
path string
// isFound 是否找到了节点
isFound bool
// wantNode 期望的路由节点
wantNode *node
}{}
// step3. 测试是否找到节点
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
foundNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
// 3.1 判断在路由树中是否找到了节点
if !found {
return
}
// 3.2 判断找到的节点和预期的节点是否相同
msg, equal := testCase.wantNode.equal(foundNode)
assert.True(t, equal, msg)
})
}
}
4.4 构造测试用例
HTTP动词对应的路由树不存在
完全命中
命中了path对应的节点,但HandleFunc为nil
根节点(特殊处理)
path对应的节点不存在
4.5 根据测试用例开发findRoute()
4.5.1 HTTP动词对应的路由树不存在
a. 实现
route.go
:
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
_, ok := r.trees[method]
if !ok {
return nil, false
}
panic("implement me")
}
b. 测试
route_test.go
:
func TestRouter_FindRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
// name 子测试用例的名称
name string
// method HTTP动词
method string
// path 路由路径
path string
// isFound 是否找到了节点
isFound bool
// wantNode 期望的路由节点
wantNode *node
}{
{
name: "Method not found",
method: http.MethodDelete,
path: "/user",
isFound: false,
wantNode: nil,
},
}
// step3. 测试是否找到节点
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
foundNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
// 3.1 判断在路由树中是否找到了节点
if !found {
return
}
// 3.2 判断找到的节点和预期的节点是否相同
msg, equal := testCase.wantNode.equal(foundNode)
assert.True(t, equal, msg)
})
}
}
4.5.2 完全命中
a. 实现
step1. 按
/
切割pathstep2. 从路由树的根节点开始,按"层次"查找节点
step3. 没找到则返回
nil, false
即可
这里我一开始的实现是这样的:
route.go
:
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
// HTTP动词对应的路由树不存在 直接返回nil false即可
root, ok := r.trees[method]
if !ok {
return nil, false
}
//
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
target := root
for _, pathSegment := range pathSegments {
target, ok = target.children[pathSegment]
if !ok {
return nil, false
}
}
return target, true
}
有2个问题:
没有对
target.children
判空"在当前节点下查找子节点"是一个独立完整的功能,应该是
node
结构体的方法
修改后的实现:
node.go
:
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找path为给定path值的节点
// 找到则返回节点 否则返回 nil, false
func (n *node) childOf(path string) (*node, bool) {
if n.children == nil {
return nil, false
}
child, found := n.children[path]
if !found {
return nil, false
}
return child, true
}
route.go
:
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
// HTTP动词对应的路由树不存在 直接返回nil false即可
root, ok := r.trees[method]
if !ok {
return nil, false
}
// 在路由树中逐层查找节点
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
target := root
for _, pathSegment := range pathSegments {
target, ok = target.children[pathSegment]
if !ok {
return nil, false
}
}
return target, true
}
b. 测试
route_test.go
:
func TestRouter_FindRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/order/detail",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
// name 子测试用例的名称
name string
// method HTTP动词
method string
// path 路由路径
path string
// isFound 是否找到了节点
isFound bool
// wantNode 期望的路由节点
wantNode *node
}{
{
name: "Method not found",
method: http.MethodDelete,
path: "/user",
isFound: false,
wantNode: nil,
},
{
name: "completely match",
method: http.MethodGet,
path: "/order/detail",
isFound: true,
wantNode: &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
}
// step3. 测试是否找到节点
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
foundNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
// 3.1 判断在路由树中是否找到了节点
if !found {
return
}
// 3.2 判断找到的节点和预期的节点是否相同
msg, equal := testCase.wantNode.equal(foundNode)
assert.True(t, equal, msg)
})
}
}
4.5.3 命中了path对应的节点,但HandleFunc为nil
这个case不需要做特殊的边缘条件检测.因为在addRoute()
时我们也没有检测HandleFunc是否为空.换言之就是:既然在注册路由时允许使用者传入空的HandleFunc,那么在查找时命中了一个HandleFunc为nil的节点就不算是错误
a. 测试
route_test.go
:
func TestRouter_FindRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/order/detail",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
// name 子测试用例的名称
name string
// method HTTP动词
method string
// path 路由路径
path string
// isFound 是否找到了节点
isFound bool
// wantNode 期望的路由节点
wantNode *node
}{
{
name: "Method not found",
method: http.MethodDelete,
path: "/user",
isFound: false,
wantNode: nil,
},
{
name: "completely match",
method: http.MethodGet,
path: "/order/detail",
isFound: true,
wantNode: &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
{
name: "nil handle func",
method: http.MethodGet,
path: "/order",
isFound: true,
wantNode: &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
HandleFunc: nil,
},
},
}
// step3. 测试是否找到节点
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
foundNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
// 3.1 判断在路由树中是否找到了节点
if !found {
return
}
// 3.2 判断找到的节点和预期的节点是否相同
msg, equal := testCase.wantNode.equal(foundNode)
assert.True(t, equal, msg)
})
}
}
4.5.4 根节点
a. 实现
route.go
:
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
// HTTP动词对应的路由树不存在 直接返回nil false即可
root, ok := r.trees[method]
if !ok {
return nil, false
}
target := root
// 对根节点做特殊处理
if path == "/" {
return target, true
}
// 在路由树中逐层查找节点
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
for _, pathSegment := range pathSegments {
target, ok = target.children[pathSegment]
if !ok {
return nil, false
}
}
return target, true
}
b. 测试
route_test.go
:
func TestRouter_FindRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodGet,
path: "/",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
// name 子测试用例的名称
name string
// method HTTP动词
method string
// path 路由路径
path string
// isFound 是否找到了节点
isFound bool
// wantNode 期望的路由节点
wantNode *node
}{
{
name: "Method not found",
method: http.MethodDelete,
path: "/user",
isFound: false,
wantNode: nil,
},
{
name: "completely match",
method: http.MethodGet,
path: "/order/detail",
isFound: true,
wantNode: &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
{
name: "nil handle func",
method: http.MethodGet,
path: "/order",
isFound: true,
wantNode: &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
HandleFunc: nil,
},
},
{
name: "root node",
method: http.MethodGet,
path: "/",
isFound: true,
wantNode: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: nil,
HandleFunc: mockHandle,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandle,
},
},
}
// step3. 测试是否找到节点
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
foundNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
// 3.1 判断在路由树中是否找到了节点
if !found {
return
}
// 3.2 判断找到的节点和预期的节点是否相同
msg, equal := testCase.wantNode.equal(foundNode)
assert.True(t, equal, msg)
})
}
}
4.5.5 path对应的节点不存在
a. 测试
route_test.go
:
func TestRouter_FindRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodGet,
path: "/",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
// name 子测试用例的名称
name string
// method HTTP动词
method string
// path 路由路径
path string
// isFound 是否找到了节点
isFound bool
// wantNode 期望的路由节点
wantNode *node
}{
{
name: "Method not found",
method: http.MethodDelete,
path: "/user",
isFound: false,
wantNode: nil,
},
{
name: "completely match",
method: http.MethodGet,
path: "/order/detail",
isFound: true,
wantNode: &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
{
name: "nil handle func",
method: http.MethodGet,
path: "/order",
isFound: true,
wantNode: &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
HandleFunc: nil,
},
},
{
name: "root node",
method: http.MethodGet,
path: "/",
isFound: true,
wantNode: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: nil,
HandleFunc: mockHandle,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandle,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandle,
},
},
{
name: "path not found",
method: http.MethodGet,
path: "/login",
isFound: false,
wantNode: nil,
},
}
// step3. 测试是否找到节点
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
foundNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
// 3.1 判断在路由树中是否找到了节点
if !found {
return
}
// 3.2 判断找到的节点和预期的节点是否相同
msg, equal := testCase.wantNode.equal(foundNode)
assert.True(t, equal, msg)
})
}
}
PART5. route集成至Server
5.1 实现
server.go
:
// ServeHTTP 是http.Handler接口的方法 此处必须先写个实现 不然Server不是http.Handler接口的实现
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := &Context{
Req: r,
Resp: w,
}
s.serve(ctx)
}
// serve 查找路由树并执行匹配到的节点所对应的处理函数
func (s *HTTPServer) serve(ctx *Context) {
method := ctx.Req.Method
path := ctx.Req.URL.Path
targetNode, found := s.router.findRoute(method, path)
if !found || targetNode.HandleFunc == nil {
ctx.Resp.WriteHeader(http.StatusNotFound)
_, _ = ctx.Resp.Write([]byte("not found"))
return
}
targetNode.HandleFunc(ctx)
}
5.2 测试
server_test.go
:
package v4_rc
import (
"net/http"
"testing"
)
// TestServer_Start 测试服务器启动
func TestServer_Start(t *testing.T) {
s := NewHTTPServer()
handleFunc := func(ctx *Context) {
ctx.Resp.Write([]byte("hello order detail"))
}
s.addRoute(http.MethodGet, "/order/detail", handleFunc)
err := s.Start(":8081")
if err != nil {
t.Fatal(err)
}
}
PART6. 通配符路由的注册与查找
6.1 通配符路由的定义与设计
6.1.1 通配符路由的定义
通配符路由:用*
表达匹配任何路径
6.1.2 通配符路由的设计
一个
*
只能表达1段路由不做可回溯的路由匹配机制
6.2 通配符路由的注册
6.2.1 修改node
的结构
node
的结构node.go
:
// node 路由树中的节点
type node struct {
// path 路由路径
path string
// children 子节点 key为子节点的路由路径 value为路径对应子节点
children map[string]*node
// wildcardChild 通配符子节点
wildcardChild *node
// HandleFunc 路由对应的处理函数
HandleFunc
}
6.2.2 定义测试用例
route_test.go
:
func TestRouter_wildcard(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
// 普通节点的通配符子节点
{
method: http.MethodGet,
path: "/order/*",
},
}
r := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 断言路由树
wantRouter := &router{trees: map[string]*node{
"/": &node{
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: nil,
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
wildcardChild: nil,
HandleFunc: nil,
},
}}
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
根据debug的结果可知:应该把*
创建在wildcardChild
字段上,现在则是创建在了children
字段上
6.2.3 修改创建子节点的逻辑
正确的逻辑是:当路径为*
时,将节点创建在wildcardChild
字段上
node.go
:
// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
// 若路径为* 则查找或创建通配符子节点
if segment == "*" {
if n.wildcardChild == nil {
n.wildcardChild = &node{
path: "*",
}
}
return n.wildcardChild
}
if n.children == nil {
n.children = make(map[string]*node)
}
target, exist := n.children[segment]
if !exist {
// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
target = &node{
path: segment,
}
n.children[segment] = target
return target
}
// 当前节点的子节点映射中存在目标子节点 则直接返回
return target
}
6.2.4 修改判断子节点相等的逻辑
由于新增了通配符子节点字段,所以对于2个节点,也要比对各自的通配符子节点是否相同
route_test.go
:
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比较2个节点的通配符子节点是否相同
if n.wildcardChild != nil {
if target.wildcardChild == nil {
msg = fmt.Sprintf("目标节点的通配符子节点为空")
return msg, false
}
_, equal := n.wildcardChild.equal(target.wildcardChild)
if !equal {
msg = fmt.Sprintf("期望节点 %s 的通配符子节点与目标节点 %s 的通配符子节点不等", n.path, target.path)
return msg, false
}
}
// step5. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step6. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step6.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step6.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
此时再跑测试用例,就可以跑通了
6.2.5 添加其他测试用例
6.2.5.1 根节点的通配符子节点
route_test.go
:
func TestRouter_wildcard(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
// 普通节点的通配符子节点
{
method: http.MethodGet,
path: "/order/*",
},
// 根节点的通配符子节点
{
method: http.MethodGet,
path: "/*",
},
}
r := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 断言路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: nil,
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
6.2.5.2 通配符子节点的通配符子节点
route_test.go
:
func TestRouter_wildcard(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
// 普通节点的通配符子节点
{
method: http.MethodGet,
path: "/order/*",
},
// 根节点的通配符子节点
{
method: http.MethodGet,
path: "/*",
},
// 通配符子节点的通配符子节点
{
method: http.MethodGet,
path: "/*/*",
},
}
r := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 断言路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: nil,
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
6.2.5.3 通配符子节点的普通子节点
route_test.go
:
func TestRouter_wildcard(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
// 普通节点的通配符子节点
{
method: http.MethodGet,
path: "/order/*",
},
// 根节点的通配符子节点
{
method: http.MethodGet,
path: "/*",
},
// 通配符子节点的通配符子节点
{
method: http.MethodGet,
path: "/*/*",
},
// 通配符子节点的普通子节点
{
method: http.MethodGet,
path: "/*/get",
},
}
r := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 断言路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: nil,
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
wildcardChild: &node{
path: "*",
children: map[string]*node{
"get": &node{
path: "get",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
},
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
6.2.5.4 通配符子节点的普通子节点的通配符子节点
route_test.go
:
func TestRouter_wildcard(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
// 普通节点的通配符子节点
{
method: http.MethodGet,
path: "/order/*",
},
// 根节点的通配符子节点
{
method: http.MethodGet,
path: "/*",
},
// 通配符子节点的通配符子节点
{
method: http.MethodGet,
path: "/*/*",
},
// 通配符子节点的普通子节点
{
method: http.MethodGet,
path: "/*/get",
},
// 通配符子节点的普通子节点的通配符子节点
{
method: http.MethodGet,
path: "/*/order/*",
},
}
r := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 断言路由树
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: nil,
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
wildcardChild: &node{
path: "*",
children: map[string]*node{
"get": &node{
path: "get",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: nil,
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
wildcardChild: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
}
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
6.3 通配符路由的匹配
6.3.1 实现
思路比较简单:
若当前节点的children映射为空,则有可能匹配到通配符子节点
若在当前节点的children映射中没有找到path对应的子节点,则有可能匹配到通配符子节点
此处说"有可能匹配到",是因为还有一种可能是通配符子节点为空,这种情况就属于没匹配到了
node.go
:
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找path为给定path值的节点
// 找到则返回节点 否则返回 nil, false
func (n *node) childOf(path string) (*node, bool) {
if n.children == nil {
return n.wildcardChild, n.wildcardChild != nil
}
child, found := n.children[path]
if !found {
return n.wildcardChild, n.wildcardChild != nil
}
return child, true
}
6.3.2 测试
a. 匹配普通节点的通配符子节点
route_test.go
:
func TestRouter_FindRoute_Wildcard(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/order/*",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
name string
method string
path string
isFound bool
wantNode *node
}{
{
name: "普通节点的通配符子节点",
method: http.MethodGet,
path: "/order/detail",
isFound: true,
wantNode: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandle,
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
targetNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
if !found {
return
}
msg, equal := testCase.wantNode.equal(targetNode)
assert.True(t, equal, msg)
})
}
}
b. 匹配普通节点下普通子节点和通配符子节点共存
func TestRouter_FindRoute_Wildcard(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/order/*",
},
{
method: http.MethodGet,
path: "/order/create",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
name string
method string
path string
isFound bool
wantNode *node
}{
{
name: "普通节点的通配符子节点",
method: http.MethodGet,
path: "/order/detail",
isFound: true,
wantNode: &node{
path: "*",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandle,
},
},
{
name: "普通节点下通配符子节点和普通子节点共存",
method: http.MethodGet,
path: "/order/create",
isFound: true,
wantNode: &node{
path: "create",
children: nil,
wildcardChild: nil,
HandleFunc: mockHandle,
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
targetNode, found := r.findRoute(testCase.method, testCase.path)
assert.Equal(t, testCase.isFound, found)
if !found {
return
}
msg, equal := testCase.wantNode.equal(targetNode)
assert.True(t, equal, msg)
})
}
}
c. server组合route时的通配符匹配
server_test.go
:
package v4_rc
import (
"net/http"
"testing"
)
// TestServer_Start 测试服务器启动
func TestServer_Start(t *testing.T) {
s := NewHTTPServer()
wildcardHandleFunc := func(ctx *Context) {
ctx.Resp.Write([]byte("hello order wildcard"))
}
s.addRoute(http.MethodGet, "/order/*", wildcardHandleFunc)
handleFunc := func(ctx *Context) {
ctx.Resp.Write([]byte("hello order detail"))
}
s.addRoute(http.MethodGet, "/order/detail", handleFunc)
err := s.Start(":8081")
if err != nil {
t.Fatal(err)
}
}
PART7. 参数路由的注册与查找
7.1 参数路由的定义与设计
7.1.1 参数路由的定义
参数路由:就是指在路由中带上参数,同时这些参数对应的值可以被业务取出来使用.在我们的设计中用:参数名
的形式表示路由参数
例:/user/:id
,如果输入路径/user/123
,则会命中这个路由/user/:id
,并且在业务函数中可以取到变量id = 123
7.1.2 参数路径的设计
是否允许同样的参数路由和通配符路由一起注册?
例如同时注册/user/:id
和/user/*
一起注册?
可以允许,但没必要,且用户也不该设计这种路由
7.2 实现参数路由节点的创建
7.2.1 修改node的结构
node.go
// node 路由树中的节点
type node struct {
path string // path 路由路径
children map[string]*node // children 子节点 key为子节点的路由路径 value为路径对应子节点
wildcardChild *node // wildcardChild 通配符子节点
paramChild *node // paramChild 参数路由子节点
HandleFunc // HandleFunc 路由对应的处理函数
}
7.2.2 定义测试用例
router_test.go
func TestRouter_addParamRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/order/detail/:id",
},
}
r := newRouter()
mockHandleFunc := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树
wantRoute := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
wildcardChild: nil,
paramChild: &node{
path: ":id",
children: nil,
wildcardChild: nil,
paramChild: nil,
HandleFunc: mockHandleFunc,
},
HandleFunc: nil,
},
},
wildcardChild: nil,
paramChild: nil,
HandleFunc: nil,
},
},
wildcardChild: nil,
paramChild: nil,
HandleFunc: nil,
},
},
}
msg, equal := wantRoute.equal(r)
assert.True(t, equal, msg)
}
7.2.3 修改创建子节点的逻辑
node.go
:
// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
// 若路径以:开头 则查找或创建参数子节点
if strings.HasPrefix(segment, ":") {
if n.paramChild == nil {
n.paramChild = &node{
path: segment,
}
}
return n.paramChild
}
// 若路径为* 则查找或创建通配符子节点
if segment == "*" {
if n.wildcardChild == nil {
n.wildcardChild = &node{
path: "*",
}
}
return n.wildcardChild
}
if n.children == nil {
n.children = make(map[string]*node)
}
target, exist := n.children[segment]
if !exist {
// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
target = &node{
path: segment,
}
n.children[segment] = target
return target
}
// 当前节点的子节点映射中存在目标子节点 则直接返回
return target
}
7.2.4 修改判断子节点相等的逻辑
router_test.go
:
func (n *node) equal(target *node) (msg string, ok bool) {
// step1. 目标节点为空 则必然不等
if target == nil {
msg = "目标节点为nil"
return msg, false
}
// step2. 比较节点的路径是否相同
if n.path != target.path {
msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
return msg, false
}
// step3. 比较2个节点的子节点数量是否相同
if len(n.children) != len(target.children) {
msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
return msg, false
}
// step4. 比对2个节点的参数子节点是否相同
if n.paramChild != nil {
if target.paramChild == nil {
msg = fmt.Sprintf("目标节点的参数节点为空")
return msg, false
}
_, equal := n.paramChild.equal(target.paramChild)
if !equal {
msg = fmt.Sprintf("期望节点 %s 的参数子节点与目标节点 %s 的参数子节点不等", n.path, target.path)
return msg, false
}
}
// step5. 比较2个节点的通配符子节点是否相同
if n.wildcardChild != nil {
if target.wildcardChild == nil {
msg = fmt.Sprintf("目标节点的通配符子节点为空")
return msg, false
}
_, equal := n.wildcardChild.equal(target.wildcardChild)
if !equal {
msg = fmt.Sprintf("期望节点 %s 的通配符子节点与目标节点 %s 的通配符子节点不等", n.path, target.path)
return msg, false
}
}
// step5. 比较2个节点的处理函数是否相同
wantHandler := reflect.ValueOf(n.HandleFunc)
targetHandler := reflect.ValueOf(target.HandleFunc)
if wantHandler != targetHandler {
msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
return msg, false
}
// step6. 比较2个节点的子节点是否相同
for path, child := range n.children {
// step6.1 比对2个节点的子节点的路径是否相同
dstChild, exist := target.children[path]
if !exist {
msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
return msg, false
}
// step6.2 对路径相同的子节点递归比对
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
7.3 参数路由的校验
7.3.1 实现校验逻辑
设计中是不准备支持同样的参数路由和通配符路由一起注册的(即/user/*
和/user/:id
).
所以从逻辑上来讲,只需要实现:注册参数路由时检测通配符路由是否存在;注册通配符路由时检测参数路由是否存在.若对方存在,则不允许注册即可.
node.go
// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
// 若路径以:开头 则查找或创建参数子节点
if strings.HasPrefix(segment, ":") {
if n.wildcardChild != nil {
msg := fmt.Sprintf("web: 非法路由,节点 %s 已有通配符路由.不允许同时注册通配符路由和参数路由", n.path)
panic(msg)
}
if n.paramChild == nil {
n.paramChild = &node{
path: segment,
}
}
return n.paramChild
}
// 若路径为* 则查找或创建通配符子节点
if segment == "*" {
if n.paramChild != nil {
msg := fmt.Sprintf("web: 非法路由,节点 %s 已有参数路由.不允许同时注册通配符路由和参数路由", n.path)
panic(msg)
}
if n.wildcardChild == nil {
n.wildcardChild = &node{
path: "*",
}
}
return n.wildcardChild
}
if n.children == nil {
n.children = make(map[string]*node)
}
target, exist := n.children[segment]
if !exist {
// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
target = &node{
path: segment,
}
n.children[segment] = target
return target
}
// 当前节点的子节点映射中存在目标子节点 则直接返回
return target
}
7.3.2 测试
router_test.go
:
func TestRouter_findRoute_param_and_wildcard_coexist(t *testing.T) {
// step1. 注册通配符路由
r := newRouter()
mockHandleFunc := func(ctx *Context) {}
r.addRoute(http.MethodGet, "/user/*", mockHandleFunc)
// step2. 断言非法注册
panicFunc := func() {
r.addRoute(http.MethodGet, "/user/:id", mockHandleFunc)
}
assert.Panicsf(t, panicFunc, "web: 非法路由,节点 detail 已有通配符路由.不允许同时注册通配符路由和参数路由")
}
7.4 参数路由的查找
7.4.1 编写测试用例
router_test.go
func TestRouter_findParamRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []*TestNode{
{
method: http.MethodGet,
path: "/order/:id",
},
{
method: http.MethodGet,
path: "/user/:id/detail",
},
}
r := newRouter()
mockHandle := func(ctx *Context) {}
for _, testRoute := range testRoutes {
r.addRoute(testRoute.method, testRoute.path, mockHandle)
}
// step2. 构造测试用例
testCases := []struct {
name string
method string
path string
isFound bool
wantNode *node
}{
{
name: "param route",
method: http.MethodGet,
path: "/order/5",
isFound: true,
wantNode: &node{
path: ":id",
children: nil,
wildcardChild: nil,
paramChild: nil,
HandleFunc: mockHandle,
},
},
{
name: "param route",
method: http.MethodGet,
path: "/user/1/detail",
isFound: true,
wantNode: &node{
path: "detail",
children: nil,
wildcardChild: nil,
paramChild: nil,
HandleFunc: mockHandle,
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
findNode, found := r.findRoute(testCase.method, testCase.path)
assert.True(t, found, "节点未找到")
if !found {
return
}
msg, equal := testCase.wantNode.equal(findNode)
assert.True(t, equal, msg)
})
}
}
此时运行测试用例报错节点未找到,因为此时没有查找参数子节点的逻辑
7.4.2 实现参数路由的查找
node.go
:
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找path为给定path值的节点
// 找到则返回节点 否则返回 nil, false
func (n *node) childOf(path string) (*node, bool) {
if n.children == nil {
if n.paramChild != nil {
return n.paramChild, true
}
return n.wildcardChild, n.wildcardChild != nil
}
child, found := n.children[path]
if !found {
if n.paramChild != nil {
return n.paramChild, true
}
return n.wildcardChild, n.wildcardChild != nil
}
return child, true
}
此时测试用例即可通过
7.5 参数路由的参数值
7.5.1 定义新类型
需要定义一个新的类型,该类型包含命中的节点,以及当该节点为参数路由节点时的参数名和参数值
match_node.go
:
package v4_rc
type matchNode struct {
node *node // node 命中的节点
pathParams map[string]string // pathParams 节点对应的路由参数 其中key为参数名 value为参数值
}
7.5.2 修改node.childOf()
方法
node.childOf()
方法node.childOf()
方法需要告知其调用者(也就是router.findRoute()
方法)找到的节点是否为参数子节点.因为查找节点时,拿到的是参数值,而参数名是放在查找到的节点的path字段上的.
node.go
:
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找对应的子节点
// 若未在当前节点的子节点映射中查找到path对应的节点 则尝试查找当前节点的参数子节点
// 若未查找到当前节点的参数子节点 则尝试查找当前节点的通配符子节点
func (n *node) childOf(path string) (targetNode *node, isParamNode bool, isFound bool) {
if n.children == nil {
if n.paramChild != nil {
return n.paramChild, true, true
}
return n.wildcardChild, false, n.wildcardChild != nil
}
child, found := n.children[path]
if !found {
if n.paramChild != nil {
return n.paramChild, true, true
}
return n.wildcardChild, false, n.wildcardChild != nil
}
return child, false, true
}
7.5.3 为matchNode
结构体添加写入路由参数的方法
matchNode
结构体添加写入路由参数的方法match_node.go
:
// addPathParam 用于添加路径参数
func (m *matchNode) addPathParam(key string, value string) {
if m.pathParams == nil {
m.pathParams = make(map[string]string)
}
m.pathParams[key] = value
}
7.5.4 修改router.findRoute()
方法
router.findRoute()
方法router.findRoute()
方法则返回一个*matchNode
类型的实例,其中包含了参数名和参数值(如果有的话).
router.go
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*matchNode, bool) {
targetMatchNode := &matchNode{}
// HTTP动词对应的路由树不存在 直接返回nil false即可
root, ok := r.trees[method]
if !ok {
return nil, false
}
target := root
// 对根节点做特殊处理
if path == "/" {
targetMatchNode.node = target
return targetMatchNode, true
}
// 在路由树中逐层查找节点
path = strings.TrimLeft(path, "/")
pathSegments := strings.Split(path, "/")
for _, pathSegment := range pathSegments {
child, isParam, ok := target.childOf(pathSegment)
if !ok {
return nil, false
}
if isParam {
key := strings.TrimPrefix(child.path, ":")
value := pathSegment
targetMatchNode.addPathParam(key, value)
}
target = child
}
targetMatchNode.node = target
return targetMatchNode, true
}
7.5.5 修改Context
结构体
Context
结构体context.go
// Context 路由处理函数的上下文
type Context struct {
Req *http.Request // Req HTTP请求
Resp http.ResponseWriter // Resp HTTP响应
PathParams map[string]string // PathParams 参数路由的参数
}
7.5.6 修改HTTPServer.serve()
方法
HTTPServer.serve()
方法server.go
// serve 查找路由树并执行匹配到的节点所对应的处理函数
func (s *HTTPServer) serve(ctx *Context) {
method := ctx.Req.Method
path := ctx.Req.URL.Path
targetNode, found := s.router.findRoute(method, path)
if !found || targetNode.node.HandleFunc == nil {
ctx.Resp.WriteHeader(http.StatusNotFound)
_, _ = ctx.Resp.Write([]byte("not found"))
return
}
ctx.PathParams = targetNode.pathParams
targetNode.node.HandleFunc(ctx)
}
7.5.7 测试HTTPServer
server_test.go
:
func TestServer_Start(t *testing.T) {
s := NewHTTPServer()
wildcardHandleFunc := func(ctx *Context) {
ctx.Resp.Write([]byte("hello order wildcard"))
}
s.addRoute(http.MethodGet, "/order/*", wildcardHandleFunc)
handleFunc := func(ctx *Context) {
ctx.Resp.Write([]byte("hello order detail"))
}
s.addRoute(http.MethodGet, "/order/detail", handleFunc)
paramFunc := func(ctx *Context) {
ctx.Resp.Write([]byte(fmt.Sprintf("%s", ctx.PathParams)))
}
s.addRoute(http.MethodGet, "/user/:id", paramFunc)
err := s.Start(":8081")
if err != nil {
t.Fatal(err)
}
}
Last updated