首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Go 语言 Web 编程系列(三)—— 基于官方标准库自定义路由处理器

1、引子

从这一篇教程起,我们将从自定义路由器开始探索 Go Web 编程之旅。

开始之前,我们还是回顾下创建第一个 Web 应用中的示例代码:

代码语言:javascript
复制
代码语言:javascript
复制
http.HandleFunc("/", sayHelloWorld)
err := http.ListenAndServe(":9091", nil)

我们在上篇教程介绍过这段代码的底层实现,这里 http.ListenAndServe 方法第二个参数传入的是 nil,表示底层会使用默认的 DefaultServeMux 实现将上述 HandleFunc 方法传入的处理函数转化为基于闭包方式定义的路由:

如上篇教程所言,如果我们想要实现自定义的路由处理器,则需要构建一个自定义的、实现了 Handler 接口的类实例作为 http.ListenAndServe 的第二个参数传入。

在开始介绍自定义路由处理器实现之前,我们先来看看 DefaultServeMux 是如何保存路由映射规则以及分发请求做路由匹配的。

2、路由映射规则

DefaultServeMux 所属的类型是 ServeMux

代码语言:javascript
复制
代码语言:javascript
复制
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

首先我们来看一下 ServeMux 的数据结构:

代码语言:javascript
复制
type ServeMux struct {
    mu    sync.RWMutex. // 由于请求涉及到并发处理,因此这里需要一个锁机制
    m     map[string]muxEntry // 路由规则字典,存放URL路径与处理器的映射关系
    es    []muxEntry // MuxEntry 切片(按照最长到最短排序)
    hosts bool       // 路由规则中是否包含 host 信息
}

这里,我们需要重点关注的是 muxEntry 结构:

代码语言:javascript
复制
type muxEntry struct {
    h   Handler       // 处理器具体实现
    pattern string    // 模式匹配字符串
}

最后我们来看一下 Handler 的定义,这是一个接口:

代码语言:javascript
复制
代码语言:javascript
复制
type Handler interface {
    ServeHTTP(ResponseWriter, *Request) // 路由处理实现方法
}

我们之前定义的 sayHelloWorld 方法并没有实现 Handler 接口,之所以可以成功添加到路由映射规则中是因为在底层通过 HandlerFunc() 函数将其强制转化为了 HandlerFunc 类型,而 HandlerFunc 类型实现了 ServeHTTP 方法,这样,sayHelloWorld 方法也就实现了 Handler 接口:

代码语言:javascript
复制
代码语言:javascript
复制
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
      panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

...

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

对于 sayHelloWorld 方法来说,它已然变成了 HandlerFunc 类型的函数类型,当我们在其实例上调用 ServeHTTP 方法时,调用的是 sayHelloWorld 方法本身。

当我们在 DefaultServeMux.HandleFunc 中调用 mux.Handle 方法时,实际上是将其路由映射规则保存到 DefaultServeMux 路由处理器的数据结构中:

代码语言:javascript
复制
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)
  }
  e := muxEntry{h: handler, pattern: pattern}
  mux.m[pattern] = e
  if pattern[len(pattern)-1] == '/' {
    mux.es = appendSorted(mux.es, e)
  }

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

还是以 sayHelloWorld 为例,这里的 pattern 字符串对应的是请求路径 /handler 对应的是 sayHelloWorld 函数。

3、请求分发与路由匹配

保存好路由映射规则之后,客户端请求又是怎么分发的呢?或者说请求 URL 与 DefaultServeMux 中保存的路由映射规则是如何匹配的呢?

我们在上篇教程介绍过,处理客户端请求时,会调用默认 ServeMux 实现的 ServeHTTP 方法:

代码语言:javascript
复制
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        w.Header().Set("Connection", "close")
        w.WriteHeader(StatusBadRequest)
        return
    }
    
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}
代码语言:javascript
复制

如上所示,路由处理器接收到请求之后,如果 URL 路径是 *,则关闭连接,否则调用 mux.Handler(r) 返回对应请求路径匹配的处理器,然后执行 h.ServeHTTP(w, r),也就是调用对应路由 handlerServerHTTP 方法,以 / 路由为例,调用的就是 sayHelloWorld 函数本身。

至于 mux.Handler(r) 的底层匹配实现,感兴趣的同学可以去 net/http 包中查看对应的底层源码,这里就不详细展开了。

通过上面的介绍,我们了解了基于 DefaultServeMux 实现的整个路由规则存储(Web 应用启动期间进行)和请求匹配过程(客户端发起请求时进行),下面我们来看一下如何实现自定义的 路由处理器。

4、自定义路由处理器

如果你搞清楚了上面的默认实现,编写自定义的路由处理器就会非常简单,我们只需要定义一个实现了 Handler 接口的类,然后将其实例传递给 http.ListenAndServe 方法即可:

代码语言:javascript
复制
代码语言:javascript
复制
package main

import (
    "fmt"
    "net/http"
)

type MyHander struct {

}

func (handler *MyHander) ServeHTTP(w http.ResponseWriter, r *http.Request)  {
    if r.URL.Path == "/" {
        sayHelloGolang(w, r)
        return
    }
    http.NotFound(w, r)
    return
}

func sayHelloGolang(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello Golang!")
}

func main()  {
    handler := MyHander{}
    http.ListenAndServe(":9091", &handler)
}

我们运行 go run router.go 来启动这个应用,然后在浏览器中就可以访问 / 路由了:

这个实现很简单,而且我们并没有在应用启动期间初始化路由映射规则,而是在应用启动之后根据请求参数动态判断来做分发的,这样做会影响性能,而且非常不灵活,我们当然可以仿照 DefaultServeMux 来实现自定义的 ServeMux,不过已经有非常好的第三方轮子可以直接拿来用了,比如 gorilla/mux,后续教程我们都将使用它作为路由器,下篇教程我们将简单介绍它的基本使用。

下一篇
举报
领券