feat: init gateway v1.0.0, AI-First documentation refined (by AI)

This commit is contained in:
AI Engineer 2026-05-13 00:49:29 +08:00
parent 0984b688be
commit 6ed590e018
3 changed files with 68 additions and 45 deletions

View File

@ -1,57 +1,42 @@
# @go/gateway # @go/gateway
基于 `@go/service` 和 Redis 的高性能、事件驱动的动态 API 网关。 基于 `@go/service` 和 Redis 的高性能、事件驱动动态 API 网关。
详细配置指南请参考核心文档:[https://apigo.cc/go/service](https://apigo.cc/go/service)
`gateway` 专注于充当配置下发的搬运工,将路由匹配、转发等所有繁重工作剥离给底层的 `service` 核心。支持通过 Redis 进行 0 延迟、0 丢包的路由级全量/局部热更新。 ## 路由匹配逻辑 (AI-First 规范)
## 功能特性 为了确保 AI 模型和运维人员能够精准配置而不产生幻觉,网关遵循以下严格的路径匹配优先级:
- **极致性能**:基于 `service` 包的 Copy-on-Write (写时复制) 原语,无锁构建,转发阶段无锁等待。 ### 1. 路径匹配 (Path Matching)
- **事件驱动热更新**:摒弃轮询,监听 Redis Pub/Sub精准下发变更配置旧连接不受影响。 - **精确匹配 (Exact)**:
- **服务发现原生集成**:与 `@go/discover` 无缝集成,支持基于服务名的直连与负载均衡。 - 语法:`"Path": "/user/info"`
- **生命周期接管**:由 `@go/starter` 接管,支持通过 `kill -SIGHUP` 触发本地全量重新同步。 - 行为:仅当请求路径完全等于 `/user/info` 时触发。
- **前缀匹配 (Prefix)**:
- 语法:`"Path": "/api/*"` (**必须以 `/*` 结尾**)
- 行为:匹配所有以 `/api/` 开头的路径(如 `/api/login`, `/api/v1/status`)。
- *映射规则*: 若配置 `{"Path": "/v1/*", "ToPath": "/v2/*"}`,请求 `/v1/a` 将被转发至 `/v2/a`
- **正则匹配 (Regex)**:
- 语法:任何包含括号 `(` 的路径将被视为正则表达式。
- 示例:`"Path": "^/api/(.*)$"`。使用 `$1` 进行 `ToPath` 替换。
## 配置模型 (Redis Hash) ### 2. 域名选择 (Host Selection)
匹配请求头中的 `Host` 字段,按以下顺序尝试,匹配即止:
1. `example.com:8080` (精确域名 + 端口)
2. `example.com` (仅域名,忽略端口)
3. `:8080` (仅端口,匹配该端口下的所有 Host)
4. `*` (全局兜底通配符)
Gateway 将配置存储在 Redis 的 Hash 结构中,并按照 **Host (域名)** 进行字段隔离,提升拉取效率和管理粒度。 ## 动态配置模型 (Redis)
- **`gateway:proxies`** 代理规则 网关以 **Host (域名)** 为最小更新单元。更新某域名配置时,请向 Redis 频道发布该域名的纯字符串。
- **`gateway:rewrites`** 重写规则
- **`gateway:statics`** 静态目录服务
### 结构示例 - **存储结构 (Hash)**:
- Key: `gateway:host:<hostname>` (例如 `gateway:host:api.example.com`)
- Fields: `proxies` (JSON数组), `rewrites` (JSON数组), `statics` (JSON对象)
- **热更新通知 (Pub/Sub)**:
- Channel: `gateway:channel` (默认)
- Payload: `"api.example.com"` (域名原始字符串)
假设配置的 Host 为 `api.example.com`:
```json
// HSET gateway:proxies "api.example.com"
[
{ "Path": "^/user/(.*)$", "AuthLevel": 0, "ToApp": "user-service", "ToPath": "/$1" },
{ "Path": "/direct", "AuthLevel": 0, "ToApp": "http://10.0.0.1:8080", "ToPath": "/hello" }
]
// HSET gateway:rewrites "api.example.com"
[
{ "Path": "^/old-api/(.*)$", "ToPath": "/api/$1" }
]
// HSET gateway:statics "www.example.com"
{
"/ui": "/var/www/html"
}
```
## 动态热更新 API (Pub/Sub)
无需重启进程,向 Redis 的 `gateway:channel` 频道发布 JSON 消息,网关会在收到消息后仅拉取变化的那一部分(局部全量更新):
```json
{
"action": "update",
"type": "proxy", // 可选: "proxy", "rewrite", "static"
"host": "api.example.com"
}
```
## 启动与管理 ## 启动与管理

View File

@ -35,6 +35,9 @@ func TestGateway(t *testing.T) {
service.Host("*").GET("/hello", func(req *service.Request) string { service.Host("*").GET("/hello", func(req *service.Request) string {
return "hello from backend, path: " + req.RequestURI return "hello from backend, path: " + req.RequestURI
}) })
service.Host("*").GET("/hello/world", func(req *service.Request) string {
return "hello from backend, path: " + req.RequestURI
})
asBackend := service.AsyncStart() asBackend := service.AsyncStart()
defer asBackend.Stop() defer asBackend.Stop()
@ -48,6 +51,7 @@ func TestGateway(t *testing.T) {
proxyRules := []service.ProxyRule{ proxyRules := []service.ProxyRule{
{Path: "^/api/(.*)$", ToApp: "test-backend", ToPath: "/$1"}, {Path: "^/api/(.*)$", ToApp: "test-backend", ToPath: "/$1"},
{Path: "/direct", ToApp: "test-backend", ToPath: "/hello"}, {Path: "/direct", ToApp: "test-backend", ToPath: "/hello"},
{Path: "/v2/*", ToApp: "test-backend", ToPath: "/hello/*"},
} }
rewriteRules := []service.RewriteRule{ rewriteRules := []service.RewriteRule{
{Path: "^/old-api/(.*)$", ToPath: "/api/$1"}, {Path: "^/old-api/(.*)$", ToPath: "/api/$1"},
@ -130,6 +134,22 @@ func TestGateway(t *testing.T) {
t.Fatalf("Proxy regexp failed, got: %s", body) t.Fatalf("Proxy regexp failed, got: %s", body)
} }
// ============================================
// 测试 2.1: 极速通配符前缀匹配 (Discover)
// ============================================
req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/v2/world?x=2", nil)
req.Host = "gw.test"
res, err = client.Do(req)
body = ""
if err == nil && res != nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
body = string(b)
}
if err != nil || !strings.Contains(body, "hello from backend, path: /hello/world?x=2") {
t.Fatalf("Proxy wildcard failed, got: %s", body)
}
// ============================================ // ============================================
// 测试 3: Rewrite 后再 Proxy // 测试 3: Rewrite 后再 Proxy
// ============================================ // ============================================

20
main.go
View File

@ -12,7 +12,25 @@ func main() {
_ = config.Load(&GatewayConf, "gateway") _ = config.Load(&GatewayConf, "gateway")
// 2. 初始化 Starter 信息 // 2. 初始化 Starter 信息
starter.SetAppInfo("gateway", "2.0.0") starter.SetAppInfo("gateway", "1.0.0")
starter.SetUsage(`High-performance, event-driven dynamic API gateway.
Full Documentation: https://apigo.cc/go/service
Routing Path Matching Rules:
- Exact Match (Default): Path is treated as a literal string.
Example: "Path": "/api" only matches EXACTLY "/api".
- Prefix Match (Mandatory "/*" suffix): Matches any path starting with the given prefix.
Example: "Path": "/api/*" matches "/api/v1", "/api/login", etc.
Note: The trailing "/*" is required to enable prefix matching logic.
- Regex Match: Path containing "(" is treated as a Regular Expression.
Example: "Path": "^/api/(.*)$" captures groups for replacement in ToPath.
Host Selection Mechanism:
Requests are matched against the defined "Host" field in following order:
1. Exact Host:Port (e.g., "aa.com:8080")
2. Host Only (e.g., "aa.com") - matches any port for this host.
3. Port Only (e.g., ":80") - matches any host on this port.
4. Global Wildcard ("*" or "") - fallback for all other requests.`)
// 3. 注册 Gateway 服务 // 3. 注册 Gateway 服务
// GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口 // GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口