请求和响应流程
/*
创建一个简单的Go Web服务器
*/
func main() {
http.HandleFunc("/hello", helloWorld)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
func helloWorld(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello Go Web")
}
Go Web服务器的请求和响应流程如下:
- 客户端发送请求;
- 服务器端的多路复用器收到请求:
- 多路复用器根据请求的URL找到注册的处理器,将请求交由处理器处理;
- 处理器执行程序逻辑,如果必要,则与数据库进行交互,得到处理结果;
- 处理器调用模板引擎将指定的模板和上一步得到的结果渲染成客户端可识别的数据格式(通常是HTML格式);
- 服务器端将数据通过HTTP响应返回给客户端;
- 客户端拿到数据,执行对应的操作(例如渲染出来呈现给用户)。
接收请求
ServeMux 和 DefaultServeMux
ServeMux是一个结构体,其中包含一个映射,这个映射会将URL映射至相应的处理器。它会在映射中找出与被请求URL最为匹配的URL,然后调用与之相对应的处理器的ServeHTTP()方法来处理请求。
DefaultServeMux是net/htp包中默认提供的一个多路复用器,其实质是ServeMux的一个实例。多路复用器的任务是一根据请求的URL将请求重定向到不同的处理器。如果用户没有为Server对象指定处理器,则服务器默认使用DefaultServeMux作为ServeMux结构体的实例。
ServeMux也是一个处理器,可以在需要时对其实例实施处理器串联。默认的多路复用器DefaultServeMux位于库文件src/net/http/server.go中,其声明语句如下:
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
HandleFunc()函数用于为指定的URL注册一个处理器。HandleFunc()处理器函数会在内部调用DefaultServeMux对象的对应方法,其内部实现如下:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
通过上面的方法体可以看出,http.HandleFunc()函数将处理器注册到多路复用器中。用默认多路复用器还可以指定多个处理器,其使用方法如下。
/*
用默认多路处理器指定多个处理器
*/
func main() {
handle1 := handle1{}
handle2 := handle2{}
//nil表明服务器使用默认的多路复用器DefaultServeMux
server := http.Server{
Addr: "0.0.0.0:8080",
Handler: nil,
}
//handle()函数调用的是多路复用器的DefaultServeMux.Handler()方法
http.Handle("/handle1", handle1)
http.Handle("/handle2", handle2)
server.ListenAndServe()
}
//定义多个处理器
type handle1 struct{}
func (h1 handle1) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hi, handle1")
}
type handle2 struct{}
func (h2 handle2) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hi, handle2")
}
在上面的代码中,直接用http.Handle(0函数来指定多个处理器。Handle()函数的代码如下:
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
通过代码可以看到,在http.Handle()函数中调用了DefaultServeMux.Handle()方法来处理请求。服务器收到的每个请求都会调用对应多路复用器的ServeHTTP()方法。该方法的代码详情如下:
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
在ServeMux对象的ServeHTTP()方法中,会根据URL查找我们注册的处理器,然后将请求交由它处理。
虽然默认的多路复用器使用起来很方便,但是在生产环境中不建议使用。这是因为:DefaultServeMux是一个全局变量,所有代码(包括第三方代码)都可以修改它。有些第三方代码会在DefaultServeMux中注册一些处理器,这可能与我们注册的处理器冲突。比较推荐的做法是自定义多路复用器。
自定义多路复用器也比较简单,直接调用http.NewServeMux()函数即可。然后,在新创建的多路复用器上注册处理器:
/*
自定义多路复用器
*/
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", hi)
server := http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
func hi(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi Web")
}
上面代码的功能和默认多路复用器程序的功能相同,都是启动一个HTTP服务器端。这里还创建了服务器对象Server。.通过指定服务器的参数,可以创建定制化的服务器:
server := {
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
在上面代码中,创建了一个读超时和写超时均为5s的服务器。
简单总结一下,ServerMux实现了http.Handler接口的ServeHTTP(ResponseWriter, *Request)方法。在创建Server时,如果设置Handler为空,则使DefaultServeMux作为默认的处理器,而DefaultServeMux是ServerMux的一个全局变量。
ServeMux的URL路由匹配
在实际应用中,一个Web服务器往往有很多的URL绑定,不同的URL对应不同的处理器。
假如我们现在绑定了3个URL,分别是/
、/h
i和/hi/web
。
- 如果请求的URL为/,则调用/对应的处理器。
- 如果请求的URL为/hi,则调用/hi对应的处理器。
- 如果请求的URL为/hi/web,则调用/hi/web对应的处理器。
如果注册的URL不是以/结尾的,则它只能精确匹配请求的URL。反之,即使请求的URL只有前缀与被绑定的URL相同,则ServeMux也认为它们是匹配的。例如,如果请求的URL为/hi/,则不能匹配到/hi。因为/hi不以/结尾,必须精确匹配。如果我们绑定的URL为/h/i,则当服务器找不到与/hi/others完全匹配的处理器时,就会退而求其次,开始寻找能够与/hi/匹配的处理器。
/*
路由匹配
*/
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/hi", hiHandler)
mux.HandleFunc("/hi/web", webHandler)
server := http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "处理器为:indexHandler!路径为:/")
}
func hiHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "处理器为:hiHandler!路径为:/hi")
}
func webHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "处理器为:webHandler!路径为/hi/web")
}
访问localhost:8080/
访问localhost:8080/hi
访问localhost:8080/hi/web
访问localhost:8080/hi/
这里的处理器是indexHandler,因为/hi需要精确匹配,而请求的/hi/不能与之精确匹配,所以向上查找到/。
处理器和处理器函数都可以进行URL路由匹配。通常情况下,可以使用处理器和处理器函数中的一种或同时使用两者。同时使用两者的示例代码如下。
/*
同时使用处理器和处理器函数
*/
func main() {
mux := http.NewServeMux()
//注册处理器函数
mux.HandleFunc("/hi", hiHandler1)
//注册处理器
mux.Handle("/welcome/goweb", welcomeHandler{Name: "Hi, Go Handle"})
server := http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
//处理器函数 handlerFunc
func hiHandler1(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi, Go HandleFunc")
}
//处理器 handler
type welcomeHandler struct {
Name string
}
func (h welcomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hi, %s", h.Name)
}
处理器和处理器函数
处理器
服务器在收到请求后,会根据其URL将请求交给相应的多路复用器;然后,多路复用器将请求转发给处理器处理。处理器是实现了Handler接口的结构。Handler接口被定义在net/http包中:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
可以看到,Handler接口中只有一个ServeHTTP()处理方法。任何实现了Handler接口的对象,对可以被注册到注册到多路复用器中。
可以定义一个结构体来实现该接口的方法,以注册这个结构体类型的对象到多路复用器中,见下方示例代码。
/*
Handler接口的使用示例
*/
type WelComeHandler struct {
Language string
}
func (h WelComeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", h.Language)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/cn", WelComeHandler{
Language: "1111",
})
mux.Handle("/en", WelComeHandler{
Language: "2222",
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
处理器函数
/*
处理器函数
*/
func hihihi(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hihihi")
}
func main() {
http.HandleFunc("/", hihihi)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
虽然,自定义处理器这种方式比较灵活和强大,但是它需要定义一个新的结构来实现ServeHTTP()方法,还是比较烦琐的。
为了方便使用,net/http包提供了以函数的方式注册处理器,即用HandleFunc()函数来注册处理器。如果一个函数实现了匿名函数func(w http.ResponseWriter,.r*http.Request),则这个函数被称为“处理器函数”。HandleFunc()函数内部调用了ServeMux对象的HandleFunc()方法。
ServeMux对象的HandleFunc()方法的具体代码如下:
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
继续查看内部代码可以发现HandlerFunc()函数最终也实现了Handler接口的ServeHTTP()方法。其实现代码如下:
type HandlerFunc func(w *Responsewriter,r *Request)
func (f HandlerFunc) ServeHTTP (w ResponseWriter,r *Request) {
f(w,r)
}
以上这几个函数或方法名很容易混淆,它们的调用关系如图所示。
- Handler:处理器接口。定义在net/http包中,实现Handler接口的对象,可以被注册到多路复用器中。
- Handle():注册处理器过程中的调用函数。
- HandleFunc():处理器函数。
- HandlerFunc:底层为func(w ResponseWriter,r*Request)匿名函数,实现了Handler处理器接口。它用来连接处理器函数与处理器。
简而言之,HandlerFunc()是一个处理器函数其内部通过对ServeMux中一系列方法的调用,最终在底层实现了Handler处理器接口的ServeHTTP()方法,从而实现处理器的功能。
串联多个处理器和处理器函数
函数可以被当作参数传递给另一个函数,即可以串联多个函数来对某些方法进行复用,从而解决代码的重复和强依赖问题。
而实际上,处理器也是一个函数。所以在处理诸如日志记录、安全检查和错误处理这样的操作时,我们往往会把这些通用的方法进行复用,这时就需要串联调用这些函数。可以使用串联技术来分隔代码中需要复用的代码。串联多个函数的示例代码如下。
/*
串联多个函数
*/
func main() {
http.HandleFunc("/", log(index))
http.ListenAndServe(":8080", nil)
}
func log(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Printf(" time: %s| handlerfunc:%s\n", time.Now().String(),
runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name())
}
}
func log2(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("*******")
h.ServeHTTP(w, r)
})
}
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello index!")
}
构建模型
一个完整的Wb项目包含“处理器处理请求”“用模型操作数据库”“通过模板引擎(或处理器)将模型从数据库中返回的数据和模板拼合在一起,并生成HTML或者其他格式的文档”,以及“通过HTTP报文传输给客户端”这4步。
服务器端通过模型连接处理器和数据库的流程如图所示。
- 新建一个保存用户模型的包model:
package model
- 创建一个名为User的结构体
type User struct {
Uid int
Name string
Phone string
}
- 在模型结构体中定义3个字段Uid、Name、Phone,分别为int、string、string类型。为了访问数据库,还需要导入“database/sql”包及“github.com/go-sql-driver/mysql”包,并定义一个db的全局变量:
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
var DB *sql.DB
- 通过init()函数初始化数据库连接
//初始化数据库连接
func init() {
DB, _ = sql.Open("mysql",
"root:123456@tcp(127.0.0.1:3306)/chapter05")
}
- 定义用来获取用户信息的函数:
//获取用户信息
func GetUser(uid int) (u User) {
// 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
err := DB.QueryRow("select uid,name,phone from `user` where uid=?", uid).Scan(&u.Uid, &u.Name, &u.Phone)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
return u
}
- 创建HTML模板文件
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Welcome to my page</title>
</head>
<body>
<ul>
{{ range . }}
<h1 style="text-align:center">{{ . }}</h1>
{{ end}}
<h1 style="text-align:center">Welcome to my page</h1>
<p style="text-align:center">this is the user info page</p>
</ul>
</body>
</html>
- 创建控制器
package controller
import (
"../model"
"fmt"
"html/template"
"net/http"
"strconv"
)
type UserController struct {
}
func (c UserController) GetUser(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
uid, _ := strconv.Atoi(query["uid"][0])
//此处调用模型从数据库中获取数据
user := model.GetUser(uid)
fmt.Println(user)
t, _ := template.ParseFiles("./src/bookWebPro/chapter3/view/t3.html")
fmt.Println(t == nil)
userInfo := []string{user.Name, user.Phone}
t.Execute(w, userInfo)
}
- main函数
import (
"./controller"
"log"
"net/http"
)
func main() {
http.HandleFunc("/getUser", controller.UserController{}.GetUser)
if err := http.ListenAndServe(":8088", nil); err != nil {
log.Fatal(err)
}
}
- 测试
浏览器访问127.0.0.1:8088/getUser?uid=1