尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

KubeSphere 依赖深读:gorilla/mux 请求路由器的完整原理与实战指南

KubeSphere 依赖深读:gorilla/mux 请求路由器的完整原理与实战指南 KubeSphere 依赖深读gorilla/mux 请求路由器的完整原理与实战指南【免费下载链接】kubesphereThe container platform tailored for Kubernetes multi-cloud, datacenter, and edge management ⎈ ☁️项目地址: https://gitcode.com/GitHub_Trending/ku/kubesphere本篇以 KubeSphere 仓库 vendor 目录中的 gorilla/mux 官方 README 为主体骨架系统讲解这个 Go HTTP 请求路由器HTTP request multiplexer的全部核心能力带正则约束的路径变量、Host/Method/Header/Query 多维匹配、子路由命名空间、URL 反向构建、中间件链与 CORS 中间件、优雅关闭与 Handler 测试。读完你可以独立编写、调试基于 mux 的 HTTP 服务并能对照 vendor/github.com/gorilla/mux/mux.go 等源码理解匹配与调度的底层机制。1. 定位mux 是什么在 KubeSphere 中处于什么位置Packagegorilla/mux实现了一个请求路由器与分发器router and dispatcher将入站请求匹配到各自的 handler。名字 mux 即 HTTP request multiplexer。与标准库http.ServeMux一样mux.Router将入站请求与已注册路由列表逐一比对并调用匹配路由的 handler。其核心特性见 README 开头与 doc.go实现http.Handler接口可与标准库http.ServeMux互换请求可基于 URL host、path、path 前缀、scheme、header 与 query 值、HTTP method或自定义 matcher 匹配URL host、path、query 值可带变量的模板且变量可附加可选的正则表达式约束已注册 URL 可以反向构建reversed方便代码中维护资源引用路由可用作子路由器subrouter嵌套路由只在父路由匹配时才会被测试既便于分组又优化了匹配过程。在 KubeSphere 仓库中mux 以间接依赖形式存在——go.mod 中声明为github.com/gorilla/mux v1.8.1 // indirect。它并非 KubeSphere 主程序直接引用而是被 vendored 依赖引入vendor/github.com/docker/distribution/registry/api/v2/routes.goDocker Registry V2 API 的路由构建镜像仓库服务KubeSphere 的镜像/制品能力相关依赖vendor/github.com/open-policy-agent/opa/v1/plugins/plugins.goOPA 策略引擎的插件 HTTP 服务。因此深入理解 mux有助于读懂 KubeSphere 供应链中 Registry、OPA 等组件的 HTTP 层实现。mux 采用 BSD 许可见 vendor/github.com/gorilla/mux/LICENSE。2. 安装在正确配置的 Go 工具链下go get -u github.com/gorilla/mux由于 KubeSphere 仓库采用 vendor 模式构建时直接使用仓库内 vendor/github.com/gorilla/mux/ 下的源码mux.go、route.go、regexp.go、middleware.go无需联网拉取。3. 基础用法注册路径与 handler先注册几个 URL 路径和 handlerfunc main() { r : mux.NewRouter() r.HandleFunc(/, HomeHandler) r.HandleFunc(/products, ProductsHandler) r.HandleFunc(/articles, ArticlesHandler) http.Handle(/, r) }这里注册了三条路由把 URL 路径映射到 handler。其工作方式等价于http.HandleFunc()当入站请求 URL 匹配某条路径时对应 handler 会被调用参数为 (http.ResponseWriter,*http.Request)。对照源码HandleFunc只是NewRoute().Path(path).HandlerFunc(f)的组合糖见 mux.go而NewRouter()只是初始化了一个带namedRoutes映射的空Router结构体见 mux.go。3.1 路径变量路径可以包含变量格式为{name}或{name:pattern}。若未定义正则变量默认匹配到下一个斜杠之前的任意内容r : mux.NewRouter() r.HandleFunc(/products/{key}, ProductHandler) r.HandleFunc(/articles/{category}/, ArticlesCategoryHandler) r.HandleFunc(/articles/{category}/{id:[0-9]}, ArticleHandler)变量名用于构造路由变量 map通过mux.Vars()获取func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { vars : mux.Vars(r) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, Category: %v\n, vars[category]) }这就是基本用法的要点更高级的选项见下文。源码级补充默认模式与捕获组约束。regexp.go 中的newRouteRegexp揭示了变量模板的编译细节默认 pattern 按匹配类型区分path 变量默认[^/]query 变量默认.*host 变量默认[^.]每个变量被编译成命名捕获组(?Pnamepattern)同时生成一份反向模板reverse template用于 URL 构建若模板中意外出现捕获组如/{sort:(asc|desc)}编译后子表达式数量与变量数不一致mux 会直接 panic要求改写为非捕获组(?:asc|desc)——doc.go 也明确提示这一点避免使用捕获组导致的行为异常。另外pattern 内部可以使用分组group但必须是非捕获形式(?:re)例如r.HandleFunc(/articles/{category}/{sort:(?:asc|desc|new)}, ArticlesCategoryHandler)4. 路由匹配Host、前缀、方法、Scheme、Header、Query 与自定义 matcher4.1 全部匹配器路由还可以限制域名或子域。定义一个 host 模板即可host 模板同样支持变量r : mux.NewRouter() // 仅当域名为 www.example.com 时匹配。 r.Host(www.example.com) // 匹配动态子域名。 r.Host({subdomain:[a-z]}.example.com)还有几种可叠加的匹配器。匹配路径前缀r.PathPrefix(/products/)匹配 HTTP 方法r.Methods(GET, POST)匹配 URL schemer.Schemes(https)匹配 header 值r.Headers(X-Requested-With, XMLHttpRequest)匹配 query 值r.Queries(key, value)使用自定义 matcher 函数r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { return r.ProtoMajor 0 })最后可以在一条路由上组合多个匹配器r.HandleFunc(/products, ProductsHandler). Host(www.example.com). Methods(GET). Schemes(http)源码级补充匹配顺序与方法不匹配。路由按注册顺序测试若两条路由都能匹配先注册者胜出Router.Match对r.routes顺序遍历见 mux.go。当路径匹配但方法不匹配时route.go 的Route.Match会记录ErrMethodMismatch并继续尝试后续路由最终若无路由完全匹配Router.ServeHTTP返回 405可被MethodNotAllowedHandler覆盖或 404可被NotFoundHandler覆盖这两个哨兵错误定义在 mux.go。路由按注册顺序测试的示例r : mux.NewRouter() r.HandleFunc(/specific, specificHandler) r.PathPrefix(/).Handler(catchAllHandler)4.2 子路由Subrouting反复设置相同的匹配条件会很烦人mux 提供了子路由把共享条件的路由分组。假设若干 URL 只在 host 为www.example.com时才应匹配可先为该 host 创建路由并取其子路由器r : mux.NewRouter() s : r.Host(www.example.com).Subrouter()然后在子路由器中注册路由s.HandleFunc(/products/, ProductsHandler) s.HandleFunc(/products/{key}, ProductHandler) s.HandleFunc(/articles/{category}/{id:[0-9]}, ArticleHandler)上面三条路径只在域名为www.example.com时才会被测试因为子路由器会被先行测试。这不仅方便也优化了请求匹配。你可以用任意属性匹配器组合创建子路由器。子路由器可用来构建域名或路径命名空间在集中位置定义子路由器各业务模块相对该子路由器注册自己的路径。若子路由器带有路径前缀内部路由会把它作为自身路径的基座r : mux.NewRouter() s : r.PathPrefix(/products).Subrouter() // /products/ s.HandleFunc(/, ProductsHandler) // /products/{key}/ s.HandleFunc(/{key}/, ProductHandler) // /products/{key}/details s.HandleFunc(/{key}/details, ProductDetailsHandler)仓库实例佐证。vendor 中的 Docker Registry V2 API 正是子路由 命名路由的典型用法routes.gofunc RouterWithPrefix(prefix string) *mux.Router { rootRouter : mux.NewRouter() router : rootRouter if prefix ! { router router.PathPrefix(prefix).Subrouter() } router.StrictSlash(true) for _, descriptor : range routeDescriptors { router.Path(descriptor.Path).Name(descriptor.Name) } return rootRouter }该实现为 Registry 的 manifest、tags、blob、blob-upload、catalog 等 V2 端点统一生成命名路由RouteNameManifest、RouteNameBlobUpload等常量见 routes.go并开启StrictSlash(true)统一斜杠行为——与后文第 7 节的行为配置直接呼应。5. 静态文件服务PathPrefix()提供的路径代表一个通配符PathPrefix(/static/).Handler(...)意味着 handler 会接收匹配 /static/* 的所有请求。这让用 mux 服务静态文件变得容易func main() { var dir string flag.StringVar(dir, dir, ., the directory to serve files from. Defaults to the current dir) flag.Parse() r : mux.NewRouter() // 文件将在 http://localhost:8000/static/filename 下提供 r.PathPrefix(/static/).Handler(http.StripPrefix(/static/, http.FileServer(http.Dir(dir)))) srv : http.Server{ Handler: r, Addr: 127.0.0.1:8000, // 良好实践为你创建的服务器设置超时 WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Fatal(srv.ListenAndServe()) }6. 服务单页应用SPA多数场景下 SPA 应与 API 分开放在不同 Web 服务器上但有时希望两者同出一处。可以为 SPA 写一个简单的 handler例如配合 React Router 的 BrowserRouter并利用 mux 的强大路由能力承载 API 端点package main import ( encoding/json log net/http os path/filepath time github.com/gorilla/mux ) // spaHandler implements the http.Handler interface, so we can use it // to respond to HTTP requests. The path to the static directory and // path to the index file within that static directory are used to // serve the SPA in the given static directory. type spaHandler struct { staticPath string indexPath string } // ServeHTTP inspects the URL path to locate a file within the static dir // on the SPA handler. If a file is found, it will be served. If not, the // file located at the index path on the SPA handler will be served. This // is suitable behavior for serving an SPA (single page application). func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Join internally call path.Clean to prevent directory traversal path : filepath.Join(h.staticPath, r.URL.Path) // check whether a file exists or is a directory at the given path fi, err : os.Stat(path) if os.IsNotExist(err) || fi.IsDir() { // file does not exist or path is a directory, serve index.html http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) return } if err ! nil { // if we got an error (that wasnt that the file doesnt exist) stating the // file, return a 500 internal server error and stop http.Error(w, err.Error(), http.StatusInternalServerError) return } // otherwise, use http.FileServer to serve the static file http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) } func main() { router : mux.NewRouter() router.HandleFunc(/api/health, func(w http.ResponseWriter, r *http.Request) { // an example API handler json.NewEncoder(w).Encode(map[string]bool{ok: true}) }) spa : spaHandler{staticPath: build, indexPath: index.html} router.PathPrefix(/).Handler(spa) srv : http.Server{ Handler: router, Addr: 127.0.0.1:8000, // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Fatal(srv.ListenAndServe()) }该 handler 的策略是命中真实文件就提供文件否则回退 index.html——这正是前端 history 路由浏览器直链、刷新页面所需的行为。注意filepath.Join内部调用path.Clean可防止目录穿越。7. 命名路由与 URL 反向构建下面看如何构建已注册的 URL。路由可以命名所有定义了名字的路由都可以反向构建其 URL。通过Name()定义名字r : mux.NewRouter() r.HandleFunc(/articles/{category}/{id:[0-9]}, ArticleHandler). Name(article)构建 URL 时先按名字取得路由再调用URL()方法按顺序传入路由变量的 key/value 对url, err : r.Get(article).URL(category, technology, id, 42)得到的url.URL路径为/articles/technology/42host 与 query 变量同样支持r : mux.NewRouter() r.Host({subdomain}.example.com). Path(/articles/{category}/{id:[0-9]}). Queries(filter, {filter}). HandlerFunc(ArticleHandler). Name(article) // url.String() will be http://news.example.com/articles/technology/42?filtergorilla url, err : r.Get(article).URL(subdomain, news, category, technology, id, 42, filter, gorilla)路由中定义的所有变量都是必需的且取值必须符合对应模式。这些约束保证了生成的 URL 总能匹配某条已注册路由——唯一例外是显式声明BuildOnly()的仅构建路由它永不匹配请求见 route.go。Header 也支持正则匹配例如r.HeadersRegexp(Content-Type, application/(text|json))该路由同时匹配Content-Type为application/json和application/text的请求。还可以只构建 URL 的 host 或 path 部分使用URLHost()或URLPath()// http://news.example.com/ host, err : r.Get(article).URLHost(subdomain, news) // /articles/technology/42 path, err : r.Get(article).URLPath(category, technology, id, 42)若使用子路由器分开定义的 host 与 path 也可以组合构建r : mux.NewRouter() s : r.Host({subdomain}.example.com).Subrouter() s.Path(/articles/{category}/{id:[0-9]}). HandlerFunc(ArticleHandler). Name(article) // http://news.example.com/articles/technology/42 url, err : r.Get(article).URL(subdomain, news, category, technology, id, 42)要列出某条路由调用URL()时的全部必需变量可使用GetVarNames()r : mux.NewRouter() r.Host({domain}). Path(/{group}/{item_id}). Queries(some_data1, {some_data1}). Queries(some_data2, {some_data2}). Name(article) // Will print [domain group item_id some_data1 some_data2] nil fmt.Println(r.Get(article).GetVarNames())源码级补充反向构建的原理。第 3 节提到的newRouteRegexp在解析模板时同步生成了reverse反向模板变量位置以%s占位并对每个变量 pattern 编译一个取值校验器正则^pattern$regexp.go。因此URL()生成的每个取值都会先过校验器再填进反向模板——生成 URL 必然匹配已注册路由正是由这条校验链路保证的。8. 遍历路由Walkmux.Router上的Walk函数可访问路由器上注册的所有路由。下面的示例打印所有已注册路由package main import ( fmt net/http strings github.com/gorilla/mux ) func handler(w http.ResponseWriter, r *http.Request) { return } func main() { r : mux.NewRouter() r.HandleFunc(/, handler) r.HandleFunc(/products, handler).Methods(POST) r.HandleFunc(/articles, handler).Methods(GET) r.HandleFunc(/articles/{id}, handler).Methods(GET, PUT) r.HandleFunc(/authors, handler).Queries(surname, {surname}) err : r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { pathTemplate, err : route.GetPathTemplate() if err nil { fmt.Println(ROUTE:, pathTemplate) } pathRegexp, err : route.GetPathRegexp() if err nil { fmt.Println(Path regexp:, pathRegexp) } queriesTemplates, err : route.GetQueriesTemplates() if err nil { fmt.Println(Queries templates:, strings.Join(queriesTemplates, ,)) } queriesRegexps, err : route.GetQueriesRegexp() if err nil { fmt.Println(Queries regexps:, strings.Join(queriesRegexps, ,)) } methods, err : route.GetMethods() if err nil { fmt.Println(Methods:, strings.Join(methods, ,)) } fmt.Println() return nil }) if err ! nil { fmt.Println(err) } http.Handle(/, r) }从源码结构看mux.goWalk按注册顺序深度优先遍历子路由器回调收到当前路由、当前路由器以及到达该路由的祖先路由链回调返回SkipRouter可跳过某个子路由器返回其他错误则中止遍历。这一能力常用于生成 API 文档或调试路由表。9. 优雅关闭Graceful ShutdownGo 1.8 引入了对*http.Server的优雅关闭能力。以下是配合 mux 的完整做法package main import ( context flag log net/http os os/signal time github.com/gorilla/mux ) func main() { var wait time.Duration flag.DurationVar(wait, graceful-timeout, time.Second * 15, the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m) flag.Parse() r : mux.NewRouter() // Add your routes as needed srv : http.Server{ Addr: 0.0.0.0:8080, // Good practice to set timeouts to avoid Slowloris attacks. WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, IdleTimeout: time.Second * 60, Handler: r, // Pass our instance of gorilla/mux in. } // Run our server in a goroutine so that it doesnt block. go func() { if err : srv.ListenAndServe(); err ! nil { log.Println(err) } }() c : make(chan os.Signal, 1) // Well accept graceful shutdowns when quit via SIGINT (CtrlC) // SIGKILL, SIGQUIT or SIGTERM (Ctrl/) will not be caught. signal.Notify(c, os.Interrupt) // Block until we receive our signal. -c // Create a deadline to wait for. ctx, cancel : context.WithTimeout(context.Background(), wait) defer cancel() // Doesnt block if no connections, but will otherwise wait // until the timeout deadline. srv.Shutdown(ctx) // Optionally, you could run srv.Shutdown in a goroutine and block on // -ctx.Done() if your application should wait for other services // to finalize based on context cancellation. log.Println(shutting down) os.Exit(0) }要点ReadTimeout/WriteTimeout防止 Slowloris 类慢连接攻击-graceful-timeout参数控制srv.Shutdown(ctx)等待存量连接结束的上限仅监听 SIGINTCtrlCSIGKILL、SIGQUIT、SIGTERM 不会被捕获。10. 中间件MiddlewareMux 支持向Router追加中间件一旦找到匹配路由含其子路由器中间件按添加顺序执行。中间件通常是小段代码接收一个请求、对其做处理、再向下传递给下一个中间件或最终 handler。常见用途包括请求日志、header 改写、ResponseWriter劫持如 gzip 压缩。Mux 中间件采用事实标准类型定义type MiddlewareFunc func(http.Handler) http.Handler通常返回的 handler 是一个闭包对传入的http.ResponseWriter和http.Request做些事情然后调用作为参数传入的 handler。这利用了闭包可访问其定义处上下文变量的特性同时保持MiddlewareFunc签名的一致性。一个记录请求 URI 的最简中间件func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Do stuff here log.Println(r.RequestURI) // Call the next handler, which can be another middleware in the chain, or the final handler. next.ServeHTTP(w, r) }) }通过Router.Use()将中间件挂到路由器上r : mux.NewRouter() r.HandleFunc(/, handler) r.Use(loggingMiddleware)一个更复杂的认证中间件会话 token 到用户的映射// Define our struct type authenticationMiddleware struct { tokenUsers map[string]string } // Initialize it somewhere func (amw *authenticationMiddleware) Populate() { amw.tokenUsers[00000000] user0 amw.tokenUsers[aaaaaaaa] userA amw.tokenUsers[05f717e5] randomUser amw.tokenUsers[deadbeef] user0 } // Middleware function, which will be called for each request func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token : r.Header.Get(X-Session-Token) if user, found : amw.tokenUsers[token]; found { // We found the token in our map log.Printf(Authenticated user %s\n, user) // Pass down the request to the next middleware (or final handler) next.ServeHTTP(w, r) } else { // Write an error and stop the handler chain http.Error(w, Forbidden, http.StatusForbidden) } }) }r : mux.NewRouter() r.HandleFunc(/, handler) amw : authenticationMiddleware{tokenUsers: make(map[string]string)} amw.Populate() r.Use(amw.Middleware)注意如果你的中间件没有调用next.ServeHTTP()handler 链就会在此中断——这正是中间件主动中止请求的手段。中间件若决定终止请求应当写ResponseWriter若不终止则不应写。源码级补充链的构造顺序。中间件的包装发生在Router.Match命中路由之后mux.go从r.middlewares的尾部向头部依次用r.middlewares[i].Middleware(match.Handler)包裹 handler。因此最先Use()的中间件处于最外层、最先执行与 README按添加顺序执行的表述一致且当MatchErr非空例如方法不匹配走了 405 路径时不会构建中间件链。Use与MiddlewareFunc的实现见 middleware.go。11. 处理 CORS 请求CORSMethodMiddleware旨在简化Access-Control-Allow-Methods响应头的严格设置其余 CORS 头如Access-Control-Allow-Origin仍需你自己的 CORS handler 设置中间件会把路由上所有 method matcher例如r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)写入Access-Control-Allow-Methods-Access-Control-Allow-Methods: GET,PUT,OPTIONS若未指定任何方法则重要路由必须存在OPTIONSmethod matcher中间件才会设置该头。下面是CORSMethodMiddleware配合自定义OPTIONShandler 设置全部所需 CORS 头的示例package main import ( net/http github.com/gorilla/mux ) func main() { r : mux.NewRouter() // IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers r.HandleFunc(/foo, fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions) r.Use(mux.CORSMethodMiddleware(r)) http.ListenAndServe(:8080, r) } func fooHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set(Access-Control-Allow-Origin, *) if r.Method http.MethodOptions { return } w.Write([]byte(foo)) }对该/foo端点发起如下的请求curl localhost:8080/foo -v响应形如* Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) GET /foo HTTP/1.1 Host: localhost:8080 User-Agent: curl/7.59.0 Accept: */* HTTP/1.1 200 OK Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS Access-Control-Allow-Origin: * Date: Fri, 28 Jun 2019 20:13:30 GMT Content-Length: 3 Content-Type: text/plain; charsetutf-8 * Connection #0 to host localhost left intact foo源码级补充。middleware.go 中CORSMethodMiddleware对每个请求调用getAllMethodsForRoute遍历r.routes凡能匹配该请求、或产生ErrMethodMismatch的路由都收集其GetMethods()结果仅当集合中包含OPTIONS时才设置响应头。这解释了必须声明 OPTIONS matcher这一前置条件的实现原因。12. 测试 Handler用 Go 测试 HTTP handler 很直接mux 也不会增加任何额外复杂度。给定两个文件endpoints.go与endpoints_test.go首先一个简单的健康检查 handler// endpoints.go package main func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { // A very simple health check. w.Header().Set(Content-Type, application/json) w.WriteHeader(http.StatusOK) // In the future we could report back on the status of our DB, or our cache // (e.g. Redis) by performing a simple PING, and include them in the response. io.WriteString(w, {alive: true}) } func main() { r : mux.NewRouter() r.HandleFunc(/health, HealthCheckHandler) log.Fatal(http.ListenAndServe(localhost:8080, r)) }对应的测试代码// endpoints_test.go package main import ( net/http net/http/httptest testing ) func TestHealthCheckHandler(t *testing.T) { // Create a request to pass to our handler. We dont have any query parameters for now, so well // pass nil as the third parameter. req, err : http.NewRequest(GET, /health, nil) if err ! nil { t.Fatal(err) } // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. rr : httptest.NewRecorder() handler : http.HandlerFunc(HealthCheckHandler) // Our handlers satisfy http.Handler, so we can call their ServeHTTP method // directly and pass in our Request and ResponseRecorder. handler.ServeHTTP(rr, req) // Check the status code is what we expect. if status : rr.Code; status ! http.StatusOK { t.Errorf(handler returned wrong status code: got %v want %v, status, http.StatusOK) } // Check the response body is what we expect. expected : {alive: true} if rr.Body.String() ! expected { t.Errorf(handler returned unexpected body: got %v want %v, rr.Body.String(), expected) } }如果路由带有变量可以把它们放进请求里测试并用表驱动测试覆盖多种路由变量取值// endpoints.go func main() { r : mux.NewRouter() // A route with a route variable: r.HandleFunc(/metrics/{type}, MetricsHandler) log.Fatal(http.ListenAndServe(localhost:8080, r)) }对应的表驱动测试// endpoints_test.go func TestMetricsHandler(t *testing.T) { tt : []struct{ routeVariable string shouldPass bool }{ {goroutines, true}, {heap, true}, {counters, true}, {queries, true}, {adhadaeqm3k, false}, } for _, tc : range tt { path : fmt.Sprintf(/metrics/%s, tc.routeVariable) req, err : http.NewRequest(GET, path, nil) if err ! nil { t.Fatal(err) } rr : httptest.NewRecorder() // To add the vars to the context, // we need to create a router through which we can pass the request. router : mux.NewRouter() router.HandleFunc(/metrics/{type}, MetricsHandler) router.ServeHTTP(rr, req) // In this case, our MetricsHandler returns a non-200 response // for a route variable it doesnt know about. if rr.Code http.StatusOK !tc.shouldPass { t.Errorf(handler should have failed on routeVariable %s: got %v want %v, tc.routeVariable, rr.Code, http.StatusOK) } } }注意这里必须把请求经由router.ServeHTTP处理而不是直接调用 handler——因为路由变量是在ServeHTTP中匹配成功后才写入请求 Context 的见 mux.go 中的requestWithVarshandler 内部mux.Vars(r)依赖这份 Context。13. 完整示例一个可运行的最小 mux 服务器package main import ( net/http log github.com/gorilla/mux ) func YourHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte(Gorilla!\n)) } func main() { r : mux.NewRouter() // Routes consist of a path and a handler function. r.HandleFunc(/, YourHandler) // Bind to a port and pass our router in log.Fatal(http.ListenAndServe(:8000, r)) }14. 进阶行为配置StrictSlash、UseEncodedPath、SkipClean 与请求上下文README 主线之外的路由行为差异值得对照 mux.go 的结构说明清楚StrictSlash初始 false设为 true 后路由路径为 /path/ 时访问 /path 会被 301 重定向到前者反之亦然保证应用始终以路由定义的形式看到路径。源码注释特别警告对 POST/PUT 等非幂等方法多数客户端重定向后会变为 GET需要自行用中间件或客户端配置规避带PathPrefix()的路由因仅凭前缀无法确定重定向行为而忽略 strict slash但其子路由器会继承该设置mux.go。Docker Registry 的RouterWithPrefix正是显式开启此选项。SkipClean初始 false设为 true 后 /path//to 的双斜杠会被保留适合诸如/fetch/http://xkcd.com/534/这类路径否则会被 cleanPath 清洗为/fetch/http/xkcd.com/534mux.go。UseEncodedPath默认按未编码路径匹配即 /path/foo%2Fbar/to 会按 /path/foo/bar/to 参与匹配调用后改为用编码路径匹配使 /path/foo%2Fbar/to 能命中 /path/{var}/to 这样的单段变量mux.go。404/405 定制Router.NotFoundHandler与Router.MethodNotAllowedHandler字段允许自定义未匹配与方法不允许的响应mux.go。请求上下文匹配成功后路由变量与匹配到的路由分别以varsKey、routeKey写入请求 Contextmux.Vars(r)与mux.CurrentRoute(r)是读取入口其中CurrentRoute只在匹配路由的 handler 内部有效mux.go。15. 小结与延伸阅读原文档vendor/github.com/gorilla/mux/README.md本文骨架来源含全部示例与许可说明。包文档vendor/github.com/gorilla/mux/doc.go包级注释补充了非捕获组与捕获组 panic 的说明。路由核心vendor/github.com/gorilla/mux/mux.goRouter、Match/ServeHTTP、Walk、Context 存取。模板编译vendor/github.com/gorilla/mux/regexp.go变量解析、正则生成、反向模板。匹配器与路由属性vendor/github.com/gorilla/mux/route.goHost/Path/Methods/Queries/Name/BuildOnly 等。中间件与 CORSvendor/github.com/gorilla/mux/middleware.go。仓库内真实用例vendor/github.com/docker/distribution/registry/api/v2/routes.go、vendor/github.com/open-policy-agent/opa/v1/plugins/plugins.go。mux 的价值在于把多维条件匹配 变量提取 反向 URL 构建收敛到一套链式 API 中注册期即完成模板编译与正则校验错误在路由定义时暴露而非运行时运行期按注册顺序短路匹配命中后以 Context 传递变量。掌握这些机制后阅读 KubeSphere vendor 依赖中基于 mux 构建的 HTTP 服务Registry V2 API、OPA 插件接口将不再有障碍。【免费下载链接】kubesphereThe container platform tailored for Kubernetes multi-cloud, datacenter, and edge management ⎈ ☁️项目地址: https://gitcode.com/GitHub_Trending/ku/kubesphere创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表