一、基础 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)
}
九、关键优化方向
- 路由匹配算法:使用 Trie 树或 Radix 树提高性能
- Context 池:使用
sync.Pool
重用 Context 对象 - 参数绑定:实现自动的 JSON/Form 数据绑定
- 错误处理:统一错误处理中间件
- 测试覆盖:添加单元测试和基准测试
十、 路由设计
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
字段快速查找子节点。 - 优先级排序:根据节点的优先级调整匹配顺序。
- 减少内存分配:预分配内存,避免频繁分配。