feat: align with unified 'To' syntax and KV maps, AI-First example-driven documentation (by AI)

This commit is contained in:
AI Engineer 2026-05-13 00:49:29 +08:00
parent 0984b688be
commit de564b8159
4 changed files with 130 additions and 75 deletions

139
README.md
View File

@ -1,79 +1,78 @@
# @go/gateway # @go/gateway (高性能动态网关)
基于 `@go/service` 和 Redis 的高性能、事件驱动的动态 API 网关。 基于 `@go/service` 实现的微服务 API 网关。详细路由语法与高级配置参考:[https://apigo.cc/go/service](https://apigo.cc/go/service)
`gateway` 专注于充当配置下发的搬运工,将路由匹配、转发等所有繁重工作剥离给底层的 `service` 核心。支持通过 Redis 进行 0 延迟、0 丢包的路由级全量/局部热更新。 ---
## 功能特性 ## 🚀 1. 快速开始 (1分钟)
- **极致性能**:基于 `service` 包的 Copy-on-Write (写时复制) 原语,无锁构建,转发阶段无锁等待。
- **事件驱动热更新**:摒弃轮询,监听 Redis Pub/Sub精准下发变更配置旧连接不受影响。
- **服务发现原生集成**:与 `@go/discover` 无缝集成,支持基于服务名的直连与负载均衡。
- **生命周期接管**:由 `@go/starter` 接管,支持通过 `kill -SIGHUP` 触发本地全量重新同步。
## 配置模型 (Redis Hash)
Gateway 将配置存储在 Redis 的 Hash 结构中,并按照 **Host (域名)** 进行字段隔离,提升拉取效率和管理粒度。
- **`gateway:proxies`** 代理规则
- **`gateway:rewrites`** 重写规则
- **`gateway:statics`** 静态目录服务
### 结构示例
假设配置的 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"
}
```
## 启动与管理
```bash ```bash
# 以后台进程启动 # 1. 安装最新版
./gateway start go get apigo.cc/go/gateway@latest
# 查看服务状态 (通过 IPC) # 2. 配置 env.yml (KV 极简语法)
./gateway status # ----------------------------------------------------------------------
service:
Listen: ":80" # 自动识别 HTTP 协议
Proxies:
"api.example.com": # 匹配域名 (支持: aa.com, :80, *, "")
"/user/*": "user-svc/v1/*" # [前缀通配] 必须以 /* 结尾映射后缀
"/direct": "http://1.1.1.1/hello" # [直接转发] 转发至 URL
Rewrites:
"*": # 通配所有域名
"^/old/(.*)$": "/new/$1" # [正则匹配] 包含 ( 即视为正则
Statics:
"www.example.com":
"/ui": "./dist" # [静态目录] 映射本地路径
# 平滑重启 / 重载兜底
./gateway restart
./gateway reload # 触发底层 service 的 OnReload全量同步 Redis
```
## 环境变量配置
支持通过环境变量或 `env.yml` 覆盖。
```yaml
gateway: gateway:
Redis: "127.0.0.1:6379" Redis: "127.0.0.1:6379" # (可选) 动态配置中心
Prefix: "gateway" # ----------------------------------------------------------------------
Channel: "gateway:channel"
# 3. 运行
go run . start
``` ```
---
## 🔄 2. 动态路由热更新 (Redis)
网关自动发现 `gateway:host:*` 的配置,修改后通过 Pub/Sub 秒级推送。
### A. 配置存储 (HSET)
```bash
# 存储域名 api.example.com 的规则 (支持 KV 极简 JSON)
HSET gateway:host:api.example.com \
proxies '{"/user/*":"user-svc/v1/*", "/auth":"auth-svc"}' \
rewrites '{"^/api/(.*)$":"/v2/$1"}' \
statics '{"/assets":"/var/www/data"}'
```
### B. 推送生效 (PUBLISH)
```bash
# 发布域名即可触发全集群该域名的局部热更新
PUBLISH gateway:channel "api.example.com"
```
---
## 🛠 3. 命令行常用操作
```bash
./gateway start # 后台启动
./gateway status # 查看状态 (地址、动态域名数、队列积压)
./gateway reload # 重载本地 YAML 并全量刷入 Redis 动态配置
./gateway stop # 优雅停止
```
---
## 📝 路由语法总结 (AI-First)
| 模式 | 语法示例 | 说明 |
| :--- | :--- | :--- |
| **精确匹配** | `"/login"` | 路径必须完全一致 |
| **高性能通配**| `"/api/*"` | **必须带 `/*` 结尾**,自动剥离前缀转发 |
| **正则匹配** | `"^/user/(.*)$"` | 路径包含 `(` 即视为正则,支持 `$1` 替换 |
*目标格式 (`To`)`app/path` (服务发现) 或 `http://host/path` (直接代理)。*

24
app.go
View File

@ -6,6 +6,7 @@ import (
"apigo.cc/go/redis" "apigo.cc/go/redis"
"apigo.cc/go/service" "apigo.cc/go/service"
"context" "context"
"fmt"
"strings" "strings"
) )
@ -89,6 +90,20 @@ func (g *GatewayApp) Reload() error {
return err return err
} }
// Status 返回网关运行状态
func (g *GatewayApp) Status() (string, error) {
addr, err := g.WebServer.Status()
if err != nil {
return "", err
}
hostCount := 0
if g.rd != nil {
hostCount = len(g.rd.Do("KEYS", GatewayConf.Prefix+":host:*").Strings())
}
return fmt.Sprintf("%s, dynamic_hosts: %d", addr, hostCount), nil
}
// loadAll 初始化或 Reload 阶段从 Redis 扫描所有网关配置并加载 // loadAll 初始化或 Reload 阶段从 Redis 扫描所有网关配置并加载
func (g *GatewayApp) loadAll(logger *log.Logger) { func (g *GatewayApp) loadAll(logger *log.Logger) {
if g.rd == nil { if g.rd == nil {
@ -97,10 +112,15 @@ func (g *GatewayApp) loadAll(logger *log.Logger) {
logger.Info("gateway loading full configuration") logger.Info("gateway loading full configuration")
hosts := g.rd.Do("SMEMBERS", GatewayConf.Prefix+":hosts").Strings() // 直接从 Redis 扫描所有符合前缀的 Key (无需手动维护 hosts Set)
for _, host := range hosts { keys := g.rd.Do("KEYS", GatewayConf.Prefix+":host:*").Strings()
prefixLen := len(GatewayConf.Prefix + ":host:")
for _, key := range keys {
if len(key) > prefixLen {
host := key[prefixLen:]
g.loadHost(host, logger) g.loadHost(host, logger)
} }
}
} }
// loadHost 加载指定 Host 下的所有类型配置 // loadHost 加载指定 Host 下的所有类型配置

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"},
@ -56,7 +60,6 @@ func TestGateway(t *testing.T) {
proxyJson, _ := json.Marshal(proxyRules) proxyJson, _ := json.Marshal(proxyRules)
rewriteJson, _ := json.Marshal(rewriteRules) rewriteJson, _ := json.Marshal(rewriteRules)
rd.Do("SADD", "gateway:hosts", "gw.test")
rd.Do("HSET", "gateway:host:gw.test", "proxies", string(proxyJson)) rd.Do("HSET", "gateway:host:gw.test", "proxies", string(proxyJson))
rd.Do("HSET", "gateway:host:gw.test", "rewrites", string(rewriteJson)) rd.Do("HSET", "gateway:host:gw.test", "rewrites", string(rewriteJson))
@ -130,6 +133,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
// ============================================ // ============================================

19
main.go
View File

@ -12,7 +12,24 @@ 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.1")
starter.SetUsage(`High-performance API gateway based on apigo.cc/go/service.
Full Guide: https://apigo.cc/go/service
Example env.yml:
service:
Listen: ":80"
Proxies:
"api.example.com":
"/user/*": "user-svc/v1/*" # KV Mode
"/auth": {"To": "auth-svc", "Auth": 1} # Object Mode
gateway:
Redis: "127.0.0.1:6379"
Matching Logic:
- Exact: "/login"
- Prefix: "/api/*" (requires suffix "/*")
- Regex: "^/v1/(.*)$" (contains "(")`)
// 3. 注册 Gateway 服务 // 3. 注册 Gateway 服务
// GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口 // GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口