Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sinFrame

纯 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/sin
import "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)
}

Context API

类别 方法 说明
取参 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](冲突路由将被拒绝注册),测试与运行输出可能较"热闹",属预期行为。

许可

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages