gorilla/mux

GitHub: gorilla/mux

一个功能强大的 Go 语言 HTTP 路由器和 URL 匹配器,用于将传入的 Web 请求精准分发到对应的处理函数。

Stars: 21835 | Forks: 1889

# gorilla/mux ![测试](https://static.pigsec.cn/wp-content/uploads/repos/cas/2e/2ee4501713cc900ebd3da7006a2f6b4319054bd7168b47855074fd411ec4f8f0.svg) [![codecov](https://codecov.io/github/gorilla/mux/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/mux) [![godoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) [![sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github/gorilla/mux?badge) ![Gorilla Logo](https://static.pigsec.cn/wp-content/uploads/repos/cas/92/920e1e2c318d22d5d854634ae2de6ad6379f923a2ddcaeb669df3da996c19be6.png) 包 `gorilla/mux` 实现了一个请求路由器和分发器,用于将传入的请求匹配到它们各自的 handler。 mux 这个名字代表“HTTP 请求多路复用器”。与标准的 `http.ServeMux` 一样,`mux.Router` 会根据已注册的路由列表匹配传入的请求,并为匹配 URL 或其他条件的路由调用 handler。主要特性包括: * 它实现了 `http.Handler` 接口,因此与标准的 `http.ServeMux` 兼容。 * 可以根据 URL 主机、路径、路径前缀、协议 (scheme)、header 和 query 值、HTTP 方法或使用自定义匹配器来匹配请求。 * URL 主机、路径和 query 值可以包含带有可选正则表达式的变量。 * 可以构建或“反转”已注册的 URL,这有助于维护对资源的引用。 * 路由可以用作子路由器:只有在父路由匹配时,才会对嵌套路由进行测试。这对于定义共享公共条件(如主机、路径前缀或其他重复属性)的路由组非常有用。作为额外的好处,这优化了请求匹配。 * [安装](#install) * [示例](#examples) * [匹配路由](#matching-routes) * [静态文件](#static-files) * [服务单页应用](#serving-single-page-applications)(例如 React、Vue、Ember.js 等) * [已注册的 URL](#registered-urls) * [遍历路由](#walking-routes) * [优雅关闭](#graceful-shutdown) * [中间件](#middleware) * [处理 CORS 请求](#handling-cors-requests) * [测试 Handlers](#testing-handlers) * [完整示例](#full-example) ## 安装 拥有一个[正确配置](https://golang.org/doc/install#testing)的 Go 工具链后: ``` go get -u github.com/gorilla/mux ``` ## 示例 让我们从注册几个 URL 路径和 handler 开始: ``` func 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`) 作为参数。 路径可以包含变量。它们使用 `{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"]) } ``` 以上就是关于基本用法你需要了解的全部内容。更多高级选项在下面进行说明。 ### 匹配路由 路由也可以被限制在某个域名或子域名上。只需定义一个要匹配的主机模式即可。它们同样可以包含变量: ``` r := mux.NewRouter() // Only matches if domain is "www.example.com". r.Host("www.example.com") // Matches a dynamic subdomain. r.Host("{subdomain:[a-z]+}.example.com") ``` 还可以添加几个其他的匹配器。匹配路径前缀: ``` r.PathPrefix("/products/") ``` ...或者 HTTP 方法: ``` r.Methods("GET", "POST") ``` ...或者 URL scheme: ``` r.Schemes("https") ``` ...或者 header 值: ``` r.Headers("X-Requested-With", "XMLHttpRequest") ``` ...或者 query 值: ``` r.Queries("key", "value") ``` ...或者使用自定义匹配器函数: ``` 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") ``` 路由按照它们被添加到路由器的顺序进行测试。如果两个路由匹配,第一个胜出: ``` r := mux.NewRouter() r.HandleFunc("/specific", specificHandler) r.PathPrefix("/").Handler(catchAllHandler) ``` 一遍又一遍地设置相同的匹配条件可能会令人厌烦,因此我们提供了一种方法来将共享相同要求的几个路由进行分组。我们称之为“子路由”。 例如,假设我们有几个只有在主机是 `www.example.com` 时才应该匹配的 URL。为该主机创建一个路由,并从中获取一个“子路由器”: ``` 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` 时,才会对我们上面注册的三个 URL 路径进行测试,因为会首先测试子路由器。这不仅方便,还优化了请求匹配。你可以结合路由接受的任何属性匹配器来创建子路由器。 子路由器可用于创建域名或路径“命名空间”:你在一个中心位置定义子路由器,然后应用程序的各个部分可以注册相对于给定子路由器的路径。 关于子路由还有一件事。当子路由器具有路径前缀时,内部路由会将其作为路径的基础: ``` 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) ``` ### 静态文件 请注意,提供给 `PathPrefix()` 的路径代表一个“通配符”:调用 `PathPrefix("/static/").Handler(...)` 意味着该 handler 将接收并处理 任何匹配 "/static/\*" 的请求。这使得使用 mux 提供 (serve) 静态文件变得非常容易: ``` 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() // This will serve files under http://localhost:8000/static/ r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) srv := &http.Server{ Handler: r, 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()) } ``` ### 服务单页应用 大多数情况下,将你的 SPA 部署在与 API 不同的独立 Web 服务器上是有意义的, 但有时也希望能从同一个地方同时提供它们。你可以编写一个简单的 handler 来提供你的 SPA(例如与 React Router 的 [BrowserRouter](https://reacttraining.com/react-router/web/api/BrowserRouter) 一起使用),并利用 mux 强大的路由功能来处理你的 API endpoint。 ``` 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 wasn't that the file doesn't 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()) } ``` ### 已注册的 URL 现在让我们看看如何构建已注册的 URL。 路由可以被命名。所有定义了名称的路由都可以构建或“反转”其 URL。我们通过在路由上调用 `Name()` 来定义名称。例如: ``` r := mux.NewRouter() r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). Name("article") ``` 要构建一个 URL,需获取该路由并调用 `URL()` 方法,为路由变量传入一系列键值对。对于上面的路由,我们会这样做: ``` url, err := r.Get("article").URL("category", "technology", "id", "42") ``` ...结果将是一个具有以下路径的 `url.URL`: ``` "/articles/technology/42" ``` 这也适用于主机和 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?filter=gorilla" url, err := r.Get("article").URL("subdomain", "news", "category", "technology", "id", "42", "filter", "gorilla") ``` 路由中定义的所有变量都是必需的,并且它们的值必须符合相应的模式。这些要求保证了生成的 URL 将始终匹配已注册的路由 —— 唯一的例外是显式定义的“仅用于构建 (build-only)”路由,它们永远不会被匹配。 ``` r.HeadersRegexp("Content-Type", "application/(text|json)") ``` ...该路由将同时匹配 Content-Type 为 `application/json` 以及 `application/text` 的请求。 还有一种方法可以仅构建路由的 URL 主机或路径:改用 `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") ``` 如果你使用子路由器,单独定义的主机和路径也可以被构建: ``` 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] fmt.Println(r.Get("article").GetVarNames()) ``` ### 遍历路由 `mux.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) } ``` ### 优雅关闭 Go 1.8 引入了[优雅关闭](https://golang.org/doc/go1.8#http_shutdown) `*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 doesn't block. go func() { if err := srv.ListenAndServe(); err != nil { log.Println(err) } }() c := make(chan os.Signal, 1) // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) // 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() // Doesn't 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) } ``` ### 中间件 Mux 支持向一个 [Router](https://godoc.org/github.com/gorilla/mux#Router) 添加中间件,如果找到匹配项,中间件将按添加顺序执行,包括其子路由器。 中间件(通常)是一小段代码,它接收一个请求,对其进行某些处理,然后将其传递给另一个中间件或最终的 handler。中间件的一些常见用例是请求日志记录、header 操作或 `ResponseWriter` 劫持。 Mux 中间件使用事实上的标准类型来定义: ``` type MiddlewareFunc func(http.Handler) http.Handler ``` 通常,返回的 handler 是一个闭包,它对传递给它的 http.ResponseWriter 和 http.Request 进行某些处理,然后调用作为参数传递给 MiddlewareFunc 的 handler。这利用了闭包能够访问其创建上下文中的变量的特性,同时保留了接收者强制要求的签名。 一个用于记录正在处理的请求 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) ``` 一个更复杂的、将 session 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`;如果它 _不_ 打算终止请求,则 _不应_ 写入 `ResponseWriter`。 ### 处理 CORS 请求 [CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) 旨在让严格设置 `Access-Control-Allow-Methods` 响应 header 变得更容易。 * 你仍然需要使用自己的 CORS handler 来设置其他的 CORS header,例如 `Access-Control-Allow-Origin` * 中间件会将 `Access-Control-Allow-Methods` header 设置为路由上所有的匹配方法(例如 `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) * 如果你没有指定任何方法,那么: 这是一个使用 `CORSMethodMiddleware` 以及自定义 `OPTIONS` handler 来设置所有必需的 CORS header 的示例: ``` 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; charset=utf-8 < * Connection #0 to host localhost left intact foo ``` ### 测试 Handlers 在 Go Web 应用程序中测试 handler 非常简单,而且 _mux_ 也不会使其变得更加复杂。假设有两个文件:`endpoints.go` 和 `endpoints_test.go`,下面是我们如何测试一个使用 _mux_ 的应用程序。 首先,我们简单的 HTTP 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 don't have any query parameters for now, so we'll // 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) } } ``` 如果我们的路由包含[变量](#examples),我们可以在请求中传递它们。我们可以编写 [表驱动测试](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) 来根据需要测试 多种可能的路由变量。 ``` // 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)) } ``` 我们的测试文件,包含针对 `routeVariables` 的表驱动测试: ``` // 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 doesn't 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) } } } ``` ## 完整示例 这是一个基于 `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)) } ``` ## License BSD licensed。有关详细信息,请参阅 LICENSE 文件。
标签:EVTX分析, 日志审计