Compare commits

..

1 Commits
main ... v1.0.0

Author SHA1 Message Date
AI Engineer
6ed590e018 feat: init gateway v1.0.0, AI-First documentation refined (by AI) 2026-05-13 01:57:08 +08:00
6 changed files with 133 additions and 159 deletions

124
README.md
View File

@ -1,78 +1,64 @@
# @go/gateway (高性能动态网关)
# @go/gateway
基于 `@go/service` 实现的微服务 API 网关。详细路由语法与高级配置参考:[https://apigo.cc/go/service](https://apigo.cc/go/service)
基于 `@go/service` 和 Redis 的高性能、事件驱动动态 API 网关。
详细配置指南请参考核心文档:[https://apigo.cc/go/service](https://apigo.cc/go/service)
---
## 路由匹配逻辑 (AI-First 规范)
## 🚀 1. 快速开始 (1分钟)
为了确保 AI 模型和运维人员能够精准配置而不产生幻觉,网关遵循以下严格的路径匹配优先级:
### 1. 路径匹配 (Path Matching)
- **精确匹配 (Exact)**:
- 语法:`"Path": "/user/info"`
- 行为:仅当请求路径完全等于 `/user/info` 时触发。
- **前缀匹配 (Prefix)**:
- 语法:`"Path": "/api/*"` (**必须以 `/*` 结尾**)
- 行为:匹配所有以 `/api/` 开头的路径(如 `/api/login`, `/api/v1/status`)。
- *映射规则*: 若配置 `{"Path": "/v1/*", "ToPath": "/v2/*"}`,请求 `/v1/a` 将被转发至 `/v2/a`
- **正则匹配 (Regex)**:
- 语法:任何包含括号 `(` 的路径将被视为正则表达式。
- 示例:`"Path": "^/api/(.*)$"`。使用 `$1` 进行 `ToPath` 替换。
### 2. 域名选择 (Host Selection)
匹配请求头中的 `Host` 字段,按以下顺序尝试,匹配即止:
1. `example.com:8080` (精确域名 + 端口)
2. `example.com` (仅域名,忽略端口)
3. `:8080` (仅端口,匹配该端口下的所有 Host)
4. `*` (全局兜底通配符)
## 动态配置模型 (Redis)
网关以 **Host (域名)** 为最小更新单元。更新某域名配置时,请向 Redis 频道发布该域名的纯字符串。
- **存储结构 (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"` (域名原始字符串)
## 启动与管理
```bash
# 1. 安装最新版
go get apigo.cc/go/gateway@latest
# 以后台进程启动
./gateway start
# 2. 配置 env.yml (KV 极简语法)
# ----------------------------------------------------------------------
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" # [静态目录] 映射本地路径
# 查看服务状态 (通过 IPC)
./gateway status
# 平滑重启 / 重载兜底
./gateway restart
./gateway reload # 触发底层 service 的 OnReload全量同步 Redis
```
## 环境变量配置
支持通过环境变量或 `env.yml` 覆盖。
```yaml
gateway:
Redis: "127.0.0.1:6379" # (可选) 动态配置中心
# ----------------------------------------------------------------------
# 3. 运行
go run . start
Redis: "127.0.0.1:6379"
Prefix: "gateway"
Channel: "gateway:channel"
```
---
## 🔄 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,7 +6,6 @@ import (
"apigo.cc/go/redis"
"apigo.cc/go/service"
"context"
"fmt"
"strings"
)
@ -90,20 +89,6 @@ func (g *GatewayApp) Reload() error {
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 扫描所有网关配置并加载
func (g *GatewayApp) loadAll(logger *log.Logger) {
if g.rd == nil {
@ -112,15 +97,10 @@ func (g *GatewayApp) loadAll(logger *log.Logger) {
logger.Info("gateway loading full configuration")
// 直接从 Redis 扫描所有符合前缀的 Key (无需手动维护 hosts Set)
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:]
hosts := g.rd.Do("SMEMBERS", GatewayConf.Prefix+":hosts").Strings()
for _, host := range hosts {
g.loadHost(host, logger)
}
}
}
// loadHost 加载指定 Host 下的所有类型配置

View File

@ -60,6 +60,7 @@ func TestGateway(t *testing.T) {
proxyJson, _ := json.Marshal(proxyRules)
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", "rewrites", string(rewriteJson))

34
go.mod
View File

@ -3,26 +3,26 @@ module apigo.cc/go/gateway
go 1.25.0
require (
apigo.cc/go/cast v1.3.3
apigo.cc/go/config v1.3.1
apigo.cc/go/log v1.3.4
apigo.cc/go/redis v1.3.2
apigo.cc/go/service v1.3.4
apigo.cc/go/starter v1.0.5
apigo.cc/go/cast v1.3.1
apigo.cc/go/config v1.3.0
apigo.cc/go/log v1.3.1
apigo.cc/go/redis v1.3.0
apigo.cc/go/service v1.3.1
apigo.cc/go/starter v1.0.1
)
require (
apigo.cc/go/crypto v1.3.1 // indirect
apigo.cc/go/discover v1.3.2 // indirect
apigo.cc/go/encoding v1.3.1 // indirect
apigo.cc/go/file v1.3.2 // indirect
apigo.cc/go/http v1.3.2 // indirect
apigo.cc/go/id v1.3.1 // indirect
apigo.cc/go/rand v1.3.1 // indirect
apigo.cc/go/safe v1.3.1 // indirect
apigo.cc/go/shell v1.3.1 // indirect
apigo.cc/go/timer v1.3.1 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
apigo.cc/go/crypto v1.3.0 // indirect
apigo.cc/go/discover v1.3.0 // indirect
apigo.cc/go/encoding v1.3.0 // indirect
apigo.cc/go/file v1.3.0 // indirect
apigo.cc/go/http v1.3.0 // indirect
apigo.cc/go/id v1.3.0 // indirect
apigo.cc/go/rand v1.3.0 // indirect
apigo.cc/go/safe v1.3.0 // indirect
apigo.cc/go/shell v1.3.0 // indirect
apigo.cc/go/timer v1.3.0 // indirect
github.com/gomodule/redigo v1.9.3 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/net v0.54.0 // indirect

74
go.sum
View File

@ -1,45 +1,51 @@
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
apigo.cc/go/crypto v1.3.1 h1:ulQ2zX9bUWirk0sEacx1Srsjs2Jow7HlZq7ED7msNcg=
apigo.cc/go/crypto v1.3.1/go.mod h1:SwHlBFDPddttWgFFtzsEMla8CM/rcFy9nvdsJjW4CIs=
apigo.cc/go/discover v1.3.2 h1:hzo5PQSAbJrF9Qk7yCMoXWatHGe7lR2MNUqLLQ5oUSk=
apigo.cc/go/discover v1.3.2/go.mod h1:77kZLGovdCYAMspkmL6iif65Yzhyg143ffQseZx+j40=
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
apigo.cc/go/http v1.3.2 h1:0Or5KfoIq4+yeWKYusYPV8XLPw8XuzJMeaFv7dZViLI=
apigo.cc/go/http v1.3.2/go.mod h1:Q9R7Ors0Fz2A6Mxg0dykO2PjCzdAHRRXreOUMjMOLwA=
apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g=
apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20=
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/redis v1.3.2 h1:iUWL/CHHnfonz0dJq6/V4IG3QuXBoHA2L1xnoGEbNEQ=
apigo.cc/go/redis v1.3.2/go.mod h1:/k5wcfAzB9jrfd9otabio9CPUxEsLPgEs4oggBG5sbs=
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
apigo.cc/go/service v1.3.4 h1:RYvyvb9RXdZ2IFwHahsc68ioTQWcsJOOHtrX0i3sUB4=
apigo.cc/go/service v1.3.4/go.mod h1:BshopRL5GX7euDv9ZmTThaOUJSROGtR4HTesuor+FPI=
apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
apigo.cc/go/starter v1.0.5 h1:pgjBun7zc3J+3hcWnP22bGtgB3+TiNXOrOGVyGM7u1Q=
apigo.cc/go/starter v1.0.5/go.mod h1:auAvnBknZuMMps6HRtlwf8Z5gCHRQYsVw7WhhiGwWpg=
apigo.cc/go/timer v1.3.1 h1:YMSusF1LfJYOf6tAW94Yipj3pHrX6QhfP7Rk3nGFT8k=
apigo.cc/go/timer v1.3.1/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
apigo.cc/go/cast v1.3.1 h1:Y64mit3tCtA1gnSaeaPNf9QjvwX1RA+hFc80j/yUMnI=
apigo.cc/go/cast v1.3.1/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI=
apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8=
apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc=
apigo.cc/go/discover v1.3.0 h1:CXuKtAZygU+4TMHtebVkjWyyWmPgoLbsJFdKFGiCOd8=
apigo.cc/go/discover v1.3.0/go.mod h1:VMu1qC6AngVFQMdaCwGoq3/PPX0xDnjkG+1AcSA+Zvs=
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
apigo.cc/go/http v1.3.0 h1:1ZweotOuAxTI8wfib9knWYXM2t0POOJ3ezgOKObH3sg=
apigo.cc/go/http v1.3.0/go.mod h1:DC3phxBNbt/dOWdhxtffAEYeUs3j6P3BD8e6J8gxU9U=
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA=
apigo.cc/go/log v1.3.1 h1:ihpVtAzpE4Q3hnid8b5GpBBCxGyzPUQInmIzJeL+2BA=
apigo.cc/go/log v1.3.1/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE=
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU=
apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ=
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
apigo.cc/go/service v1.3.1 h1:AvUpGLJBdqcMLyMuWR5w2r9LLexdovYk7xhD6lnWNDU=
apigo.cc/go/service v1.3.1/go.mod h1:HQfsODicW0nN84oAosgl3kNEhn+IUnsnRbd1sIzHgYs=
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4=
apigo.cc/go/starter v1.0.1 h1:7Qv/rRlEVlTX7wjr1LpV1XX1wUD4UAssDi6J+YCh73s=
apigo.cc/go/starter v1.0.1/go.mod h1:xHfo+36hXGdVhhnRqd1l+Vk6Fp1ecN2LDAcsDOVodXk=
apigo.cc/go/timer v1.3.0 h1:dorVGKw0xR6Gj8Pwfl86K46szMBfD31XyO+uUqxU+EI=
apigo.cc/go/timer v1.3.0/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
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/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8=
github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=

33
main.go
View File

@ -12,24 +12,25 @@ func main() {
_ = config.Load(&GatewayConf, "gateway")
// 2. 初始化 Starter 信息
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
starter.SetAppInfo("gateway", "1.0.0")
starter.SetUsage(`High-performance, event-driven dynamic API gateway.
Full Documentation: 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"
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.
Matching Logic:
- Exact: "/login"
- Prefix: "/api/*" (requires suffix "/*")
- Regex: "^/v1/(.*)$" (contains "(")`)
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 服务
// GatewayApp 自身实现了 starter.Service 和 starter.Reloader 接口