纯 Go 标准库实现的迷你 Web 框架,零外部依赖。项目以学习为目的,用直白的代码展示一个 Web 框架的核心组成:前缀树路由、上下文封装与请求分发。
-
零依赖:仅使用 Go 标准库
-
基于 Trie(前缀树)的路由,支持三类路由模式:
模式 示例 匹配的请求路径 静态路由 /hello/hello参数路由 /hello/:name/hello/任意单段通配路由 /assets/*filepath/assets/ 及其下任意多段 -
匹配优先级:静态段 >
:参数段 >*通配段;某分支匹配失败会自动回溯到下一候选 -
路由冲突检测:静态段可与
:参数段、*通配段共存(如/user/list与/user/:id、/assets/js与/assets/*rest),匹配时静态优先、通配兜底;同一位置只允许一个通配段(:id与:name、:id与*rest互斥);冲突与重复注册均被拒绝,AddGet等注册接口以error返回结果(同时打印[WARNING]),Trie 与 handler 表始终同步 -
404 / 500 均由中间件承担:
middleware.NotFound()在整条链无人写出响应时兜底 404(不抢占已写出的响应),middleware.Recovery()捕获 panic 返回 500;不挂载NotFound时未匹配路由为空响应 -
路由表由
RWMutex保护,注册与请求分发并发安全 -
路由分组:
AddGroup派生子分组,支持嵌套与前缀逐级拼接(AddGroup("/api").AddGroup("/v1")→/api/v1);组内AddGet等注册自动拼接组前缀,引擎与分组共用同一套路由表 -
中间件:
AddMiddleware挂载全局或组级中间件,按「分组从外到内、组内注册顺序」执行;前缀匹配到完整路径段(/go分组不影响/golang/...),未匹配路由的 404 请求同样经过中间件 -
通用中间件:
sin/middleware子包提供Recovery()(panic 捕获 + 500)、Logger()(状态码与耗时日志,自动带请求 ID)、NotFound()(404 兜底)、RequestID()(透传 / 生成X-Request-Id)、CORS()(跨域支持,预检 204 直接应答)、Timeout()(限时执行,超时 504 并丢弃迟到写入)、BodyLimit()(请求体上限,超限 413)、Gzip()(响应压缩) -
拦截原语:
Context.Abort()终止后续处理链(鉴权 / 限流 / CORS 预检等拦截型中间件的基础),Set/Get请求级键值表供中间件与业务 handler 传值 -
静态文件:
Static("/static", "./static")把 URL 前缀映射到本地目录 -
HTML 模板:
SetFuncMap+LoadHTMLGlob加载模板,RetHtml(code, name, data)按文件名渲染 -
Context 封装:请求侧
Query/PostForm/Param取参,响应侧RetString/RetJson/RetHtml/RetData输出 -
JSON 请求体绑定:
BindJSON(obj)读取并解析 JSON 请求体(失败返回 error,业务自行应答),宽松版ShouldBind适合可选参数 -
优雅关闭:
RunWithConfig带读写超时启动,收到 SIGINT / SIGTERM 后先停止接受新连接、等活跃请求处理完毕再退出;启动失败(如端口占用)直接返回 error -
路由缓存:以「方法 + 请求路径」为键缓存 Trie 匹配结果,命中时跳过树搜索;容量可调(
SetRouteCacheSize,0 禁用)、可清空(ClearRouteCache)、可观测(GetRouteCacheStats),注册新路由自动失效
- Go 1.26+(见
go.mod)
go run main.go监听地址按 环境变量 ADDR > .env 文件 > 默认 :9999 的优先级动态解析。复制 .env.example 为 .env 即可修改端口(.env 已被 gitignore,不会入库):
Copy-Item .env.example .env使用 main.go 中注册的示例路由进行验证:
# 静态路由,渲染 HTML 模板
curl http://localhost:9999/
# => <h1>Hello sin</h1>
# 查询参数
curl "http://localhost:9999/hello?name=sin"
# => hello sin, you're at /hello
# 参数路由
curl http://localhost:9999/hello/sin
# => hello sin, you're at /hello/sin
# 通配路由,返回 JSON
curl http://localhost:9999/assets/css/sin.css
# => {"filepath":"css/sin.css"}
# 分组路由:/api/v1 前缀自动拼接
curl http://localhost:9999/api/v1/hello/sin
# => api v1 hello sin, you're at /api/v1/hello/sin
# 分组内 POST 表单
curl -X POST -d "username=sin" http://localhost:9999/api/v1/login
# => {"username":"sin"}
# 静态文件服务:/static/* 映射到 ./static 目录
curl http://localhost:9999/static/css/sin.css
# 错误恢复演示:handler panic,客户端收到 500,服务不中断
curl http://localhost:9999/panic
# => Internal Server Error
# 未匹配路由:由 NotFound 兜底中间件返回标准 404
curl http://localhost:9999/nope
# => 404 NOT FOUND: /nope
# CORS 预检:OPTIONS + Origin 直接 204 应答,不进入业务 handler(/api 分组挂了 CORS)
curl -X OPTIONS -H "Origin: https://example.com" -H "Access-Control-Request-Method: POST" `
http://localhost:9999/api/v1/login
# Timeout:/slow 组超过 100ms 的 handler 返回 504,迟到的业务写入被丢弃
curl http://localhost:9999/slow/too-slow
# => 504 Gateway Timeout: GET /slow/too-slow
# BodyLimit:/api 请求体上限 1KiB,超出直接 413(用大于 1KiB 的文件演示)
curl -X POST --data-binary @bigfile http://localhost:9999/api/v1/login
# => 413 Request Entity Too Large
# Gzip:客户端带 Accept-Encoding: gzip 时响应被压缩
curl -H "Accept-Encoding: gzip" --compressed http://localhost:9999/api/v1/hello/sin
# JSON 请求体绑定:BindJSON 解析请求体,非法 JSON 或缺 name 返回 400
curl -X POST -H "Content-Type: application/json" -d '{"name":"sin"}' http://localhost:9999/api/v1/echo
# => {"message":"hello sin"}
# 路由缓存统计:cache_count / cache_limit / cache_enabled
curl http://localhost:9999/admin/cache/stats
# => {"cache_count":5,"cache_enabled":true,"cache_limit":1000}
# 优雅关闭:Ctrl+C(SIGINT)后服务先处理完存量请求再退出(默认上限 10s)sinFrame 是标准 Go 模块(github.com/898601566/sinFrame),框架本体在 sin 子包,任何项目都可以直接引用:
go mod init yourapp
go get github.com/898601566/sinFrame/sinimport "github.com/898601566/sinFrame/sin"根目录的 main.go 只是本仓库自带的使用示例,不会被引入;未打版本标签时也可用分支 / 提交哈希引用(如 go get github.com/898601566/sinFrame/sin@main,得到伪版本号)。
package main
import (
"log"
"net/http"
"github.com/898601566/sinFrame/sin"
"github.com/898601566/sinFrame/sin/middleware"
)
func main() {
// 引擎类型未导出,需通过类型推断使用
s := sin.NewSin()
// 全局中间件标准栈:错误恢复(panic -> 500)、请求日志、404 兜底
s.AddMiddleware(middleware.Recovery(), middleware.Logger(), middleware.NotFound())
// 注册接口返回 error(路由冲突 / 重复注册时非 nil),示例中统一快速失败
must := func(err error) {
if err != nil {
log.Fatal(err)
}
}
must(s.AddGet("/", func(c *sin.Context) {
// 渲染 templates/index.html(须先 LoadHTMLGlob 加载)
c.RetHtml(http.StatusOK, "index.html", sin.H{"name": "sin"})
}))
must(s.AddGet("/hello/:name", func(c *sin.Context) {
// 访问 /hello/sin 时,Param("name") == "sin"
c.RetString(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
}))
must(s.AddPost("/login", func(c *sin.Context) {
// 表单提交 username
c.RetJson(http.StatusOK, sin.H{
"username": c.PostForm("username"),
})
}))
// 路由分组:前缀逐级拼接(/api + v1 -> /api/v1),组内注册自动带前缀
api := s.AddGroup("/api")
v1 := api.AddGroup("v1")
must(v1.AddGet("/hello/:name", func(c *sin.Context) {
// 访问 /api/v1/hello/sin 时命中
c.RetString(http.StatusOK, "api v1 hello %s\n", c.Param("name"))
}))
// 静态文件服务:/static/* 映射到 ./static 目录
must(s.Static("/static", "./static"))
log.Fatal(s.Run(":9999"))
}注意:路由表由
RWMutex保护,注册与请求分发并发安全;仍建议在调用Run启动服务前完成全部注册,行为更可预期。
// 简单模式:无超时控制,收到中断信号时连接直接断开
log.Fatal(s.Run(":9999"))
// 优雅关闭模式:创建带读写超时的 http.Server,收到 SIGINT / SIGTERM 后
// 先停止接受新连接、等活跃请求处理完毕(上限 ShutdownTimeout)再退出;
// 启动失败(如端口被占用)立即返回 error,而不是挂在等信号上
config := sin.DefaultServerConfig(":9999") // 30s / 30s / 10s / 1MB
config.ShutdownTimeout = 15 * time.Second // 按需覆盖单项
if err := s.RunWithConfig(config); err != nil {
log.Fatal(err)
}| 类别 | 方法 | 说明 |
|---|---|---|
| 取参 | Query(key) |
URL 查询参数,如 /hello?name=sin |
PostForm(key) |
表单参数(取不到时回退到 URL 查询参数) | |
Param(key) |
路由参数,来自 :name 或 *filepath 段 |
|
| 绑定 | BindJSON(obj) |
读取请求体并按 JSON 解析到 obj(失败返回 error,业务自行应答) |
ShouldBind(obj) |
宽松版绑定:失败仅返回 false,适合可选参数 | |
| 响应 | RetString(code, format, a...) |
纯文本,format 语法同 fmt.Sprintf |
RetJson(code, v) |
JSON(自动设置 Content-Type) | |
RetHtml(code, name, data) |
渲染 HTML 模板(须先 SetFuncMap + LoadHTMLGlob 加载) |
|
RetData(code, data) |
原始字节,不设置 Content-Type | |
| 链路 | Next() |
执行处理链中剩余的 handler;中间件内调用可包裹后续链路(前置 / 后置逻辑) |
Abort() / IsAborted() |
终止后续处理链 / 查询是否已终止(拦截型中间件用) | |
Set(key, v) / Get(key) |
请求级键值表,中间件与业务 handler 传值(如 RequestID、当前用户) | |
| 其他 | Path / Method |
请求路径与方法 |
SetStatusCode(code) / SetHeader(k, v) |
状态码与响应头 |
sinFrame/
├── main.go # 使用示例
├── templates/ # HTML 模板(LoadHTMLGlob 加载,RetHtml 渲染)
│ └── index.html
├── static/ # 静态文件目录(s.Static 提供服务)
│ └── css/sin.css
├── sin/ # 框架本体
│ ├── sin.go # 引擎:注册入口、中间件收集与分发、模板加载、Run / RunWithConfig
│ ├── router.go # 路由器:Trie + handler map 两套数据结构、请求分发
│ ├── node.go # 前缀树节点:插入 / 搜索 / 冲突检测 / 树形打印
│ ├── group.go # 路由分组:前缀拼接、中间件挂载、静态文件服务
│ ├── middleware/ # 通用中间件子包
│ │ ├── logger.go # Logger:请求日志(状态码 + 耗时,可带请求 ID)
│ │ ├── recover.go # Recovery:panic 捕获、记录调用栈、返回 500
│ │ ├── notfound.go # NotFound:链尾无人响应时兜底 404
│ │ ├── requestid.go # RequestID:透传 / 生成 X-Request-Id
│ │ ├── cors.go # CORS:跨域头与预检应答
│ │ ├── timeout.go # Timeout:限时执行,超时 504、迟到写入丢弃
│ │ ├── bodylimit.go # BodyLimit:请求体上限,超限 413
│ │ └── gzip.go # Gzip:响应压缩(惰性启用)
│ ├── context.go # Context:请求与响应封装、处理链驱动(Next)
│ ├── router_test.go # 路由匹配与参数解析测试
│ ├── node_test.go # Trie 插入 / 搜索 / 优先级测试
│ ├── group_test.go # 分组与静态文件测试
│ ├── context_test.go # Context 取参、绑定、响应与模板渲染测试
│ ├── sin_test.go # 引擎生命周期测试(默认配置 / 优雅关闭 / 端口占用)
│ └── middleware_test.go # 中间件链、作用域与 404 链测试
├── CLAUDE.md # Claude Code 项目指引
├── .gitignore
└── go.mod
go build ./... # 编译
go test ./... # 运行全部测试
go test -race ./sin # 竞态检测(验证路由表并发安全,需 cgo/gcc)
go test ./sin -run TestGetRoute # 运行单个测试(-run 支持正则)
go test ./sin -cover # 查看测试覆盖率
go vet ./... # 静态检查
gofmt -l . # 格式检查(应无输出)说明:路由冲突与重复注册会打印 [WARNING](冲突路由将被拒绝注册),测试与运行输出可能较"热闹",属预期行为。