本节我们来实现框架的基础路由部分,我们会按照5步循序渐进的实现这个路由。
go原生实现基础路由
go的http包很强大,可以直接实现基础路由做测试用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintln(writer, "hello 我们用原生go实现了一个接口")
})
err := http.ListenAndServe(":8111", nil)
if err != nil {
log.Fatal(err)
}
}
|
以上的代码就实现了一个web接口,开启8111端口,可以直接访问localhost:8111/hello
,页面会输出hello 我们用原生go实现了一个接口
。
很明显当我们要实现n多接口时,要写很多模版语法,重复操作很多。
1. 最简单的路由实现
为了实现路由,我们需要
- 首先抽象一个
router结构体
,具备一个handlerMap
的map存储不同的http.HandleFunc,那这个结构体还要具备什么功能呢,它需要能够添加不同的路由,也就是基本的Add 功能做指针接收器,实现添加http.HandleFunc
。
- 这个server.go提供一个New函数,暴露一个路由引擎,改引擎具有一个Run的指针接收器,实现
http.ListenAndServe
。
简约的server.go代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package xxx
import (
"log"
"net/http"
)
type HandleFunc func(w http.ResponseWriter, r *http.Request)
type router struct {
handleFuncMap map[string]HandleFunc
}
func (r *router) Add(name string, handleFunc HandleFunc) {
r.handleFuncMap[name] = handleFunc
}
type Engine struct {
router
}
func New() *Engine {
return &Engine{
router: router{handleFuncMap: map[string]HandleFunc{}},
}
}
func (e *Engine) Run() {
for key, value := range e.handleFuncMap {
http.HandleFunc(key, value)
}
err := http.ListenAndServe(":8111", nil)
if err != nil {
log.Fatal(err)
}
}
|
在我们的main.go里面,我们可以这么使用:
1
2
3
4
5
6
7
|
//2. 使用手写框架实现
engine := xxx.New()
engine.Add("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s 欢迎来到手写web框架", "wushao")
})
engine.Run()
|
这时候,我们在页面上就可以正常访问localhost:8111/hello
了,用这种方式,我们应用层只需要不断注册路由,实现自己的handler中的业务逻辑就行,相对第一节省略了一些重复操作。
但是,我们真实业务中,路由肯定是要分组的,也就是一类路由有统一的前缀,目的是为了保证写的接口归属于某一个模块,这样便于管理以及维护,代码也更为清晰,比如:/user/getUser
/user/createUser
都同属于user模块
2. 实现分组路由
为了实现分组路由,我们需要:
- 引入新的结构体
routerGroup
,做分组用,我们再想一下,原来的handleFuncMap、Add的指针接收器也要归属于routerGroup
,通过组来实现用来路由的功能,比如/user
组下添加具体的/getUser /createUser
,实现不同的接口。
- 原来的router结构体,需要
routerGroups
切片来存储不同的routerGroup,它需要一个Group的指针接收器,来实现分组的功能
- 原来的Run函数,就需要双层遍历了,先遍历组,再遍历组里面不同的handleFunc实现
改造server.go代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
type HandleFunc func(w http.ResponseWriter, r *http.Request)
// 路由组
type routerGroup struct {
name string
handleFuncMap map[string]HandleFunc
}
func (r *routerGroup) Add(name string, handleFunc HandleFunc) {
r.handleFuncMap[name] = handleFunc
}
// 3、user /get/list user组下面才是url
//路由表 由路由组组成
type router struct {
routerGroups []*routerGroup
}
func (r *router) Group(name string) *routerGroup {
group := &routerGroup{
//handleFuncMap: map[string]HandleFunc{},
// 和上面的写法一个效果
handleFuncMap: make(map[string]HandleFunc),
name: name,
}
r.routerGroups = append(r.routerGroups, group)
return group
}
type Engine struct {
*router //这样写的原因是啥呢
}
func New() *Engine {
return &Engine{
&router{},
}
}
func (e *Engine) Run() {
for _, group := range e.routerGroups {
for key, value := range group.handleFuncMap {
groupNameHasSlash := strings.HasPrefix(group.name, "/")
routerKeyHasSlash := strings.HasPrefix(key, "/")
var groupName string
var handleFuncName string
if groupNameHasSlash {
groupName = group.name
} else {
groupName = "/" + group.name
}
if routerKeyHasSlash {
handleFuncName = groupName + key
} else {
handleFuncName = groupName + "/" + key
}
http.HandleFunc(handleFuncName, value)
}
}
|
在上面的代码中我们兼容处理了用户注册url时没有写/
的情况。
main.go使用时稍微更改一下,先注册组,在注册组下面的handleFuc
1
2
3
4
5
6
7
|
//2. 使用手写框架实现
groupUser := engine.Group("user")
groupUser.Add("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s 欢迎来到手写web框架", "wushao")
})
engine.Run()
|
这时候,我们在页面上就可以访问localhost:8111/user/hello
了,再改造的过程中可以发现我们是一步步的向上抽象的过程,顶层现在是group。
但是,等一等,让我们回想一下第一节课中的路由分析部分,当时我们说有两个重点
要实现,其中第一点就是请求的方法是什么,到目前为止,我们的路由只是能访问url,但是什么类型的http请求我们无法知道。
3. 支持不同的请求方式
为了让我们的路由支持不同的http请求方式,我们需要继续改造我们的server.go:
- routerGroup需要一个handlerMethodMap存储我们的组中的不同的url属于什么http方法,类似于
1
2
3
4
|
{
"get": ["/getUser", "/hi"],
"post": ["/createUser"]
}
|
就是说user组下面的/getUser
是get方法,/createUser
是post方法,即:
1
2
|
GET http://localhost:8111/user/getUser
POST http://localhost:8111/user/createUser
|
- routerGroup的Add方法不再需要,因为我们现在要根据不同的请求方式来注册对应的handlerMap
- engine引擎的Run方法不能再简单的
http.HandleFunc
了,我们要把所有的请求全拦截http.Handle
,在拦截里面在做处理。
在go的net/http/server.go源码中我们可以知道http.Handle的详细签名
1
2
3
4
5
|
// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
|
注意其中的Handler,他是一个interface:
1
2
3
|
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
|
所以我们要实现ServeHTTP方法,即实现了Handler接口,也就是我们的engine引擎需要一个ServeHTTP的指针接收器。
我们自己的server.go的对应修改如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
// 路由组
type routerGroup struct {
name string
handlerFuncMap map[string]HandleFunc
handlerMethodMap map[string][]string
}
//去掉Add方法 添加下面的具体的请求方式的指针接收器
func (r *routerGroup) Any(name string, handlerFunc HandleFunc) {
r.handlerFuncMap[name] = handlerFunc
r.handlerMethodMap["Any"] = append(r.handlerMethodMap["any"], name)
}
func (r *routerGroup) Get(name string, handlerFunc HandleFunc) {
r.handlerFuncMap[name] = handlerFunc
r.handlerMethodMap[http.MethodGet] = append(r.handlerMethodMap[http.MethodGet], name)
}
func (r *routerGroup) Post(name string, handlerFunc HandleFunc) {
r.handlerFuncMap[name] = handlerFunc
r.handlerMethodMap[http.MethodPost] = append(r.handlerMethodMap[http.MethodPost], name)
}
// ... 把你需要实现的http方法都按照上面的格式加入即可
func (r *router) Group(name string) *routerGroup {
group := &routerGroup{
handleFuncMap: make(map[string]HandleFunc),
name: name,
//初始化时一定要注意routerGroup结构体增加新属性,这里就有初始化
handlerMethodMap: make(map[string][]string),
}
r.routerGroups = append(r.routerGroups, group)
return group
}
func (e *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
// 先拿到请求的方法类型 GET POST啥的
method := request.Method
groups := e.router.routerGroups
// 根据url进行匹配
for _, group := range groups {
for name, methodHandle := range group.handlerFuncMap {
groupNameHasSlash := strings.HasPrefix(group.name, "/")
routerKeyHasSlash := strings.HasPrefix(name, "/")
var groupName string
var requestUrl string
if groupNameHasSlash {
groupName = group.name
} else {
groupName = "/" + group.name
}
if routerKeyHasSlash {
requestUrl = groupName + name
} else {
requestUrl = groupName + "/" + name
}
//http.HandleFunc(requestUrl, methodHandle)
// 比较请求url和拼接的url是否相同
if request.RequestURI == requestUrl {
// 先看看属不属于any类型的
routers, ok := group.handlerMethodMap["Any"]
if ok {
for _, routerName := range routers {
// 比较map中存储的切片中的最后一级url和name
if routerName == name {
methodHandle(writer, request)
return
}
}
}
// any中不ok 就去methodMap中遍历
routers, ok = group.handlerMethodMap[method]
if ok {
for _, routerName := range routers {
// 比较map中存储的切片中的最后一级url和name
if routerName == name {
methodHandle(writer, request)
return
}
}
}
// 具体method中不ok 就报错了
writer.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(writer, "%s %s not allowed \n", request.RequestURI, method)
return
}
}
}
// url不匹配
writer.WriteHeader(http.StatusNotFound)
fmt.Fprintf(writer, "%s %s not found \n", request.RequestURI, method)
}
func (e *Engine) Run() {
// 3、支持不同的方法,全拦截
http.Handle("/", e)
err := http.ListenAndServe(":8111", nil)
if err != nil {
log.Fatal(err)
}
}
|
我们在main.go使用时再稍微更改一下:
1
2
3
4
5
6
7
8
9
10
|
//2. 使用手写框架实现
engine := xxx.New()
groupUser := engine.Group("user")
groupUser.Get("/getUser", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s 欢迎来到手写web框架", "wushao")
})
groupUser.Post("/createUser", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s 2欢迎来到手写web框架", "wushao")
})
engine.Run()
|
到此为止,我们终于能够访问到下面的这种支持不同http请求方式的url了。
1
2
|
GET http://localhost:8111/user/getUser
POST http://localhost:8111/user/createUser
|
为了实现支持不同请求方式的路由组,我们:
- 进一步抽象了路由组的方法,按照不同http请求存储二级url
- 并且简化了Run函数的监听,把比较重的业务逻辑放到了Handler接口的ServeHTTP方法中,在Engine的指针接收器里面实现这个方法
但是,上述的代码还存在一个问题,restful规范中是可以可以支持同一个路由使用不同的请求方式,例如
1
2
|
GET http://localhost:8111/user/user
POST http://localhost:8111/user/user
|
按照目前的写法,我们的代码是哪个方法类型的后注册,只能访问到哪个,前一个就会被覆盖掉。
4. 同一路径支持不同的请求方式,引入上下文方式
为了修改第三节中遗留的问题,我们需要:
- 继续改造routerGroup结构体的handlerFuncMap属性,因为目前我们无法区分出二级url的方法类型,所以我们要:
1
2
3
|
handlerFuncMap map[string]HandleFunc
// 同一路径的不同请求方式
handlerFuncMap map[string]map[string]HandleFunc
|
这样我们的handlerFuncMap存储的结构就由:
1
2
3
|
{
"/user": HandleFunc,
}
|
变成了:
1
2
3
4
5
6
|
{
"/user": {
"post": HandleFunc,
"get": HandleFunc,
}
}
|
- 改造了
handlerFuncMap
后,我们势必需要把routerGroup的方法处理函数进行修改,需要加上method字段,同时抽象公共函数handle简化操作,并改造原来的支持不同请求方式的那些函数,简化ServeHTTP。
- 同时提取一个上下文context的结构体,简化
type HandleFunc func(w http.ResponseWriter, r *http.Request)
中的形参
对应的我们的server.go的对应修改如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
const Any = "ANY"
// HandleFunc 使用上下文结构体改造
type HandleFunc func(ctx *Context)
// 路由组
type routerGroup struct {
name string
// 同一路径的不同请求方式
handlerFuncMap map[string]map[string]HandleFunc
//支持不同的请求方式 {"post": ["/hi", "/hello"]}
handlerMethodMap map[string][]string
}
// 增加新的统一handle方法
func (r *routerGroup) handle(name string, method string, handleFunc HandleFunc) {
_, ok := r.handlerFuncMap[name]
if !ok {
r.handlerFuncMap[name] = make(map[string]HandleFunc)
}
_, ok = r.handlerFuncMap[name][method]
if ok {
panic("有重复路由")
}
r.handlerFuncMap[name][method] = handleFunc
r.handlerMethodMap[method] = append(r.handlerMethodMap[method], name)
}
func (r *routerGroup) Any(name string, handlerFunc HandleFunc) {
r.handle(name, Any, handlerFunc)
}
func (r *routerGroup) Get(name string, handlerFunc HandleFunc) {
r.handle(name, http.MethodGet, handlerFunc)
}
func (r *routerGroup) Post(name string, handlerFunc HandleFunc) {
r.handle(name, http.MethodPost, handlerFunc)
}
func (r *router) Group(name string) *routerGroup {
group := &routerGroup{
//初始化handlerFuncMap修改
handlerFuncMap: make(map[string]map[string]HandleFunc),
name: name,
handlerMethodMap: make(map[string][]string),
}
r.routerGroups = append(r.routerGroups, group)
return group
}
func (e *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
// 先拿到请求的方法类型 GET POST啥的
method := request.Method
groups := e.router.routerGroups
// 根据url进行匹配
for _, group := range groups {
for name, methodHandle := range group.handlerFuncMap {
groupNameHasSlash := strings.HasPrefix(group.name, "/")
routerKeyHasSlash := strings.HasPrefix(name, "/")
var groupName string
var requestUrl string
if groupNameHasSlash {
groupName = group.name
} else {
groupName = "/" + group.name
}
if routerKeyHasSlash {
requestUrl = groupName + name
} else {
requestUrl = groupName + "/" + name
}
//http.HandleFunc(requestUrl, methodHandle)
// 比较请求url和拼接的url是否相同
if request.RequestURI == requestUrl {
//构造上下文
ctx := &Context{
W: writer,
R: request,
}
// 先看看属不属于any类型的
handle, ok := methodHandle[Any]
if ok {
handle(ctx)
return
}
handle, ok = methodHandle[method]
if ok {
handle(ctx)
return
}
// 具体method中不ok 就报错了
writer.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(writer, "%s %s not allowed \n", request.RequestURI, method)
return
}
}
}
// url不匹配
writer.WriteHeader(http.StatusNotFound)
fmt.Fprintf(writer, "%s %s not found \n", request.RequestURI, method)
}
|
经过上面的改造我们在main.go中使用就不会存在同一个url不能访问不同请求方式的问题了。
1
2
3
4
5
6
7
8
9
|
//2. 使用手写框架实现
engine := msgo.New()
groupUser := engine.Group("user")
groupUser.Get("/user", func(ctx *xxx.Context) {
fmt.Fprintf(ctx.W, "%s get 欢迎来到手写web框架", "wushao")
})
groupUser.Post("/user", func(ctx *xxx.Context) {
fmt.Fprintf(ctx.W, "%s post 欢迎来到手写web框架", "wushao")
})
|
文章作者
吴少林
上次更新
2022-09-16
(3c97a70)
许可协议
MIT