0
点赞
收藏
分享

微信扫一扫

go 从零单排星耀之质量局:造轮子miniweb

一、基础 HTTP 服务器

任何 Web 框架的核心都是基于 Go 标准库的 net/http 包。我们从最简单的 HTTP 服务器开始:

// main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, MiniWeb!")
    })
    http.ListenAndServe(":8080", nil)
}

二、实现路由系统

1. 定义路由结构

// router.go
package miniweb

import "net/http"

type Router struct {
    routes map[string]map[string]http.HandlerFunc // method -> path -> handler
}

func NewRouter() *Router {
    return &Router{
        routes: make(map[string]map[string]http.HandlerFunc),
    }
}

// 添加路由
func (r *Router) AddRoute(method, path string, handler http.HandlerFunc) {
    if r.routes[method] == nil {
        r.routes[method] = make(map[string]http.HandlerFunc)
    }
    r.routes[method][path] = handler
}

// 实现 http.Handler 接口
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    method := req.Method
    path := req.URL.Path

    if handlers, ok := r.routes[method]; ok {
        if handler, ok := handlers[path]; ok {
            handler(w, req)
            return
        }
    }

    // 未找到路由
    http.NotFound(w, req)
}

2. 使用路由

// main.go
package main

import (
    "miniweb"
    "net/http"
)

func main() {
    router := miniweb.NewRouter()
    router.AddRoute("GET", "/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello from MiniWeb!"))
    })
    
    http.ListenAndServe(":8080", router)
}

三、实现上下文(Context)

封装请求和响应,方便传递数据:

// context.go
package miniweb

import (
    "encoding/json"
    "net/http"
)

type Context struct {
    Writer  http.ResponseWriter
    Request *http.Request
    Params  map[string]string // 用于存储路由参数(后续扩展)
}

// 返回 JSON 数据
func (c *Context) JSON(code int, obj interface{}) {
    c.Writer.Header().Set("Content-Type", "application/json")
    c.Writer.WriteHeader(code)
    json.NewEncoder(c.Writer).Encode(obj)
}

// 返回字符串
func (c *Context) String(code int, format string, values ...interface{}) {
    c.Writer.Header().Set("Content-Type", "text/plain")
    c.Writer.WriteHeader(code)
    c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}

四、改进路由支持中间件

1. 定义中间件类型

// router.go
type HandlerFunc func(*Context)

// 中间件链
type Middleware func(HandlerFunc) HandlerFunc

type Router struct {
    routes      map[string]map[string]HandlerFunc
    middlewares []Middleware
}

// 注册中间件
func (r *Router) Use(middleware Middleware) {
    r.middlewares = append(r.middlewares, middleware)
}

// 创建处理链
func (r *Router) createHandler(handler HandlerFunc) HandlerFunc {
    for i := len(r.middlewares) - 1; i >= 0; i-- {
        handler = r.middlewares[i](handler)
    }
    return handler
}

2. 中间件示例

// 日志中间件
func Logger() Middleware {
    return func(next HandlerFunc) HandlerFunc {
        return func(c *Context) {
            start := time.Now()
            next(c)
            log.Printf("Request handled in %v", time.Since(start))
        }
    }
}

// 使用中间件
router.Use(Logger())

五、支持动态路由(参数匹配)

使用 Trie 树或正则表达式实现高效路由匹配,这里简化实现:

// router.go
func (r *Router) AddRoute(method, path string, handler HandlerFunc) {
    // 简单实现参数匹配(例如 /user/:id)
    if strings.Contains(path, ":") {
        // 转换为正则表达式并存储
        // 这里需要更复杂的实现(留给读者扩展)
    }
    // ...原有逻辑
}

六、实现模板渲染

// context.go
func (c *Context) HTML(code int, tmpl string, data interface{}) {
    c.Writer.Header().Set("Content-Type", "text/html")
    c.Writer.WriteHeader(code)
    
    t, err := template.ParseFiles(tmpl)
    if err != nil {
        c.String(500, "Template Error: %v", err)
        return
    }
    t.Execute(c.Writer, data)
}

七、静态文件服务

// router.go
func (r *Router) Static(prefix, dir string) {
    fs := http.FileServer(http.Dir(dir))
    handler := http.StripPrefix(prefix, fs)
    
    r.AddRoute("GET", prefix+"/*", func(c *Context) {
        handler.ServeHTTP(c.Writer, c.Request)
    })
}

八、最终使用示例


package main

import (
    "miniweb"
    "net/http"
)

func main() {
    r := miniweb.NewRouter()
    
    // 注册中间件
    r.Use(miniweb.Logger())
    
    // 静态文件
    r.Static("/static", "./static")
    
    // 路由
    r.AddRoute("GET", "/", func(c *miniweb.Context) {
        c.String(200, "Welcome to MiniWeb!")
    })
    
    r.AddRoute("GET", "/user/:id", func(c *miniweb.Context) {
        c.JSON(200, map[string]string{"id": c.Params["id"]})
    })
    
    http.ListenAndServe(":8080", r)
}

九、关键优化方向

  1. 路由匹配算法:使用 Trie 树或 Radix 树提高性能
  2. Context 池:使用 sync.Pool 重用 Context 对象
  3. 参数绑定:实现自动的 JSON/Form 数据绑定
  4. 错误处理:统一错误处理中间件
  5. 测试覆盖:添加单元测试和基准测试

十、 路由设计

1. 设计路由树节点

首先,我们需要定义路由树的节点结构。每个节点需要包含以下信息:

  • path:当前节点的路径片段。
  • children:子节点列表。
  • handlers:与路径关联的处理函数。
  • wildChild:是否包含通配符子节点。
  • nType:节点类型(静态、参数、通配符)。

type nodeType uint8

const (
    static nodeType = iota // 静态路由
    param                 // 参数路由,如 :id
    catchAll              // 通配符路由,如 *
)

type node struct {
    path      string         // 当前节点的路径片段
    children  []*node        // 子节点
    handlers  HandlersChain  // 处理函数链
    wildChild bool           // 是否包含通配符子节点
    nType     nodeType       // 节点类型
    indices   string         // 子节点索引(用于快速查找)
}

2. 插入路由

插入路由的逻辑是前缀树的核心。我们需要处理以下情况:

  • 静态路由(如 /user)。
  • 参数路由(如 /user/:id)。
  • 通配符路由(如 /static/*filepath)。

以下是插入路由的代码实现:

func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path
    n.priority++

    // 空树,直接插入
    if len(n.path) == 0 && len(n.children) == 0 {
        n.insertChild(path, fullPath, handlers)
        n.nType = static
        return
    }

walk:
    for {
        // 找到最长公共前缀
        i := longestCommonPrefix(path, n.path)

        // 分裂节点
        if i < len(n.path) {
            child := &node{
                path:      n.path[i:],
                wildChild: n.wildChild,
                nType:     static,
                indices:   n.indices,
                children:  n.children,
                handlers:  n.handlers,
            }

            n.children = []*node{child}
            n.indices = string(n.path[i])
            n.path = path[:i]
            n.handlers = nil
            n.wildChild = false
        }

        // 处理剩余路径
        if i < len(path) {
            path = path[i:]

            // 参数路由(如 :id)
            if path[0] == ':' {
                n.insertParam(path, fullPath, handlers)
                return
            }

            // 通配符路由(如 *)
            if path[0] == '*' {
                n.insertCatchAll(path, fullPath, handlers)
                return
            }

            // 静态路由
            c := path[0]
            for i, max := 0, len(n.indices); i < max; i++ {
                if c == n.indices[i] {
                    n = n.children[i]
                    continue walk
                }
            }

            // 插入新节点
            n.indices += string(c)
            child := &node{
                path: path,
            }
            n.children = append(n.children, child)
            n = child
            n.insertChild(path, fullPath, handlers)
            return
        }

        // 注册处理函数
        if n.handlers != nil {
            panic("handlers already registered for path: " + fullPath)
        }
        n.handlers = handlers
        return
    }
}

3. 查找路由

查找路由的逻辑与插入类似,需要根据路径片段匹配节点,并处理参数和通配符。

func (n *node) getRoute(path string) (handlers HandlersChain, params map[string]string) {
walk:
    for {
        // 匹配路径
        if len(path) > len(n.path) {
            if path[:len(n.path)] == n.path {
                path = path[len(n.path):]

                // 参数路由
                if n.nType == param {
                    end := 0
                    for end < len(path) && path[end] != '/' {
                        end++
                    }
                    if params == nil {
                        params = make(map[string]string)
                    }
                    params[n.path[1:]] = path[:end]
                    path = path[end:]
                }

                // 通配符路由
                if n.nType == catchAll {
                    if params == nil {
                        params = make(map[string]string)
                    }
                    params[n.path[1:]] = path
                    return n.handlers, params
                }

                // 静态路由
                c := path[0]
                for i, max := 0, len(n.indices); i < max; i++ {
                    if c == n.indices[i] {
                        n = n.children[i]
                        continue walk
                    }
                }
            }
        } else if path == n.path {
            return n.handlers, params
        }

        return nil, nil
    }
}

4. 辅助函数

计算最长公共前缀

func longestCommonPrefix(a, b string) int {
    i := 0
    for i < len(a) && i < len(b) && a[i] == b[i] {
        i++
    }
    return i
}

插入参数路由

func (n *node) insertParam(path, fullPath string, handlers HandlersChain) {
    end := 1
    for end < len(path) && path[end] != '/' {
        end++
    }

    child := &node{
        path:     path[:end],
        nType:    param,
        fullPath: fullPath,
    }

    n.children = append(n.children, child)
    n.wildChild = true
    n = child

    if end < len(path) {
        path = path[end:]
        n.insertChild(path, fullPath, handlers)
    } else {
        n.handlers = handlers
    }
}

插入通配符路由

func (n *node) insertCatchAll(path, fullPath string, handlers HandlersChain) {
    child := &node{
        path:     path,
        nType:    catchAll,
        handlers: handlers,
        fullPath: fullPath,
    }

    n.children = append(n.children, child)
    n.wildChild = true
}

5. 测试路由树

以下是一个简单的测试示例:

func main() {
    root := &node{}

    root.addRoute("/user/:id", HandlersChain{func(c *Context) {
        fmt.Println("User ID:", c.Params["id"])
    }})

    root.addRoute("/static/*filepath", HandlersChain{func(c *Context) {
        fmt.Println("Filepath:", c.Params["filepath"])
    }})

    handlers, params := root.getRoute("/user/123")
    if handlers != nil {
        c := &Context{Params: params}
        handlers[0](c)
    }

    handlers, params = root.getRoute("/static/css/style.css")
    if handlers != nil {
        c := &Context{Params: params}
        handlers[0](c)
    }
}

6. 性能优化

  • 索引优化:使用 indices 字段快速查找子节点。
  • 优先级排序:根据节点的优先级调整匹配顺序。
  • 减少内存分配:预分配内存,避免频繁分配。
举报

相关推荐

0 条评论