Appearance
18|路由配置与加载
最小代理只能把请求转发到单一后端。实际网关需要按路径把请求分发到不同服务:/api/users 转发到用户服务,/api/orders 转发到订单服务,/static/ 转发到静态文件服务器。这种映射关系叫路由表。本章把硬编码的后端地址改成可配置的路由表,并添加一个简单的管理 API 来查看和修改路由配置。
一、路由表数据结构
路由的核心是"请求路径匹配后端地址"。最简单的匹配方式是前缀匹配:请求路径以某个前缀开头,就转发到对应的后端。
go
package main
import (
"net/url"
"sync"
)
type Route struct {
Prefix string // 路径前缀,如 "/api/users"
Backend string // 后端地址,如 "http://127.0.0.1:8081"
}
type Router struct {
mu sync.RWMutex
routes []Route
}
func NewRouter() *Router {
return &Router{
routes: []Route{
{Prefix: "/api/users", Backend: "http://127.0.0.1:8081"},
{Prefix: "/api/orders", Backend: "http://127.0.0.1:8082"},
},
}
}
func (r *Router) Match(path string) *url.URL {
r.mu.RLock()
defer r.mu.RUnlock()
for _, route := range r.routes {
if len(path) >= len(route.Prefix) && path[:len(route.Prefix)] == route.Prefix {
u, _ := url.Parse(route.Backend)
return u
}
}
return nil
}Router 用 sync.RWMutex 保护路由表,支持并发读取和独占写入。Match 按顺序遍历路由表,找到第一个匹配的前缀就返回对应的后端地址。
前缀匹配的顺序很重要。如果把 / 放在最前面,所有请求都会匹配到它,后面的规则永远不会生效。通常按前缀长度从长到短排序,或者让更具体的规则排在前面。
二、代理改造
把固定代理改成按路由表匹配:
go
func main() {
router := NewRouter()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
target := router.Match(r.URL.Path)
if target == nil {
http.Error(w, "no route matched", http.StatusNotFound)
return
}
proxy := httputil.NewSingleHostReverseProxy(target)
// ... 设置 Director、ErrorHandler
proxy.ServeHTTP(w, r)
})
http.ListenAndServe(":8080", handler)
}每次请求都创建新的 ReverseProxy 实例,这在性能上不是最优的——创建代理对象有开销。更优的做法是缓存代理实例,或者自己实现 Director 来动态修改目标地址。当前版本为了代码清晰,先保持简单。
三、管理 API
网关需要提供一个管理接口,让运维人员查看当前路由配置。管理 API 和代理服务可以跑在同一个端口上(用路径区分),也可以分开端口。分开端口更安全——管理 API 不暴露在公网,只在内网访问。
本章先跑在同一个端口上,后续再加端口分离和认证。
go
func (r *Router) ListRoutes() []Route {
r.mu.RLock()
defer r.mu.RUnlock()
// 返回副本,避免调用方修改内部状态
result := make([]Route, len(r.routes))
copy(result, r.routes)
return result
}注册管理路由:
go mux
// 代理转发
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
// 管理 API 路径不走代理
if req.URL.Path == "/admin/routes" {
http.Error(w, "not found", http.StatusNotFound)
return
}
target := router.Match(req.URL.Path)
// ... 代理转发逻辑
})
// 管理 API
mux.HandleFunc("/admin/routes", func(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
routes := router.ListRoutes()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(routes)
})
http.ListenAndServe(":8080", mux)http.NewServeMux 是路由表,按精确路径匹配请求。/admin/routes 走管理 API,其他路径走代理转发。ServeMux 不支持前缀匹配,它只匹配完整路径。
四、验证
启动两个后端服务:
bash
# 用户服务
cat > backend1.go <<'EOF'
package main
import ("fmt"; "net/http")
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "user service: %s\n", r.URL.Path)
})
http.ListenAndServe(":8081", nil)
}
EOF
go run backend1.go
# 订单服务
cat > backend2.go <<'EOF'
package main
import ("fmt"; "net/http")
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "order service: %s\n", r.URL.Path)
})
http.ListenAndServe(":8082", nil)
}
EOF
go run backend2.go测试代理:
bash
curl http://127.0.0.1:8080/api/users/list
# user service: /api/users/list
curl http://127.0.0.1:8080/api/orders/123
# order service: /api/orders/123
curl http://127.0.0.1:8080/admin/routes
# [{"Prefix":"/api/users","Backend":"http://127.0.0.1:8081"},...]五、路由设计注意事项
| 问题 | 处理 |
|---|---|
| 多个前缀重叠 | 按前缀长度从长到短排序,或按注册顺序匹配第一个 |
| 没有匹配的路由 | 返回 404,或设置默认后端 |
| 后端地址无效 | 解析 URL 时检查格式,启动时校验连通性 |
| 路由表并发修改 | 用 sync.RWMutex 保护读写 |
当前路由表是内存中的硬编码数据。下一讲会引入配置文件,让路由规则可以从文件加载和动态更新。
六、常见错误
前缀匹配顺序错误
go
routes := []Route{
{Prefix: "/", Backend: "http://default"},
{Prefix: "/api", Backend: "http://api"},
}/api/users 会匹配到 /,因为 / 排在前面。应该把更具体的前缀排在前面,或按长度排序。
不加锁并发读写路由表
go
// goroutine A
route := router.Match("/api/users")
// goroutine B
router.routes = append(router.routes, newRoute)并发读写切片会触发数据竞争,可能导致不可预期的结果。sync.RWMutex 或 sync.Map 是必须的。
返回内部状态引用
go
func (r *Router) ListRoutes() []Route {
return r.routes // 返回了内部切片的引用
}调用方可以修改返回的切片,影响 Router 的内部状态。应该返回副本。