Go Web 框架必備的 ServeMux 路由

【導讀】本文介紹了 ServeMux 路由的內部實現。

功能簡介

根據 Golang 文檔 (https://golang.org/pkg/net/http/#ServeMux) 中的介紹,ServeMux 是一個 HTTP 請求多路複用器 (HTTP Request multiplexer)。

具體來說,它主要有以下兩個功能:

  1. 路由功能,將請求的 URL 與一組已經註冊的路由模式 (pattern) 進行匹配,並選擇一個最接近模式,調用其處理函數。

  2. 它會清理請求 URL 與 Host 請求頭,清除端口號,並對包含 .. 和重複/的 URL 進行處理。

註冊名字固定的路徑

ServeMux 註冊路由模式的方式有兩種,註冊名字固定的路徑註冊根路徑子樹

註冊名字固定的路徑就是指定一個固定的 URL 和請求進行精確匹配。需要注意的是 / 路由模式會匹配所有未被其他路由模式匹配的請求。

例如下面的代碼,請求/a會匹配/a路由模式,請求/abc會匹配/路由模式。

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.HandleFunc("/a", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "/a", request.URL.EscapedPath())
 })
 http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "default", request.URL.EscapedPath())
 })
 log.Fatalln(http.ListenAndServe(":8080", nil))
}
>>> curl 'localhost:8080/a'
/a /a

>>> curl 'localhost:8080/abc'
default /abc

註冊根路徑子樹

註冊根路徑子樹就是註冊一個子路徑,所有以這個子路徑開頭的請求都會轉發到對應的 handler 中。注意子路徑__必須__以 / 結尾。

例如下面的代碼中,所有以 /user/ 開頭的請求,都會被匹配。

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.HandleFunc("/user/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "user", request.URL.EscapedPath())
 })
 log.Fatalln(http.ListenAndServe(":8080", nil))
}
>>> curl '0.0.0.0:8080/user/1'
user /user/1
>>> curl '0.0.0.0:8080/user/2'
user /user/2

最長匹配

註冊根路徑子樹是符合最長路徑匹配的原則的,例如我們註冊了兩個子路徑,/image/gif//image/,URL 爲/image/gif/的請求會優先匹配第一個路由模式。

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 // 注意我把 /image/ 寫在了前面
 http.HandleFunc("/image/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "image", request.URL.EscapedPath())
 })
 http.HandleFunc("/image/gif/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "gif", request.URL.EscapedPath())
 })
 log.Fatalln(http.ListenAndServe(":8080", nil))
}
>>> curl '0.0.0.0:8080/image/gif/'
gif /image/gif/

APPEND_SLASH

APPEND_SLASH是 Django 中的一個配置,是指如果我們註冊了一個路由模式/something/,且APPEND_SLASH=True的話,那麼 URL 爲 /something 的請求會自動重定向爲/something/

註冊根路徑子樹默認也是有這種功能的,請看下面的代碼:

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.HandleFunc("/image/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "image", request.URL.EscapedPath())
 })
 log.Fatalln(http.ListenAndServe(":8080", nil))
}
>>> curl -v '0.0.0.0:8080/image'
> GET /image HTTP/1.1
> Host: 0.0.0.0:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Content-Type: text/html; charset=utf-8
< Location: /image/
< Date: Sat, 30 Nov 2019 11:30:14 GMT
< Content-Length: 42
<
<a href="/image/">Moved Permanently</a>.

我們註冊了子路徑/image/,服務器會自動將/image請求重定向爲/image/

如果我們不想讓服務器自動重定向的話,只需要添加一個/image模式就好了。

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.HandleFunc("/image/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "sub image", request.URL.EscapedPath())
 })
 http.HandleFunc("/image", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "image", request.URL.EscapedPath())
 })
 log.Fatalln(http.ListenAndServe(":8080", nil))
}
>>> curl '0.0.0.0:8080/image'
image /image
>>> curl '0.0.0.0:8080/image/'
sub image /image/
>>> curl '0.0.0.0:8080/image/gif'
sub image /image/gif

使用域名匹配

ServeMux 還支持根據主機名精確匹配,沒有指定主機名的路徑/路由模式將會是所有未匹配請求的缺省值。

下面的代碼中,主機名爲 localhost 請求將會默認匹配localhost/路由模式,其他主機名的請求會匹配/路由模式。

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.HandleFunc("localhost/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "localhost", request.URL.EscapedPath())
 })
 http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "default", request.URL.EscapedPath())
 })
 log.Fatalln(http.ListenAndServe(":8080", nil))
}
>>> curl -v '127.0.0.1:8080/'
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 30 Nov 2019 11:39:04 GMT
< Content-Length: 10
< Content-Type: text/plain; charset=utf-8
<
default /

>>> curl -v '0.0.0.0:8080/'
> GET / HTTP/1.1
> Host: 0.0.0.0:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 30 Nov 2019 11:39:14 GMT
< Content-Length: 10
< Content-Type: text/plain; charset=utf-8
<
default /

>>> curl -v 'localhost:8080/'
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 30 Nov 2019 11:39:19 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
<
localhost /

>>> curl -v 'localhost:8080/abc'
> GET /abc HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 30 Nov 2019 12:01:47 GMT
< Content-Length: 15
< Content-Type: text/plain; charset=utf-8
<
localhost /abc

處理冗餘的 URL

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.HandleFunc("/image/png/1", func(writer http.ResponseWriter, request *http.Request) {
  fmt.Fprintln(writer, "default", request.URL.EscapedPath())
 })
 log.Fatalln(http.ListenAndServe(":8080", nil))
}
>>> curl -v 'localhost:8080/image/gif/../png/1'
> GET /image/png/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 30 Nov 2019 11:42:00 GMT
< Content-Length: 23
< Content-Type: text/plain; charset=utf-8
<
localhost /image/png/1

>>> curl -v 'localhost:8080/image/gif/../png//1'
> GET /image/png//1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Content-Type: text/html; charset=utf-8
< Location: /image/png/1
< Date: Sat, 30 Nov 2019 11:42:14 GMT
< Content-Length: 47
<
<a href="/image/png/1">Moved Permanently</a>.

>>> curl -v 'localhost:8080/image/png//1'
> GET /image/png//1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Content-Type: text/html; charset=utf-8
< Location: /image/png/1
< Date: Sat, 30 Nov 2019 11:45:18 GMT
< Content-Length: 47
<
<a href="/image/png/1">Moved Permanently</a>.

轉自:

bwangel.me/2019/11/30/intro-servemux/

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/s2mHPSkUoxA6DOmaEU9Y6A