Go 学习笔记(三十二)自带的 ServeMux multiplexer

本文原创地址:博客园骏马金龙Go Web:自带的 ServeMux multiplexer

ServeMux 简介

ServeMux 扮演的角色是 Multiplexer,它用来将将请求根据 url 路由给已注册的 handler。如下图:

733013-20181125171455406-1479331184.png

上图中为 3 个路径注册了 handler,一个是 "/",另外两个是 "/hello" 和 "/world"。这表示访问http://hostname/hello时,multiplexer 会调用上图中对应的第二个 handler,当访问http://hostname/world时,multiplexer 会调用上图中对应的第三个 handler,当不是这两个路径时,将调用第一个绑定在 "/" 上的 handler。

注意, go 的 mux 路由请求时,handler 绑定的路径是否带尾随 "/" 是不一样的。带上尾随 "/",表示此路径以及此路径下的子路径,都会调用注册在此路径上的 handler

例如,当请求 uri 为 "/hello/abc" 的时候,不会调用 "/hello" 对应的 handler,而是调用 "/" 对应的 handler。只有注册 handler 的路径为 "/hello/" 时,uri 为 "/hello/abc" 才会调用此 handler。

实际上,当注册 handler 的路径带上尾随斜 "/" 时,在发起此路径的请求时,会通过 301 重定向的方式自动补齐这个尾随斜线,让浏览器发起第二次请求。例如,下面是注册 handler 的路径:

http.Handle("/hello/", &myHandler)

发起http://hostname/hello的请求时,会自动补齐为http://hostname/hello/,然后浏览器自动发起第二次请求。

ServeMux 的匹配规则

ServeMux 对每次流入的 http 请求的 URL 进行模式 (pattern) 匹配,然后调用注册在此 pattern 上的 handler 来处理这个请求。

Pattern 部分可以定义为匹配 host 的模式。如果 pattern 以 "/" 开头,表示匹配 URL 的路径部分,如果不以 "/" 开头,表示从 host 开始匹配。

匹配时选择匹配匹配度最高 (长匹配优先于短匹配)。例如为 "/images/" 注册了 handler1,"/images/thumbnails/" 注册了 handler2,如果请求的 URL 路径部分为 "/images/thumbnails/",将会调用 handler2 处理这个请求,如果请求的 URL 路径部分为 "/images/foo/",将调用 handler1 处理。

注意,注册在 "/" 上的 pattern 会在其它模式匹配不上时被选中,因为所有请求都可以匹配这个 pattern,只不过能匹配到的长度最短。

如果 pattern 带上了尾随斜线 "/",ServeMux 将会对请求不带尾随斜线的 URL 进行 301 重定向。例如,在 "/images/" 模式上注册了一个 handler,当请求的 URL 路径为 "/images" 时,将自动重定向为 "/images/"。除非再单独为 "/images" 模式注册一个 handler。

如果为 "/images" 注册了 handler,当请求 URL 路径为 "/images/" 时,将无法匹配该模式。

ServeMux 详细解释

看看 net/http/server.go 文件中 ServeMux 的结构:

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
	h       Handler
	pattern string
}

结构看上去很简单。一个字段 mu 是 RWMutex,m 是注册 handler 和 pattern 的,hosts 用于判断 pattern 是否包含了 host 的匹配。看看 Handle() 函数的定义会更清晰:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}

// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	mux.Handle(pattern, HandlerFunc(handler))
}

pattern 为空或者 handler 为空时,都会 panic。此外,想要定义重复的 pattern,也会 panic。如果 pattern 的第一个字符不是 "/",则表示这个 pattern 是从主机名开始匹配的。

唯一需要注意的是,每个 Handle() 都会对 ServeMux 实例加上写锁。

以常用的 DefaultServeMux 为例,它是 ServeMux 的一个实例。当使用 DefualtServeMux 时,每调用一次 Handle()或 HandleFunc(),都意味着向这个 DefaultServeMux 的结构中的 m 字段添加 pattern 和对应的 handler。由于加了写锁,如果使用多个 goroutine 同时启动多个 web 服务,在同一时刻将只能有一个 goroutine 启动的 web 服务能设置 DefaultServeMux。当然,一般情况下不会使用 goroutine 的方式同时启动多个 web 服务。

第三方 ServeMux

自带的默认的 DefaultServeMux 其实功能限制很大。比如请求的 URL 路径为 "/images/123.png",想要匹配这个确实容易,但是想要取出其中的 "123.png" 字符串,DefaultServeMux 就没法实现。

有一个非常强大的 Gorilla 工具包 (www.gorillatoolkit.org),它有好几个功能,其中一个功能是提供 multiplexer。

上一篇 Go 学习笔记(三十一)URLs
Go 学习笔记(目录)
下一篇 Go 学习笔记(三十三)HttpRouter 路由