手把手和你一起實現一個Web框架實戰——EzWeb框架(三)[Go語言筆記]Go項目實戰
代碼倉庫:
github
gitee
中文注釋,非常詳盡,可以配合食用
本篇代碼,請選擇demo3
這一篇文章我們進行動態路由解析功能的設計,
如xxx/:id/xxx,xxx/xxx/*mrxuexi.md
實現這處理這兩類模式的簡單小功能,實現起來不簡單,原有的map[path]HandlerFunc數據結構只能存儲靜態路由與方法對應,而無法處理動態路由,我們使用一種樹結構來進行路由表的存儲。
一、設計這個數據結構
1、節點結構體設計
type node struct {
path string /* 需要匹配的整體路由 */
part string /* 路由中的一部分,例如 :lang */
children []*node /* 存儲子節點們 */
isBlurry bool /* 如果模糊匹配則為true */
}
2、一個傳入part后,通過遍歷該節點的全部子節點們,找到擁有相同part的子節點的方法(返回首個)
func (n *node) matchChild(part string) *node {
//遍歷子節點們,對比子節點的part和part是否相同,是或者遍歷到的子節點支持模糊匹配則返回該子節點
for _, child := range n.children {
if child.part == part || child.isBlurry {
return child
}
}
return nil
}
3、一個返回匹配的子節點們的方法(返回全部,包括動態路由的存儲的部分)
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
//遍歷選擇滿足條件的子節點,加入到nodes中,然后返回
for _, child := range n.children {
if child.part == part || child.isBlurry {
nodes = append(nodes, child)
}
}
return nodes
}
4、構造路由表的插入方法,parts[]
存儲的是根據路由path分解出來的part們,我們拿到part則取檢索子節點是否存在這個part,不存在則新建一個子節點,不停的在這個樹上深入,直到遍歷完我們的全部part,然后遞歸返回。
//插入方法,用一個遞歸實現,找匹配的路徑直到找不到匹配當前part的節點,新建
func (n *node) insert(path string, parts []string, height int) {
//如果遍歷到底部了,則將我們的path存入節點,開始返回。遞歸的歸來條件。
if len(parts) == height{
n.path = path
return
}
//獲取這一節的part,並進行搜索
part := parts[height]
child := n.matchChild(part)
//若沒有搜索到匹配的子節點,則根據目前的part構造一個子節點
if child == nil {
child = &node{
part: part,
isBlurry: part[0] == ':' || part[0] == '*',
}
n.children = append(n.children, child)
}
child.insert(path, parts, height+1)
}
5、我們帶着part們一個個在存儲路由表的樹中查找,我們拿到某個節點的全部子節點,找到滿足part相同或者isBlurry:true
的節點。通過遞歸再往深處挖,挖下去直到發現某一級節點的子節點們,沒有對應匹配的part,又返回來,再去上一層的子節點看,這就是一個深度優先遍歷的情況。
//搜索方法
func (n *node) search(parts []string, height int) *node {
//如果節點到頭,或者存在*前綴的節點,開始返回
if len(parts) == height || strings.HasPrefix(n.part,"*") {
//如果此時遍歷到的n沒有存儲對應的path,說明未到目標最底層,則返回空
if n.path == "" {
return nil
}
return n
}
//搜索找到滿足part的子節點們放入children
part := parts[height]
children := n.matchChildren(part)
//接着遍歷子節點們,遞歸調用獲得下一級的子節點們,要走到頭的同時,找到了對應的節點,才返回最終我們找到的result
//這里為什么要遍歷子節點們進行深入搜索,因為它還存在滿足isBlurry:true的節點,我們也需要在其中深入搜索。
for _, child := range children {
result := child.search(parts, height+1)
if result != nil {
//返回滿足要求的節點
return result
}
}
return nil
}
二、更新路由表的存儲結構和處理方法
1、其中roots
中的第一層是roots[method]*node
type router struct {
//用於存儲相關方法
handlers map[string]HandlerFunc
//用於存儲每種請求方式的樹的根節點
roots map[string]*node
}
2、設計一個parsePath
方法,對外部傳入的路由根據"/"
進行分割,存入parts
// parsePath 用於處理傳入的url,先將其分開存儲到parts中,當然出現*前綴的部分就可以結束
func parsePath(path string) []string {
vs := strings.Split(path, "/")
parts := make([]string, 0)
for _, v := range vs {
if v != "" {
parts = append(parts, v)
if v[0] == '*' {
break
}
}
}
return parts
}
3、router
中 addRoute
方法,在 handlers map[string]HandlerFunc
中存入路由對應處理方法,進行路由注冊。存入形式為例如:{ "GET-/index" : 定義的處理方法 }
注意這里的path使我們用來構造路由表要存入的目標path
// router 中 addRoute 方法,在 handlers map[string]HandlerFunc 中存入路由對應處理方法
//存入形式為例如:{ "GET-/index" : 定義的處理方法 }
func (r *router) addRoute(method string, path string, handler HandlerFunc) {
parts := parsePath(path)
log.Printf("Route %4s - %s",method,path)
key := method + "-" + path
_, ok := r.roots[method]
//roots中不存在對應的方法入口則注冊相應方法入口
if !ok {
r.roots[method] = &node{}
}
//調用路由表插入方法,在該數據結構中插入該路由
r.roots[method].insert(path, parts, 0)
//把method-path作為key,以及handler方法作為value注入數據結構
r.handlers[key] = handler
}
4、做一個getRoute
方法,進入到對應路由樹,找到我們的路由,通過哈希表存入處理動態路由拿到param
和找到的*node
一起返回。
注意代碼中的n.path是我們注冊在路由表中的路由,path是外部傳入的!
func (r *router) getRoute(method string, path string) (*node, map[string]string) {
searchParts := parsePath(path)
params := make(map[string]string)
root, ok := r.roots[method]
if !ok {
return nil, nil
}
n := root.search(searchParts, 0)//傳入全部路徑的字符串數組,尋找到最后對應節點
if n != nil {
parts := parsePath(n.path) //n.path包含了完整的路由
for i, part := range parts {//遍歷這一條路徑
//拿到:的參數,存入params,方法中的part作為key,外面傳入的path中的數據作為value存入
if part[0] == ':' {
params[part[1:]] = searchParts[i]
}
//拿到*,此時路由表中的存入的part作為key,外面傳入的path中的數據作為value傳入params,之后也再沒有了
if part[0] == '*' && len(part) > 1{
params[part[1:]] = strings.Join(searchParts[i:],"/")
break
}
}
return n, params
}
return nil, nil
}
5.同時我們的hanle
方法和上一篇文章不同的是,不是直接拿外部傳入的path
直接在 handlers map[string]HandlerFunc
找對應的方法,因為我們外部傳入的path是動態的。我們是先通過getRoute
方法拿到參數和對應的找到存儲節點,用這個節點中存儲的path(它是靜態的,是我們之前注入的),再在 handlers map[string]HandlerFunc
找到對應的方法。
//根據context中存儲的 c.Method 和 c.Path 拿到對應的處理方法,進行執行,如果拿到的路由沒有注冊,則返回404
func (r *router) handle(c *Context) {
//獲取匹配到的節點,同時也拿到兩類動態路由中參數
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
c.Params = params
//拿目的節點中的path做key來找handlers
key := c.Method + "-" + n.path
r.handlers[key](c)
}else {
c.String(http.StatusNotFound,"404 NOT FOUND")
}
}
三、Context變更
1、修改Context結構體,構造Params來存放處理動態路由拿到的參數
// Context 結構體,內部封裝了 http.ResponseWriter, *http.Request
type Context struct {
Writer http.ResponseWriter
Req *http.Request
//請求的信息,包括路由和方法
Path string
Method string
Params map[string]string /*用於存儲外面拿到的參數 ":xxx" or "*xxx" */
//響應的狀態碼
StatusCode int
}
2、設計Param方法,拿到處理動態路由的獲取參數
// Param 是c的Param的value的獲取方法
func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}
隨便做個測試:
/*
@Time : 2021/8/16 下午4:01
@Author : mrxuexi
@File : main
@Software: GoLand
*/
package main
import (
"Ez"
"net/http"
)
func main() {
r := Ez.New()
r.POST("/hello/:id/*filepath", func(c *Ez.Context) {
c.JSON(http.StatusOK,Ez.H{
"name" : c.PostForm("name"),
"age" : c.PostForm("age"),
"id" : c.Param("id"),
"filepath" : c.Param("filepath"),
})
})
r.Run(":9090")
}
成功!
參考:
[1]: https://github.com/geektutu/7days-golang/tree/master/gee-web ""gee""
[2]: https://github.com/gin-gonic/gin ""gin""