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"