使用golang构建web应用的背后

golang提供的网络编程库对于开发者来说十分易用,使用golang能轻松写出可靠、高效的web应用。并且能够让使用者清晰地理解web应用的构成及基本原理。

什么是web应用

单从狭义的角度来看,web应用应该包含以下三大最基本的功能:

  1. 能够接收从客户端发来的HTTP请求报文。
  2. 能够对HTTP请求报文进行处理。
  3. 能够将结果通过HTTP响应报文发送回客户端。

对于这三大基本功能,一个完整的web应用应包含:多路复用器(multiplexer)与控制器(handler)。

多路复用器

当请求到来时,多路复用器会根据路径将请求送到事先绑定好的处理器,交由处理器来处理HTTP请求报文。

1
2
3
4
5
6
mux := http.NewServeMux()   //构造多路复用器mux

//绑定路径与handler
mux.Handle("/handlerfunc1", http.HandlerFunc(handlerfunc)) //使用http.HandlerFunc强转
mux.HandleFunc("/handlerfunc2", handlerfunc) //使用mux.HandleFunc隐藏强转
mux.Handle("/handler", handler) //使用handler接口的实现(实现ServeHTTP方法)

让我们来看下多路复用器的类型ServeMux

1
2
3
4
5
6
7
8
9
10
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}

type muxEntry struct {
h Handler
pattern string
}

可以看到,ServeMux结构体里包含了三部分内容,一把读写锁mu、一个map类型m以及一个布尔值hosts
map里存储的就是路径与处理器的映射。由于map不是线程安全的,所以需要一把读写锁来对map进行保护。hosts我们先不讨论。

多路复用器ServeMux类型实现了ServeHTTP方法,因此多路复用器其本质上也是一个处理器。

1
2
3
4
5
6
7
8
9
10
11
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}

为什么要将多路复用器与处理器分开呢?我想是因为一下两点原因:

  1. 职责划分不通:多路复用器是一个总索引,主要的职责是将路径与处理器绑定起来。而处理器主要是针对不通的业务进行处理。
  2. 代码清晰:将不通的处理器单独写,并由多路复用器汇总,这样代码逻辑清晰,便于维护。

除了net/http包中的ServeMux。还有很多各有特色的多路复用器,最常见的有muxHttpRouter

处理器

一个类型实现了ServeHTTP(w ResponseWriter, r *Request)函数就能成为一个简单的处理器。其中Request是客户端发来的请求,ResponseWriter则是给客户端的回复。
此外,处理器可以通过串联的方式来进行非侵入的扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type content string

func (i content) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "%s", i)
}

func logUtil(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Print("some log")
h.ServeHTTP(w, r)
})
}

func main() {
mux := http.NewServeMux()
content := content("some content")
mux.Handle("/content", logUtil(content))
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: mux,
}
server.ListenAndServe()
}

代码中logUtil就是一个用来给处理器进行扩展的函数。logUtil函数传入一个http.Handler,返回一个http.Handler。因此,可以将此类的函数进行串联,为处理器提供功能丰富的扩展。

监听

可以通过http.Server结构体类型来设置web应用的一些参数,并使用ListenAndServe()函数来启动web应用。

1
2
3
4
5
server := http.Server{
Addr: "127.0.0.1:8080",
}

server.ListenAndServe()

当然,我们仅需要关注我们感兴趣的那些参数,不必对每个参数进行设置,绝大多数参数都具有默认值。例如在http.Server结构体类型中如果不指定处理器的话,默认会使用http.DefaultServeMux来作为处理器。
同样的,我们可以为http.Server设置一个处理器来替代默认的处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//无论遇到什么请求都只会返回hello
type hello struct {}

func (h hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello")
}

func main() {
h := hello{}

server := http.Server{
Addr: "127.0.0.1:8080",
Handler:h,
}
server.ListenAndServe()
}

在调用ListenAndServe()之后web应用便启动起来了,它的本质其实就是一个死循环,在不断获取监听到的请求并交由处理器处理。
ListenAndServe()中调用了Server类型的Serve(l net.Listener)函数。web应用的死循环就是写在Serve函数中的。
Serve函数中使用无条件的for循环获取监听的连接,将连接以goroutine的方式交给conn类型的serve(ctx context.Context)函数处理。最后通过在serve函数中调用Handler接口的ServeHTTP函数来处理具体的业务逻辑。