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. 按/切割path

  • step2. 从路由树的根节点开始,按"层次"查找节点

  • 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个问题:

  1. 没有对target.children判空

  2. "在当前节点下查找子节点"是一个独立完整的功能,应该是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. 一个*只能表达1段路由

  2. 不做可回溯的路由匹配机制

6.2 通配符路由的注册

6.2.1 修改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()方法需要告知其调用者(也就是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结构体添加写入路由参数的方法

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()方法则返回一个*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.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()方法

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