手把手和你一起實現一個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""