From 3bbf4627a1c9f6c2750c2534ef87e908bdf9999e Mon Sep 17 00:00:00 2001 From: k2safe Date: Mon, 21 Sep 2026 00:57:06 +0800 Subject: [PATCH] feat: remove Gin and standardize on nethttp --- CHANGELOG.md | 8 ++ PERFORMANCE.md | 20 +++++ README.md | 26 +++--- ROADMAP.md | 24 +++-- app/app.go | 8 +- app_test.go | 12 ++- auth/auth_test.go | 5 +- cmd/go-omni/README.md | 8 +- cmd/go-omni/scaffold/version.go | 2 +- config/config.go | 6 -- config/config_test.go | 8 +- config/env.go | 1 - examples/reference/README.md | 6 +- examples/reference/main_test.go | 7 +- go.mod | 19 +--- go.sum | 61 ------------- httpx/context.go | 9 +- logx/README.md | 2 +- logx/logx.go | 2 +- middleware/middleware.go | 4 +- omni.go | 2 - scripts/verifyBusiness.py | 3 +- scripts/verifyDatabase.py | 4 +- scripts/verifyPerformance.py | 38 ++++++++ scripts/verifyScaffold.py | 6 +- scripts/verifyTransports.py | 8 +- transport/README.md | 8 +- transport/benchmark_test.go | 48 ++++++++++ transport/contract_test.go | 13 +-- transport/gin/router.go | 124 -------------------------- transport/internal/routing/pattern.go | 2 +- transport/nethttp/router.go | 4 +- transport/nethttp/router_test.go | 2 +- validate/validate.go | 2 +- 34 files changed, 197 insertions(+), 305 deletions(-) create mode 100644 PERFORMANCE.md create mode 100755 scripts/verifyPerformance.py create mode 100644 transport/benchmark_test.go delete mode 100644 transport/gin/router.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b5b0ef..ac7ca46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v0.4.0 + +- **破坏性变更**:移除 Gin 适配器及其全部依赖,`omni.New()` 默认使用只依赖标准库的 `transport/nethttp`。 +- 移除 Gin 专属的 `Config.Mode` / `WithMode` API;网络层运行模式由具体适配器或启动层自行配置。 +- 公共 `httpx.Router`、`transport/transporttest.Run` 和 `omni.NewWithRouter` 保留,自研网络层无需修改业务 Handler / Service 即可接入。 +- benchmark 改为标准库路由和完整 GoOmni 请求链,新增 `scripts/verifyPerformance.py` 作为 race、vet、build 和性能基线入口。 +- 远程业务试用、脚手架、适配器契约和 MySQL 验收全部迁移到 `v0.4.0`。 + ## v0.3.1 - 新增独立业务模块验收 `scripts/verifyBusiness.py`:从远程 `v0.2.0` 升级到 `v0.3.1`,验证无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。 diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 0000000..3e91935 --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,20 @@ +# GoOmni 性能基线 + +这份基线用于比较未来自研网络层,不代表生产容量承诺。测试只测进程内请求分发和 GoOmni 请求链,不包含真实 TCP、TLS、数据库或业务存储延迟。 + +运行: + +```sh +python3 scripts/verifyPerformance.py +``` + +本次记录环境:Go 1.25.0、darwin/arm64、Apple M2 Max;每项连续运行 3 次: + +```text +BenchmarkTransportRouteDispatch/nethttp 568–671 ns/op 1344 B/op 13 allocs/op +BenchmarkTransportApplicationRequest/nethttp 2017–2208 ns/op 3091 B/op 34 allocs/op +``` + +`BenchmarkTransportRouteDispatch` 只覆盖标准库路由、路径参数和 `http.Handler` 调用;`BenchmarkTransportApplicationRequest` 还包含 GoOmni Context、RequestID、统一 JSON 响应和默认中间件链。 + +比较新网络层时,应使用相同 Go 版本、机器和 benchmark 命令,并同时运行 `-race` 全量测试。性能下降需要结合分配次数、取消传播、错误响应和业务契约结果一起判断。 diff --git a/README.md b/README.md index dae68f7..d1d5a01 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # GoOmni -GoOmni 是一套面向业务复用的 Go 后端基础框架。当前默认用 Gin 作为网络适配器,业务代码只依赖 GoOmni 的 transport-neutral API,后续可以替换为自研网络层。 +GoOmni 是一套面向业务复用的 Go 后端基础框架。当前默认使用只依赖标准库的网络适配器,业务代码只依赖 GoOmni 的 transport-neutral API,后续可以替换为自研网络层。 远程 module 地址是 `git.yuntuo.hk/k2safe/go-omni`。业务项目可以直接引用指定版本: ```sh -go get git.yuntuo.hk/k2safe/go-omni@v0.3.1 +go get git.yuntuo.hk/k2safe/go-omni@v0.4.0 ``` +`v0.4.0` 移除了 Gin 和 Gin 专属的 `WithMode` 配置;`omni.New()` 直接使用标准库路由,已有业务 Handler / Service 无需修改。 + 创建新业务项目: ```sh -go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.3.1 +go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.4.0 go-omni new --module example.com/billing ./billing cd billing go mod tidy @@ -40,7 +42,7 @@ go-omni/ ├── validate/ # 参数校验 ├── auth/ # 认证和权限接口 ├── database/ # database/sql 生命周期和事务 -├── transport/gin/ # Gin 网络适配器,可替换 +├── transport/nethttp/ # 标准库网络适配器 ├── examples/ # 示例项目 └── cmd/ # go-omni 项目脚手架 ``` @@ -80,7 +82,7 @@ func main() { return omni.NotFound("USER_NOT_FOUND", "user was not found") ``` -默认 Gin 适配器位于 `transport/gin`。业务层不需要导入 Gin;如果后续实现自己的网络层,只需要实现 `httpx.Router` / `httpx.RouterGroup`,然后通过 `omni.NewWithRouter` 启动应用: +默认网络适配器位于 `transport/nethttp`。业务层不需要导入具体网络库;如果后续实现自己的网络层,只需要实现 `httpx.Router` / `httpx.RouterGroup`,然后通过 `omni.NewWithRouter` 启动应用: ```go application := omni.NewWithRouter(myRouter) @@ -88,11 +90,11 @@ application := omni.NewWithRouter(myRouter) 仓库还提供了使用标准库路由的 `transport/nethttp` 参考实现。两种适配器已通过同一套路由、业务处理链和取消传播测试,完整用户 CRUD 复用同一份 Handler / Service。 -自研适配器可以直接运行 `transport/transporttest.Run` 验收。具体规则、v0.3.1 的斜杠 / Any / 405 行为调整,以及自研网络内核与当前 HTTP 接口的边界,见 [适配层契约](transport/README.md)。 +自研适配器可以直接运行 `transport/transporttest.Run` 验收。具体规则、v0.4.0 的斜杠 / Any / 405 行为调整,以及自研网络内核与当前 HTTP 接口的边界,见 [适配层契约](transport/README.md)。 -`app.Engine()` 现在返回标准 `http.Handler`,`ctx.Raw()` 返回适配器提供的 `http.ResponseWriter`,避免业务 API 锁定 Gin 类型。 +`app.Engine()` 返回标准 `http.Handler`,`ctx.Raw()` 返回适配器提供的 `http.ResponseWriter`,业务 API 不锁定具体网络实现。 -业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖 Gin: +业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖具体网络库: ```go func (s *UserService) Create(ctx *httpx.Context) error { @@ -169,10 +171,12 @@ userService := service.NewUsers(services) 从远程版本生成并升级一个独立业务模块,可运行: ```sh -python3 scripts/verifyBusiness.py --version v0.3.1 +python3 scripts/verifyBusiness.py --version v0.4.0 ``` -该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.3.1`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。 +该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.4.0`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。 + +标准库路由和完整 GoOmni 请求链的可重复性能基线见 [PERFORMANCE.md](PERFORMANCE.md),运行 `python3 scripts/verifyPerformance.py` 可重新采样。 在框架根目录运行: @@ -182,7 +186,7 @@ go run ./examples/reference 默认地址为 `http://127.0.0.1:8080`。示例文档提供完整的 curl 操作流程。数据只保存在内存中,重启后清空。 -框架的 `App` 也实现了标准 `http.Handler`,可以直接用于 `http.Server` 和 `httptest.NewServer`;业务路由可通过 `*omni.RouterGroup` 注册,无需导入 Gin。 +框架的 `App` 也实现了标准 `http.Handler`,可以直接用于 `http.Server` 和 `httptest.NewServer`;业务路由可通过 `*omni.RouterGroup` 注册,无需导入网络库。 ## 当前边界 diff --git a/ROADMAP.md b/ROADMAP.md index b1609de..32dd7d8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -18,21 +18,27 @@ - 已验证初始化失败、缺少表结构、正常退出时关闭连接池,以及数据库停止后的 readiness 503、恢复后的 200。 - 自动验收入口:`python3 scripts/verifyDatabase.py --version v0.2.0`;结束后清理本次测试容器、匿名卷和临时项目。 -## 3. 网络适配一致性(已完成,v0.3.0) +## 3. 网络适配一致性(已完成,v0.4.0) -- Gin 与标准库适配器运行同一套契约测试。 +- 标准库适配器运行公共契约测试,并为自研网络层保留相同的接入边界。 - 覆盖路由参数、分组、中间件、错误响应和取消传播。 - 验收:切换适配器后,业务 Handler 和 Service 无需修改,约定的 HTTP 行为一致。 - 新增公共 `transport/transporttest.Run`,自研适配器可直接复用;覆盖静态 / 参数 / 通配路由、分组、405 Allow、鉴权短路、错误与 panic、响应提交、requestId、并发隔离、截止时间及真实客户端取消。 -- 两种适配器共享明确的路径与方法约定,修复尾部斜杠、通配参数、Any 和 Gin 405 Allow 差异。 -- 完整用户 CRUD 已用相同业务注册代码在两种适配器上验证;全量 race 测试通过。 -- 自动验收入口:`python3 scripts/verifyTransports.py --version v0.3.0`,从源码目录外消费远程版本与公共契约测试包。 +- 标准库路由实现明确的路径、方法、尾部斜杠、通配参数、Any 和 404/405 约定;不再引入 Gin 依赖。 +- 完整用户 CRUD 已用标准库适配器验证;自研适配器可直接复用同一公共契约测试包。 +- 自动验收入口:`python3 scripts/verifyTransports.py --version v0.4.0`,从源码目录外消费远程版本与公共契约测试包。 -## 4. 实际业务试用(已完成,v0.3.1) +## 4. 实际业务试用(已完成,v0.4.0) -- 在独立业务目录生成 billing service,先固定远程 `v0.2.0`,再升级到 `v0.3.1`,全程无本地 replace。 +- 在独立业务目录生成 billing service,先固定远程 `v0.2.0`,再升级到 `v0.4.0`,全程无本地 replace。 - 验证环境配置、结构化访问日志、requestId 关联、参数校验、业务错误映射、CRUD、优雅退出和升级后的 race 测试。 - 业务试用没有要求修改 Handler / Service;现有公共 API 足以支撑一套分层业务模块。 -- 自动验收入口:`python3 scripts/verifyBusiness.py --version v0.3.1`。 +- 自动验收入口:`python3 scripts/verifyBusiness.py --version v0.4.0`。 -以上阶段以验收结果判断完成。当前仍是基础框架试用阶段;MySQL、适配层契约和一次业务模块试用已完成,生产压测与自研网络内核尚未完成。后续进入稳定性与性能验证,再决定稳定版范围。 +## 5. 稳定性与性能基线(进行中) + +- 已加入标准库路由和完整 GoOmni 请求链 benchmark,输出分配次数和耗时,作为未来自研网络层的对照基线。 +- 已具备全量 race、vet、build 验收;生产压测、长连接和真实部署规模验证仍待完成。 +- 自动验收入口:`python3 scripts/verifyPerformance.py`。 + +以上阶段以验收结果判断完成。当前仍是基础框架试用阶段;MySQL、适配层契约和一次业务模块试用已完成,稳定性与性能验证、自研网络内核尚未完成。 diff --git a/app/app.go b/app/app.go index e9b2944..d1cefcb 100644 --- a/app/app.go +++ b/app/app.go @@ -15,7 +15,7 @@ import ( apierrors "git.yuntuo.hk/k2safe/go-omni/errors" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/middleware" - ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin" + netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp" ) // App is the top-level GoOmni application. Its public behavior depends on @@ -29,10 +29,10 @@ type App struct { closeErr error } -// New creates an application using the default Gin adapter. +// New creates an application using GoOmni's standard-library router. func New(options ...config.Option) *App { settings := configure(options...) - return newApp(ginrouter.New(settings.Mode), settings) + return newApp(netrouter.New(), settings) } // NewWithRouter creates an application with a custom network layer. A custom @@ -68,7 +68,7 @@ func newApp(router httpx.Router, settings config.Config) *App { func (a *App) Router() httpx.Router { return a.router } // Engine is kept as a compatibility name for the old API. It returns a -// standard http.Handler, so callers no longer need a Gin type. +// standard http.Handler, so callers do not depend on a router type. func (a *App) Engine() http.Handler { return a.router } // ServeHTTP implements http.Handler for standard servers and HTTP tests. diff --git a/app_test.go b/app_test.go index 62d284b..a18be6c 100644 --- a/app_test.go +++ b/app_test.go @@ -8,12 +8,10 @@ import ( "strings" "testing" "time" - - "github.com/gin-gonic/gin" ) func TestAppReturnsStandardResponse(t *testing.T) { - app := New(WithMode(gin.TestMode)) + app := New() app.GET("/health", func(ctx *Context) error { return ctx.OK(map[string]string{"status": "ok"}) }) @@ -41,7 +39,7 @@ func TestAppReturnsStandardResponse(t *testing.T) { } func TestAppReturnsAPIError(t *testing.T) { - app := New(WithMode(gin.TestMode)) + app := New() app.GET("/users/:id", func(ctx *Context) error { return NotFound("USER_NOT_FOUND", "user was not found") }) @@ -63,7 +61,7 @@ func TestAppReturnsAPIError(t *testing.T) { } func TestAppRecoversFromPanic(t *testing.T) { - app := New(WithMode(gin.TestMode)) + app := New() app.GET("/panic", func(ctx *Context) error { panic("boom") }) @@ -85,7 +83,7 @@ func TestAppRecoversFromPanic(t *testing.T) { } func TestAppRunContextShutsDownGracefully(t *testing.T) { - app := New(WithMode(gin.TestMode), WithShutdownTimeout(time.Second)) + app := New(WithShutdownTimeout(time.Second)) ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- app.RunContext(ctx, "127.0.0.1:0") }() @@ -102,7 +100,7 @@ func TestAppRunContextShutsDownGracefully(t *testing.T) { } func TestAppClosesResourcesInReverseOrder(t *testing.T) { - app := New(WithMode(gin.TestMode)) + app := New() var closed []string app.OnShutdown(func() error { closed = append(closed, "database") diff --git a/auth/auth_test.go b/auth/auth_test.go index bc7147e..3fc094b 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -9,11 +9,10 @@ import ( "git.yuntuo.hk/k2safe/go-omni" "git.yuntuo.hk/k2safe/go-omni/auth" - "github.com/gin-gonic/gin" ) func TestRequireAuthAndRole(t *testing.T) { - app := omni.New(omni.WithMode(gin.TestMode)) + app := omni.New() authenticator := auth.StaticAuthenticator{"good-token": {UserID: "user-1", Roles: []string{"user"}}} private := app.Group("/private", auth.RequireAuth(authenticator)) private.GET("/me", func(ctx *omni.Context) error { @@ -62,7 +61,7 @@ func TestRequireAuthAndRole(t *testing.T) { } func TestOptionalAuth(t *testing.T) { - app := omni.New(omni.WithMode(gin.TestMode)) + app := omni.New() app.GET("/public", func(ctx *omni.Context) error { principal, authenticated := auth.FromContext(ctx) return ctx.OK(map[string]any{"authenticated": authenticated, "userId": principal.UserID}) diff --git a/cmd/go-omni/README.md b/cmd/go-omni/README.md index e2ab9fd..71f8a82 100644 --- a/cmd/go-omni/README.md +++ b/cmd/go-omni/README.md @@ -3,7 +3,7 @@ 从远程安装,生成一个带 Handler / Service / Repository / Model 分层的服务骨架: ```sh -go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.3.1 +go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.4.0 go-omni new --module example.com/billing ./billing cd billing go mod tidy @@ -16,7 +16,7 @@ go run ./cmd/server 需要选择其他已发布版本时,使用完整版本号: ```sh -go-omni new --framework-version v0.3.1 --module example.com/billing ./billing +go-omni new --framework-version v0.4.0 --module example.com/billing ./billing ``` `--framework-version` 禁用自动本地替换;`main`、`latest` 等浮动引用不接受为固定版本。 @@ -48,8 +48,8 @@ MySQL 项目额外包含 `schema.sql`、MySQL Repository 和可选的数据库 发布验收使用 Python 3 标准库脚本,不需要额外 Python 依赖: ```sh -python3 scripts/verifyScaffold.py --version v0.3.1 -python3 scripts/verifyDatabase.py --version v0.3.1 +python3 scripts/verifyScaffold.py --version v0.4.0 +python3 scripts/verifyDatabase.py --version v0.4.0 ``` 脚本在源码目录外安装远程 CLI、生成项目,确认固定版本且没有 `replace`,再下载依赖、 diff --git a/cmd/go-omni/scaffold/version.go b/cmd/go-omni/scaffold/version.go index 0680377..d805309 100644 --- a/cmd/go-omni/scaffold/version.go +++ b/cmd/go-omni/scaffold/version.go @@ -10,7 +10,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni" // Source builds can carry an unpublished VCS version (including +dirty). // Keep this fallback in sync with the next release; installed CLIs use their // downloaded module version, which also has a module checksum. -const defaultFrameworkVersion = "v0.3.1" +const defaultFrameworkVersion = "v0.4.0" var frameworkVersionPattern = regexp.MustCompile(`^v[01]\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`) diff --git a/config/config.go b/config/config.go index 008e8a5..31fb3f5 100644 --- a/config/config.go +++ b/config/config.go @@ -11,7 +11,6 @@ const defaultRequestIDHeader = "X-Request-ID" // Config controls the default runtime behavior of an App. type Config struct { - Mode string Address string RequestIDHeader string EnableLogger bool @@ -53,11 +52,6 @@ func (e *ConfigError) Error() string { return "config " + e.Field + " " + e.Mess // Option changes an App's configuration. type Option func(*Config) -// WithMode sets the default transport's running mode. -func WithMode(mode string) Option { - return func(cfg *Config) { cfg.Mode = mode } -} - // WithRequestIDHeader changes the header used to read and return request IDs. func WithRequestIDHeader(header string) Option { return func(cfg *Config) { diff --git a/config/config_test.go b/config/config_test.go index 04b534c..8c5eece 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -8,7 +8,7 @@ import ( func TestLoadFromEnvUsesDefaults(t *testing.T) { for _, key := range []string{ - "GO_OMNI_MODE", "GO_OMNI_HTTP_ADDR", "GO_OMNI_REQUEST_ID_HEADER", "GO_OMNI_ENABLE_LOGGER", + "GO_OMNI_HTTP_ADDR", "GO_OMNI_REQUEST_ID_HEADER", "GO_OMNI_ENABLE_LOGGER", "GO_OMNI_LOG_LEVEL", "GO_OMNI_LOG_FORMAT", "GO_OMNI_LOG_OUTPUT", "GO_OMNI_LOG_FILE", "GO_OMNI_SERVICE", "GO_OMNI_ENVIRONMENT", "GO_OMNI_LOG_ADD_SOURCE", "GO_OMNI_SHUTDOWN_TIMEOUT", @@ -29,7 +29,6 @@ func TestLoadFromEnvUsesDefaults(t *testing.T) { } func TestLoadFromEnvOverridesSettings(t *testing.T) { - t.Setenv("GO_OMNI_MODE", "release") t.Setenv("GO_OMNI_HTTP_ADDR", "127.0.0.1:9090") t.Setenv("GO_OMNI_REQUEST_ID_HEADER", "X-Correlation-ID") t.Setenv("GO_OMNI_ENABLE_LOGGER", "true") @@ -45,7 +44,7 @@ func TestLoadFromEnvOverridesSettings(t *testing.T) { if err != nil { t.Fatal(err) } - if cfg.Mode != "release" || cfg.Address != "127.0.0.1:9090" || cfg.RequestIDHeader != "X-Correlation-ID" || !cfg.EnableLogger { + if cfg.Address != "127.0.0.1:9090" || cfg.RequestIDHeader != "X-Correlation-ID" || !cfg.EnableLogger { t.Fatalf("unexpected application settings: %+v", cfg) } if cfg.ShutdownTimeout != 3*time.Second || cfg.Log.Level != "debug" || cfg.Log.Format != "text" || cfg.Log.Output != "stdout" || cfg.Log.Service != "billing" || cfg.Log.Environment != "test" || !cfg.Log.AddSource { @@ -80,11 +79,10 @@ func TestLoadFromEnvRejectsInvalidValues(t *testing.T) { func TestWithConfigAndWithLogConfig(t *testing.T) { settings := Default() - settings.Mode = "release" settings.Log.Service = "orders" cfg := Default() WithConfig(settings)(&cfg) - if cfg.Mode != "release" || cfg.Log.Service != "orders" { + if cfg.Log.Service != "orders" { t.Fatalf("complete config was not applied: %+v", cfg) } diff --git a/config/env.go b/config/env.go index 73f9e66..bfce5c8 100644 --- a/config/env.go +++ b/config/env.go @@ -18,7 +18,6 @@ func LoadFromEnv(prefix string) (Config, error) { prefix = defaultEnvPrefix } cfg := Default() - cfg.Mode = envString(prefix+"MODE", cfg.Mode) cfg.Address = envString(prefix+"HTTP_ADDR", cfg.Address) cfg.RequestIDHeader = envString(prefix+"REQUEST_ID_HEADER", cfg.RequestIDHeader) cfg.Log.Level = envString(prefix+"LOG_LEVEL", cfg.Log.Level) diff --git a/examples/reference/README.md b/examples/reference/README.md index 6076c8f..870f979 100644 --- a/examples/reference/README.md +++ b/examples/reference/README.md @@ -41,8 +41,8 @@ examples/reference/ 调用路径:HTTP → Handler → Service → Repository。 -- Handler 依赖 GoOmni,示例代码不导入 Gin,也不调用 `Engine()` / `Raw()`。 -- Service、Repository、Model 不依赖 GoOmni 或 Gin,通过标准 `context.Context` 传递请求取消信号。 +- Handler 依赖 GoOmni,示例代码不导入具体网络库,也不调用 `Engine()` / `Raw()`。 +- Service、Repository、Model 不依赖 GoOmni 或网络库,通过标准 `context.Context` 传递请求取消信号。 - JSON 校验标签留在 Handler 的请求结构上;业务模型没有 HTTP 绑定标签。 - Service 返回业务错误,由 Handler 映射为 HTTP 错误;未知错误交给框架转换成通用 500 响应。 - 通过 `application.Group("/api/v1", handler.RequestLog(logger))` 挂载自定义中间件,再注册 `/users` 子路由。日志记录请求 ID、耗时和返回错误;panic 由框架恢复中间件处理。 @@ -156,6 +156,6 @@ go vet ./... go build ./... ``` -`TestUserCRUD` 将同一份业务注册代码分别挂载到 Gin 与 nethttp,验证替换适配器后 Handler、Service、Repository 均无需修改。 +`TestUserCRUD` 使用标准库适配器运行完整业务注册代码;同一套 Handler、Service、Repository 也可通过 `omni.NewWithRouter` 接入自研适配器。 测试覆盖完整 CRUD、认证和角色权限、校验失败、分页边界、邮箱冲突及失败更新、删除后重用邮箱、请求 ID、日志关联、内部错误隐藏、并发唯一性、取消请求不修改数据、数据库配置解析和 HTTP 优雅退出。MySQL 测试不自动连接外部数据库。 diff --git a/examples/reference/main_test.go b/examples/reference/main_test.go index ede74fb..ebcbe52 100644 --- a/examples/reference/main_test.go +++ b/examples/reference/main_test.go @@ -20,7 +20,6 @@ import ( "git.yuntuo.hk/k2safe/go-omni/examples/reference/repository" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/logx" - ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin" netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp" ) @@ -32,7 +31,7 @@ type envelope struct { } func testApp() *omni.App { - return newApp(repository.NewMemoryUsers(), testLogger(io.Discard), omni.WithMode("test")) + return newApp(repository.NewMemoryUsers(), testLogger(io.Discard)) } func testLogger(writer io.Writer) *logx.Logger { @@ -87,7 +86,6 @@ func TestProtectedWriteRoutes(t *testing.T) { repository.NewMemoryUsers(), testLogger(io.Discard), auth.StaticAuthenticator{"user-token": {UserID: "user-1", Roles: []string{"user"}}}, - omni.WithMode("test"), ) request(t, app, "POST", "/api/v1/users", `{"name":"Alice","email":"alice@example.com"}`, 401, "UNAUTHENTICATED") created := dataAs[model.User](t, requestWithAuthorization(t, app, "POST", "/api/v1/users", `{"name":"Alice","email":"alice@example.com"}`, "Bearer user-token", 201, "CREATED")) @@ -110,7 +108,6 @@ func TestUserCRUD(t *testing.T) { name string newRouter func() httpx.Router }{ - {"gin", func() httpx.Router { return ginrouter.New("test") }}, {"nethttp", func() httpx.Router { return netrouter.New() }}, } { t.Run(adapter.name, func(t *testing.T) { @@ -226,7 +223,7 @@ func TestErrorTranslationAndRequestLogging(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { var logs bytes.Buffer - app := newApp(failingUsers{repository.NewMemoryUsers(), test.err}, testLogger(&logs), omni.WithMode("test")) + app := newApp(failingUsers{repository.NewMemoryUsers(), test.err}, testLogger(&logs)) result := request(t, app, "GET", "/api/v1/users/1", "", test.status, test.code) if strings.Contains(result.Message, test.err.Error()) || len(result.Data) != 0 { t.Fatalf("internal error details leaked: %+v", result) diff --git a/go.mod b/go.mod index 85baed5..fae823a 100644 --- a/go.mod +++ b/go.mod @@ -3,36 +3,19 @@ module git.yuntuo.hk/k2safe/go-omni go 1.22 require ( - github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.20.0 github.com/go-sql-driver/mysql v1.9.3 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - golang.org/x/arch v0.8.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f2bfde9..45c9961 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -27,67 +14,19 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/httpx/context.go b/httpx/context.go index b148383..9f11b8f 100644 --- a/httpx/context.go +++ b/httpx/context.go @@ -18,9 +18,9 @@ type Handler func(*Context) error type Middleware func(Handler) Handler // Router is the small contract the application needs from a network layer. -// The default implementation is backed by Gin, but another router or a -// future GoOmni transport can implement this interface without changing -// business handlers. Register routes before serving requests. Implementations +// The default implementation is backed by the standard library, but another +// router or a future GoOmni transport can implement this interface without +// changing business handlers. Register routes before serving requests. Implementations // should pass transport/transporttest.Run; see transport/README.md for the // portable path, method, fallback and context-propagation rules. type Router interface { @@ -78,7 +78,8 @@ func RoutePattern(ctx context.Context) string { } // Context provides the business-facing request and response API. It contains -// only standard-library HTTP types, so it remains usable when Gin is replaced. +// only standard-library HTTP types, so it remains usable when the network +// implementation is replaced. type Context struct { request *http.Request writer http.ResponseWriter diff --git a/logx/README.md b/logx/README.md index 3df027b..6796356 100644 --- a/logx/README.md +++ b/logx/README.md @@ -1,6 +1,6 @@ # logx -`logx` 是 GoOmni 的业务层结构化日志入口。业务代码不需要依赖 Gin: +`logx` 是 GoOmni 的业务层结构化日志入口。业务代码不需要依赖具体网络库: ```go import "git.yuntuo.hk/k2safe/go-omni/logx" diff --git a/logx/logx.go b/logx/logx.go index b7f2603..1ed93fe 100644 --- a/logx/logx.go +++ b/logx/logx.go @@ -1,5 +1,5 @@ // Package logx provides the application logger used by GoOmni and business -// packages. Its public API is deliberately independent of Gin so the same +// packages. Its public API is deliberately independent of the network layer so the same // logger can be used from HTTP handlers, jobs and command line programs. package logx diff --git a/middleware/middleware.go b/middleware/middleware.go index 65e0f0a..80b983e 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -31,8 +31,8 @@ func RequestID(header string) httpx.Middleware { } // Recovery converts panics from handlers and middleware into the standard -// internal error. The network adapter does not need a Gin-specific recovery -// hook for this behavior. +// internal error. Recovery belongs to GoOmni's transport-neutral middleware +// rather than a router-specific hook. func Recovery() httpx.Middleware { return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) (err error) { diff --git a/omni.go b/omni.go index aa7e706..4e4d297 100644 --- a/omni.go +++ b/omni.go @@ -34,8 +34,6 @@ func NewWithRouter(router Router, options ...Option) *App { return app.NewWithRouter(router, options...) } -func WithMode(mode string) Option { return config.WithMode(mode) } - func WithRequestIDHeader(header string) Option { return config.WithRequestIDHeader(header) } func WithLogger(enabled bool) Option { return config.WithLogger(enabled) } diff --git a/scripts/verifyBusiness.py b/scripts/verifyBusiness.py index a38ffbd..e1e0690 100755 --- a/scripts/verifyBusiness.py +++ b/scripts/verifyBusiness.py @@ -25,7 +25,7 @@ def free_address(): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go") - parser.add_argument("--version", default="v0.3.1", help="Target framework release") + parser.add_argument("--version", default="v0.4.0", help="Target framework release") parser.add_argument("--from-version", default="v0.2.0", help="Version used before upgrade") args = parser.parse_args() @@ -62,7 +62,6 @@ def main(): server_env = dict(env) server_env.update( GO_OMNI_HTTP_ADDR=address, - GO_OMNI_MODE="release", GO_OMNI_ENABLE_LOGGER="true", GO_OMNI_LOG_FILE=str(log_file), GO_OMNI_LOG_FORMAT="json", diff --git a/scripts/verifyDatabase.py b/scripts/verifyDatabase.py index e0ea974..a22035b 100644 --- a/scripts/verifyDatabase.py +++ b/scripts/verifyDatabase.py @@ -145,7 +145,7 @@ class Server: def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go") - parser.add_argument("--version", default="v0.3.1") + parser.add_argument("--version", default="v0.4.0") parser.add_argument("--cli", help="Use a locally built CLI") parser.add_argument("--framework-dir", help="Local framework source for pre-release checks") parser.add_argument("--image", default="mysql:8.4") @@ -186,7 +186,7 @@ def main(): raise AssertionError(f"integration test did not pass: {name}\n{output}") print("PASS real MySQL: commit, error/panic/cancel rollback, pool limits, health timeout, close", flush=True) - serverEnv = dict(env, GO_OMNI_DB_DSN=database.dsn, GO_OMNI_MODE="release", + serverEnv = dict(env, GO_OMNI_DB_DSN=database.dsn, GO_OMNI_DB_PING_TIMEOUT="500ms", GO_OMNI_ENABLE_LOGGER="true") server = Server(binary, project, serverEnv) servers.append(server) diff --git a/scripts/verifyPerformance.py b/scripts/verifyPerformance.py new file mode 100755 index 0000000..2163bc2 --- /dev/null +++ b/scripts/verifyPerformance.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Run GoOmni's repeatable stability checks and transport performance baseline.""" + +import argparse +import os +from pathlib import Path +import subprocess + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--go", default="go") + parser.add_argument("--count", type=int, default=3) + args = parser.parse_args() + root = Path(__file__).resolve().parent.parent + env = dict(os.environ, GOWORK="off") + checks = [ + ([args.go, "test", "-race", "./..."], "race tests"), + ([args.go, "vet", "./..."], "vet"), + ([args.go, "build", "./..."], "build"), + ( + [args.go, "test", "-run", "^$", "-bench", "^BenchmarkTransport", + "-benchmem", "-count", str(args.count), "./transport"], + "transport benchmarks", + ), + ] + for command, name in checks: + print(f"Running {name}", flush=True) + result = subprocess.run(command, cwd=root, env=env, text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + print(result.stdout, end="") + if result.returncode != 0: + raise SystemExit(f"{name} failed with exit code {result.returncode}") + print("PASS stability checks and transport performance baseline", flush=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/verifyScaffold.py b/scripts/verifyScaffold.py index 9f9c173..ff08b96 100644 --- a/scripts/verifyScaffold.py +++ b/scripts/verifyScaffold.py @@ -29,7 +29,7 @@ def exerciseServer(binary, directory, env, stopSignal, cleanupMarker=None): listener.bind(("127.0.0.1", 0)) address = f"127.0.0.1:{listener.getsockname()[1]}" serverEnv = {key: value for key, value in env.items() if not key.startswith("GO_OMNI_")} - serverEnv.update(GO_OMNI_HTTP_ADDR=address, GO_OMNI_MODE="release", + serverEnv.update(GO_OMNI_HTTP_ADDR=address, GO_OMNI_ENABLE_LOGGER="true", GO_OMNI_SHUTDOWN_TIMEOUT="2s") if cleanupMarker: serverEnv["GO_OMNI_CLEANUP_MARKER"] = str(cleanupMarker) @@ -91,7 +91,7 @@ def exerciseServer(binary, directory, env, stopSignal, cleanupMarker=None): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go", help="Go executable") - parser.add_argument("--version", default="v0.3.1", help="Released CLI version to install") + parser.add_argument("--version", default="v0.4.0", help="Released CLI version to install") parser.add_argument("--cli", help="Use a locally built CLI instead of installing") parser.add_argument("--framework-version", help="Explicit version passed to the CLI") parser.add_argument("--framework-dir", help="Local checkout for pre-release verification") @@ -142,7 +142,7 @@ import ( omni "git.yuntuo.hk/k2safe/go-omni" ) func main() { - app := omni.New(omni.WithMode("release")) + app := omni.New() app.GET("/health/live", func(ctx *omni.Context) error { return ctx.OK(map[string]string{"status": "ok"}) }) app.OnShutdown(func() error { return os.WriteFile(os.Getenv("GO_OMNI_CLEANUP_MARKER"), []byte("closed"), 0600) }) if err := app.Run(os.Getenv("GO_OMNI_HTTP_ADDR")); err != nil { log.Fatal(err) } diff --git a/scripts/verifyTransports.py b/scripts/verifyTransports.py index 3c5a1c3..6f330a5 100644 --- a/scripts/verifyTransports.py +++ b/scripts/verifyTransports.py @@ -13,7 +13,7 @@ from verifyScaffold import MODULE, run def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--go", default="go") - parser.add_argument("--version", default="v0.3.1") + parser.add_argument("--version", default="v0.4.0") parser.add_argument("--framework-dir", help="Use a local checkout before release") args = parser.parse_args() env = dict(os.environ, GOWORK="off") @@ -29,15 +29,11 @@ def main(): import ( "testing" "{MODULE}/httpx" - ginrouter "{MODULE}/transport/gin" netrouter "{MODULE}/transport/nethttp" "{MODULE}/transport/transporttest" ) func TestContract(t *testing.T) {{ - t.Run("gin", func(t *testing.T) {{ - transporttest.Run(t, func() httpx.Router {{ return ginrouter.New("test") }}) - }}) t.Run("nethttp", func(t *testing.T) {{ transporttest.Run(t, func() httpx.Router {{ return netrouter.New() }}) }}) @@ -52,7 +48,7 @@ func TestContract(t *testing.T) {{ events = [json.loads(line) for line in output.splitlines() if line.startswith("{")] if any(event.get("Action") == "skip" for event in events): raise AssertionError("contract suite unexpectedly skipped tests") - for adapter in ("gin", "nethttp"): + for adapter in ("nethttp",): name = f"TestContract/{adapter}" if not any(event.get("Action") == "pass" and event.get("Test") == name for event in events): raise AssertionError(f"{adapter} contract did not pass") diff --git a/transport/README.md b/transport/README.md index c0deee1..a20bcef 100644 --- a/transport/README.md +++ b/transport/README.md @@ -12,7 +12,7 @@ application := omni.NewWithRouter(netrouter.New()) registerRoutes(application) // 与 omni.New() 复用同一份业务注册代码 ``` -`transport/gin` 默认使用 Gin 分发请求;`transport/nethttp` 使用独立的标准库路由实现。两者共用路由声明校验和路径匹配规则,Gin 的正常请求仍由 Gin 路由树处理;它的 404/405 回退由适配层统一处理。 +`transport/nethttp` 是当前内置的标准库路由实现。它提供一套可移植的路径、方法和错误回退规则;自研网络层可以直接实现同一个 `httpx.Router` 契约。 ## 可移植路由规则(v0.3.0) @@ -35,8 +35,6 @@ HTTP 回退响应属于路由层,不进入应用路由中间件,不携带统 业务层通过 `application.Handle("HEAD", path, handler)` 或 `group.Handle("OPTIONS", path, handler)` 显式注册其他方法,同样经过框架中间件和错误处理。 -`gin.NewWithEngine` 保留原生路由和中间件,但覆盖重定向、路径清理和 404/405 策略。可移植契约与 `Allow` 集合只覆盖经 GoOmni Router 注册的路由;直接操作 `Engine()` 的原生扩展不属于该保证。 - ## 业务行为 - 同一套 Handler、Service、中间件用于两种适配器;应用、分组、嵌套分组、路由中间件依次进入、反序退出。 @@ -69,9 +67,9 @@ func TestContract(t *testing.T) { ```sh go test -race ./transport/... ./examples/reference -python3 scripts/verifyTransports.py --version v0.3.0 +python3 scripts/verifyTransports.py --version v0.4.0 ``` -第一条同时验证完整用户 CRUD 在两种适配器上复用相同 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包。 +第一条验证标准库适配器复用完整 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包;自研适配器也应使用同一个公共测试包。 当前替换边界是路由与 HTTP 适配层,`App.Run` 仍使用标准库 `net/http.Server`。未来自研监听、连接管理或协议解析时,可让自研服务调用 `App.ServeHTTP`(提供标准 HTTP 请求/响应桥接),由自研启动层管理监听与关闭,并在退出时调用 `App.Close`;本阶段没有实现新的网络内核。 diff --git a/transport/benchmark_test.go b/transport/benchmark_test.go new file mode 100644 index 0000000..efaa76f --- /dev/null +++ b/transport/benchmark_test.go @@ -0,0 +1,48 @@ +package transport_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + omni "git.yuntuo.hk/k2safe/go-omni" + netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp" +) + +func BenchmarkTransportRouteDispatch(b *testing.B) { + b.Run("nethttp", func(b *testing.B) { + router := netrouter.New() + router.Handle(http.MethodGet, "/users/:id", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + request := httptest.NewRequest(http.MethodGet, "/users/42", nil) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + writer := httptest.NewRecorder() + router.ServeHTTP(writer, request) + if writer.Code != http.StatusNoContent { + b.Fatalf("unexpected status %d", writer.Code) + } + } + }) +} + +func BenchmarkTransportApplicationRequest(b *testing.B) { + b.Run("nethttp", func(b *testing.B) { + application := omni.NewWithRouter(netrouter.New()) + application.GET("/health", func(c *omni.Context) error { + return c.OK(map[string]string{"status": "ok"}) + }) + request := httptest.NewRequest(http.MethodGet, "/health", nil) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + writer := httptest.NewRecorder() + application.ServeHTTP(writer, request) + if writer.Code != http.StatusOK { + b.Fatalf("unexpected status %d", writer.Code) + } + } + }) +} diff --git a/transport/contract_test.go b/transport/contract_test.go index 5ef0200..7db5292 100644 --- a/transport/contract_test.go +++ b/transport/contract_test.go @@ -4,19 +4,12 @@ import ( "testing" "git.yuntuo.hk/k2safe/go-omni/httpx" - ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin" netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp" "git.yuntuo.hk/k2safe/go-omni/transport/transporttest" ) func TestAdapterContract(t *testing.T) { - for _, adapter := range []struct { - name string - newRouter func() httpx.Router - }{ - {"gin", func() httpx.Router { return ginrouter.New("test") }}, - {"nethttp", func() httpx.Router { return netrouter.New() }}, - } { - t.Run(adapter.name, func(t *testing.T) { transporttest.Run(t, adapter.newRouter) }) - } + t.Run("nethttp", func(t *testing.T) { + transporttest.Run(t, func() httpx.Router { return netrouter.New() }) + }) } diff --git a/transport/gin/router.go b/transport/gin/router.go deleted file mode 100644 index e3fc0a7..0000000 --- a/transport/gin/router.go +++ /dev/null @@ -1,124 +0,0 @@ -// Package gin adapts Gin to GoOmni's transport-neutral router contract. -// Business packages should import httpx and app, not this package. -package gin - -import ( - "net/http" - "sort" - "strings" - - "git.yuntuo.hk/k2safe/go-omni/httpx" - "git.yuntuo.hk/k2safe/go-omni/transport/internal/routing" - ginlib "github.com/gin-gonic/gin" -) - -// Router is the default Gin-backed GoOmni router. -type Router struct { - engine *ginlib.Engine - routes []registeredRoute -} - -type registeredRoute struct { - method string - segments []routing.Segment -} - -// New creates a Gin router without Gin's default logger or recovery middleware. -func New(mode string) *Router { - if mode != "" { - ginlib.SetMode(mode) - } - return NewWithEngine(ginlib.New()) -} - -// NewWithEngine wraps an existing engine and applies GoOmni's routing policy. -// Native middleware/routes are retained; redirect and fallback settings are -// replaced. Register portable routes through Router, not Engine(). -func NewWithEngine(engine *ginlib.Engine) *Router { - if engine == nil { - panic("go-omni/gin: nil engine") - } - engine.RedirectTrailingSlash = false - engine.RedirectFixedPath = false - engine.RemoveExtraSlash = false - engine.UseRawPath = false - // Gin 1.10 can carry backtracking state between method trees during its - // 405 lookup. Derive Allow from registered patterns instead of that lookup. - engine.HandleMethodNotAllowed = false - router := &Router{engine: engine} - engine.NoRoute(router.fallback) - return router -} - -func (r *Router) fallback(ctx *ginlib.Context) { - allowed := make(map[string]bool) - for _, route := range r.routes { - if _, matches := routing.Match(route.segments, ctx.Request.URL.Path); matches { - allowed[route.method] = true - } - } - if len(allowed) == 0 { - http.NotFound(ctx.Writer, ctx.Request) - return - } - methods := make([]string, 0, len(allowed)) - for method := range allowed { - methods = append(methods, method) - } - sort.Strings(methods) - ctx.Header("Allow", strings.Join(methods, ", ")) - http.Error(ctx.Writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) -} - -// Engine exposes the underlying Gin engine to adapter-specific integrations. -func (r *Router) Engine() *ginlib.Engine { return r.engine } - -func (r *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - r.engine.ServeHTTP(writer, request) -} - -func (r *Router) Handle(method, path string, handler http.Handler) { - segments := routing.Parse(method, path, handler) - r.engine.Handle(method, path, adapt(handler)) - r.routes = append(r.routes, registeredRoute{method: method, segments: segments}) -} - -func (r *Router) Any(path string, handler http.Handler) { - for _, method := range routing.Methods { - r.Handle(method, path, handler) - } -} - -func (r *Router) Group(path string) httpx.RouterGroup { - return &group{router: r, prefix: routing.Join("", path)} -} - -type group struct { - router *Router - prefix string -} - -func (g *group) Handle(method, path string, handler http.Handler) { - g.router.Handle(method, routing.Join(g.prefix, path), handler) -} - -func (g *group) Any(path string, handler http.Handler) { - g.router.Any(routing.Join(g.prefix, path), handler) -} - -func (g *group) Group(path string) httpx.RouterGroup { - return &group{router: g.router, prefix: routing.Join(g.prefix, path)} -} - -func adapt(handler http.Handler) ginlib.HandlerFunc { - return func(ctx *ginlib.Context) { - params := make(map[string]string, len(ctx.Params)) - for _, param := range ctx.Params { - params[param.Key] = param.Value - } - requestContext := httpx.WithRouteParams(ctx.Request.Context(), params) - requestContext = httpx.WithRoutePattern(requestContext, ctx.FullPath()) - request := ctx.Request.WithContext(requestContext) - handler.ServeHTTP(ctx.Writer, request) - } -} diff --git a/transport/internal/routing/pattern.go b/transport/internal/routing/pattern.go index 2abed77..67e5864 100644 --- a/transport/internal/routing/pattern.go +++ b/transport/internal/routing/pattern.go @@ -58,7 +58,7 @@ func Parse(method, path string, handler http.Handler) []Segment { return segments } -// CheckConflict enforces Gin-compatible ambiguity rules for one method. +// CheckConflict enforces the portable ambiguity rules for one method. func CheckConflict(existing, next []Segment) { for i := 0; i < len(existing) && i < len(next); i++ { a, b := existing[i], next[i] diff --git a/transport/nethttp/router.go b/transport/nethttp/router.go index 8dc7c0e..47d7f5a 100644 --- a/transport/nethttp/router.go +++ b/transport/nethttp/router.go @@ -1,5 +1,5 @@ -// Package nethttp provides a standard-library router for GoOmni, independent -// of Gin. Register routes before serving requests. +// Package nethttp provides GoOmni's standard-library router. Register routes +// before serving requests. package nethttp import ( diff --git a/transport/nethttp/router_test.go b/transport/nethttp/router_test.go index 03ecfb6..681711e 100644 --- a/transport/nethttp/router_test.go +++ b/transport/nethttp/router_test.go @@ -10,7 +10,7 @@ import ( netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp" ) -func TestRouterWorksWithoutGin(t *testing.T) { +func TestRouterWorks(t *testing.T) { router := netrouter.New() app := omni.NewWithRouter(router) app.GET("/users/:id", func(ctx *omni.Context) error { diff --git a/validate/validate.go b/validate/validate.go index a38c874..fc96184 100644 --- a/validate/validate.go +++ b/validate/validate.go @@ -21,7 +21,7 @@ func newValidator() *validator.Validate { } // JSON binds and validates a JSON request body using standard-library HTTP -// types. It intentionally does not depend on Gin. +// types. It intentionally does not depend on a network framework. func JSON(request *http.Request, value any) error { if request == nil || request.Body == nil { return apierrors.BadRequest("INVALID_BODY", "request body is invalid")