《Go Web 编程》读书笔记

GO Web 编程

Go 语言介绍

第一章:Go与web应用

HTTP

  • HTTP 是一种无状态、由文本构成的请求-响应(request-response)协议,这种协议使用的是客户端-服务器(client-server)计算模型
  • CGI 通用网管接口(Common Gateway Interface),允许web服务器与一个独立运行于web服务器进程之外的进程对接
  • SSI(server-side includes)服务器端,允许开发者在HTML文件里包含一些指令,衍生出了JSP(Java Server Pages),ASP(Active Server Pages)等Web模板引擎

  • http请求
    • 请求行(request-line)
    • 零个或任意多个请求首部(header)
    • 一个空行
    • 可选的报文主体(body)
  • 请求方法
    • GET
    • POST
    • HEAD
    • PUT
    • DELETE
    • TRACE
    • OPTIONS
    • CONNECT
    • PATCH

  • http响应
    • 一个状态行
    • 零个或任意数量的响应首部
    • 一个空行
    • 一个可选的报文主体
  • 响应状态码
    • 1XX 情报状态码
    • 2XX 成功状态码
    • 3XX 重定向状态码
    • 4XX 客户端错误状态码
    • 5XX 服务器错误状态码
  • 响应首部
    • Allow
    • Content-Length
    • Content-Type
    • Date
    • Location
    • Server
    • Set-Cookie
    • WWW-Authenticate

URI

  • URI一般格式为:<方案名称>:<分层部分>[ ? <查询参数>] [ # <片段>]
  • 因每个URL都是一个单独的字符串,所以URL里不能包含空格,?和#这些符号也不能做其它用途,我们需要用URL编码(百分号编码)对这些字符进行转换,做法是**将该字符在ASCII码中的字节值转换为16进制,并在前面加上%**,例如空格就被转换为%20。

处理器

Web应用中的处理器出了要接收和处理客户端发来的请求,还需要调用模板引擎,然后由模板引擎生成HTML并把数据填充至将要回传给客户端的响应报文中

模板引擎(template engine)

  • 静态模板
  • 动态模板

第二章:ChitChat论坛

  • 请求的接收和处理是所有 Web 应用的核心。
  • 多路复用器会将 HTTP 请求重定向到正确的处理器进行处理,针对静态文件的请求也是如此。
  • 处理器函数是一种接受 ResponseWriter 和 Request 指针作为参数的 Go 函数。
  • cookie 可以用作一种访问控制机制。
  • 对模板文件以及数据进行语法分析会产生相应的 HTML, 这些 HTML 会被用作返回给浏览器的响应数据。
  • 通过使用 sql 包以及相应的 SQL 语句,用户可以将数据持久地存储在关系数据库中。

第三章:接收请求

net/http标准库

net/http 标准库通常包括两个部分,客户端和服务器,我们可以通过ListenAndServe创建一个简陋的服务器

1
2
3
4
5
6
7
8
9
package main

import (
"net/http"
)

func main() {
http.ListenAndServe("", nil)
}

这会使用默认的80端口进行网络连接,并且使用默认的多路复用器DefaultServeMux,我们也可以通过Server结构进行更详细的配置

1
2
3
4
5
6
7
func main() {
server := http.Server {
Addr: "127.0.0.1:80",
Handler: nil,
}
server.ListenAndServe()
}

处理器和处理函数

处理器

前面的代码会返回404响应,因为我们还没有为请求编写相应的处理器。一个处理器就是一个拥有ServeHTTP方法的接口

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

我们通过实现这个接口来编写处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type MyHandler struct{}

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

func main() {
handler := MyHandler{}
server := http.Server {
Addr: "127.0.0.1:80",
Handler: &handler,
}
server.ListenAndServe()
}

我们可以设置多个处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type HelloHandler struct{}

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

type WorldHandler struct{}

func (world *WorldHandler) ServeHTTP(w http.ResponseWriter, r *http.Reuest) {
fmt.Fprintf(w, "world!")
}

func main() {
hello := HelloHandler{}
world := WorldHandler{}

server := http.Server {
Addr: "127.0.0.1:80",
}
http.Handle("/hello", &hello)
http.Handle("/world", &world)

server.ListenAndServe()
}

我们看一下Handle函数再源码中的定义

1
2
3
func Handle(pattern string, handler Handler) { 
DefaultServeMux.Handle(pattern, handler)
}

实际上是在调用DefaultServeMux的某个方法,前面我们已经提到过了DefaultServeMux是个默认多路复用器,实际上它也是个Handler处理器,因为他是ServeMux结构的一个实例,而ServeMux也实现了Handler接口的ServeHTTP方法。这样就可以对不同的请求做出不同的响应。

处理器函数

处理器函数是与处理器拥有同样行为的函数,它们与ServeHTTP拥有同样的函数签名。

1
2
3
4
5
6
7
8
9
10
11
12
func hello (w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello!")
}

func main() {
server := http.Server {
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/hello", hello)

server.ListenAndServe()
}

HandleFunc是Go语言拥有的一种函数类型,它可以把一个带有正确签名的f转换为带有方法f的handler。

来看看HandleFunc的源码

1
2
3
4
5
6
7
8
9
10
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}

Handle函数是不是似曾相识!

串联多个处理器和处理器函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func hello(w *http.ResponseWriter, r *Request) {
fmt.Fprintf(w, "Hello!")
}

func log(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Log!")
h(w, r)
}
}

func main() {
server := http.Server {
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/hello", log(hello))
server.ListenAndServe()
}
其中HandlerFunc是实现了Handler接口的函数类型,源码定义:

type HandlerFunc func(ResponseWriter, *Request)

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

ServeMux

ServeMux是一个HTTP请求多路复用器,负责接收HTTP请求并根据请求中的URL将请求重定向到正确的处理器。ServeMux包含一个映射,这个映射会将URL映射至相应的处理器。值得一提的是,ServeMux无法使用变量实现URL模式匹配,因此必要时我们完全可以用其它自建的多路复用器来代替ServeMux,如HttpRouter等高效轻量的第三方复用器

第四章:处理请求

请求和响应

Request结构

  • URL字段
  • Header字段
  • Body字段
  • Form, PostForm, MultipartForm字段

在处理器函数中,我们可以通过Request获取各个字段的详细信息

1
2
3
4
5
6
7
8
9
func get_request_value(w *http.ResponseWriter, r *http.Request) {
h := r.Header
brower := r.Header.Get("User-Agent")
fme.Fprintln(w, h)

body := make([]byte, r.ContentLength)
r.Body.Read(body)
fmt.Fprintln(w, string(body))
}

Go与HTML表单

用户在表单中输入的数据会以键值对的形式记录在请求的主体中,其中表单中的enctype属性决定了以何种形式发送键值对。默认属性值为application/x-www-form-urlencoded,这个属性会把表单中的数据编码一个连续的长查询字符串,另一种编码方式为multipart/form-data,表单中的数据会被转换为一条MIME报文,每个键值对都构成了这个报文的一部分。简单来说,当表单只需要传送简单数据时,默认编码更加简单,高效;而当表单需要传输大量数据(如文件)时,使用后一种编码方式会更好。 有些时候,用户可以通过Base64编码,以文本方式传送二进制数据。

使用Request结构获取表单数据的一般步骤是:

  1. 调用ParseForm或者ParseMultipartForm方法进行语法分析
  2. 访问Form, PostForm, MultipartForm等字段获取数据
1
2
3
4
5
6
func get_form_data1(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.Form)
// PostForm字段只支持默认编码,并且只返回表单值,不返回URL查询值
fmt.Fprintln(w, r.PostForm)
}

当使用multipart/form-data编码时,表单数据会被存储到MultipartForm字段中

1
2
3
4
func get_form_data2(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.MultipartForm)
}

我们也可以使用FormValue或者PostFormValue快速获取表单值,也两个方法会自动调用ParseForm或者ParseMultipartForm方法,其中PostFormValue只会返回表单键值对而不会返回URL键值对

使用FormFile方法可以快速的获取被上传的文件

1
2
3
4
5
6
7
8
9
func process(w http.ResponseWriter, r *http.Request) {
file, _, err := r.FormFile("upload")
if err == nil {
data, err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(w, string(data))
}
}
}

ResponseWriter

  • Write 接收一个字节数组,并写入到HTTP响应主体中
  • WriteHeader 改变HTTP响应状态码
  • Header 修改HTTP响应首部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
json2 "encoding/json"
"fmt"
"net/http"
)

type Post struct {
User string
Threads []string
}

func writeExample(w http.ResponseWriter, r *http.Request) {
str := `<html>
<head><title>Go</title></head>
<body><h1>Hello world</h1></body>
</html>`
w.Write([]byte(str))
}

func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(501)
fmt.Fprintln(w, "No such service, try next door")
}

func headerExample(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "http://baidu.com")
w.WriteHeader(302)
}

func jsonExample(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
post := &Post{
User: "authetic",
Threads: []string{"first", "second"},
}
json, _ := json2.Marshal(post)
w.Write(json)
}

func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
}
http.HandleFunc("/write", writeExample)
http.HandleFunc("/writeheader", writeHeaderExample)
http.HandleFunc("/redirect", headerExample)
http.HandleFunc("/json", jsonExample)
server.ListenAndServe()
}

Go与cookie

响应头部没有设置Expires字段的通常被成为会话cookie,浏览器关闭或刷新cookie就会消失,设置了Expires字段的通常被称为持久cookie,在过期时间之前会一直存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func set_cookie(w http.ResponseWriter, r *http.Request) {
c1 := http.Cookie{
Name: "first_cookie",
Value: "Go Web",
HttpOnly: true,
}
c2 := http.Cookie{
Name: "second_cookie",
Value: "Go Web",
HttpOnly: true,
}

http.SetCookie(w, &c1)
http.SetCookie(w, &c2)
}

func get_cookie(w http.ResponseWriter, r *http.Request) {
// h := r.Header["Cookie"]
cl, err := r.Cookie("first_cookie")
if err != nil {
fmt.Fprintln(w, "Something wrong")
}
cs := r.Cookies()
fmt.Fprintln(w, cl)
fmt.Fprintln(w, cs)
}

cookie实现闪现消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func setMessage(w http.ResponseWriter, r *http.Request) {
msg := []byte("Hello Go")
c := http.Cookie{
Name: "flash",
Value: base64.URLEncoding.EncodeToString(msg),
}
http.SetCookie(w, &c)
}

func showMessage(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("flash")
if err != nil {
if err == http.ErrNoCookie {
fmt.Fprintln(w, "No message found")
}
} else {
rc := http.Cookie{
Name: "flash",
MaxAge: -1,
Expires: time.Unix(1, 0),
}
http.SetCookie(w, &rc)
val, _ := base64.URLEncoding.DecodeString(c.Value)
fmt.Fprintln(w, string(val))
}
}

第五章:内容展示

  • 在 Web 应用中,模板引擎会把模板和数据进行合并,生成将要返回给客户端的 HTML。
  • Go 的标准模板引擎定义在 html/template 包当中。
  • Go 模板引擎的工作方式就是对一个模板进行语法分析,接着在执行这个模板的时候,将一个ResponseWriter 以及一些数据传递给它。被调用的模板引擎会对传入的已分析模板以及数据进行合并,然后把合并的结果传递给 ResponseWriter。
  • Go 的模板拥有一系列丰富多样并且威力强大的动作,这些动作就是一系列命令,它们可以告诉模板应该以何种方式与数据合并。
  • 除了动作之外,模板还可以包含参数、管道和变量:其中参数用于表示模板中的数据值,管道用于串联起多个参数和函数,至于变量则会作为动作的组件而存在。
  • Go 拥有一系列受限的模板函数。此外,通过创建一个函数映射并将它与模板进行绑定,用户也可以创建出自己的模板函数。
  • Go 的模板引擎可以根据数据所在的位置改变数据的显示方式,这种上下文感知特性能够有效地防御 XSS 攻击。
  • 人们在设计一个拥有一致外观和使用感受的 Web 应用时,常常会用到 Web 布局,Go可以使用嵌套模板来实现 Web 布局。

第六章:存储数据

  • 通过使用结构将数据存储在内存里面,以此来构建数据缓存机制并提高响应速度。
  • 通过使用 CSV 或者 gob 二进制格式将数据存储在文件里面,可以对用户提交的文件进行处理,或者为缓存数据提供备份。
  • 通过使用 database/sql 包,可以对关系数据库执行 CRUD 操作,并在不同的数据之间建立起相应的关系。
  • 通过 Sqlx 和 Gorm 这样的第三方数据访问库,可以使用威力更强大的工具去操纵数据库中的数据。

第七章:Go Web 服务

  • 编写 Web 服务是 Go 语言目前非常常见的用途之一,了解如何构建 Web 服务是一项非常有价值的技能。
  • Web 服务主要分为两种类型——一种是基于 SOAP 的 Web 服务,而另一种则是基于 REST 的 Web 服务
    • SOAP 是一种协议,它能够对定义在 XML 中的结构化数据进行交换。但是,因为 SOAP 的 WSDL 报文有可能会变得非常复杂,所以基于 SOAP 的 Web 服务没有基于 REST 的 Web 服务那么流行。
    • 基于 REST 的 Web 服务通过 HTTP 协议向外界公开自己拥有的资源,并允许外界通过 HTTP 协议对这些资源执行指定的动作。
  • 创建和分析 XML 以及 JSON 的步骤都是相似的,用户要么根据指定的结构去生成 XML 或者JSON,要么从指定的结构里面提取数据到 XML 或者 JSON 里面,前一种操作称为封装,而后一种操作则称为解封。

第八章:应用测试

  • Go 通过 go test 命令为用户提供了内置的测试工具,并提供了 testing 包以便实现单元测试。
  • testing 包提供了基本的功能测试以及基准测试能力。
  • 对于 Go 语言来说,Web 应用的单元测试可以通过 testing/httptest 包来完成。
  • 使用测试替身可以让测试用例变得更加独立
  • 实现测试替身的一种方法是使用依赖注入设计模式
  • Go 语言拥有许多第三方测试库,其中包括对 Go 的测试功能进行扩展的 Gocheck 包,以及实现了行为驱动测试的 Ginkgo 包。

第九章:发挥 Go 的并发优势

  • Go web 服务器本身是并发的,服务器会把接收到的每条请求都放到独立的 goroutine 里运行。
  • 并发和并行是两个相辅相成的概念,但它们并不相同。并发指的是两个或多个任务在同一时间段内启动、运行和结束,并且这些任务可能会彼此互动,而并行则是单纯地同时运行多个任务。
  • Go 通过 goroutine 和通道这两个重要的特性直接支持并发,但 Go 并不直接支特并行。
  • goroutine 用于编写并发程序,而通道则用于为不同的 goroutine 之间提供通信功能。
  • 无缓冲通道都是同步的,尝试向一个已经包含数据的无缓冲通道推入新的数据将被阻塞;但是,有缓冲通道在被填满之前都是异步的。
  • select 语句可以以先到先服务的方式,从多个通道里选出一个已经准备好执行接收操作的通道
  • WaitGroup 同样可以用于对多个通道进行同步。
  • 并发程序的性能一般都会比相应的非并发程序要高,而具体提升多少则取决于所使用的算法(即使在只使用一个 CPU 的情况下,也是如此)。
  • 在条件允许的情况下,并发的 Web 应用将自动地获得并行带来的优势。

第十章:Go 的部署

  • 部署 Go Web 服务最简单的方法就是直接将二进制可执行文件放置到服务器里面(这个服务器可以是虚拟机,也可以是实际存在的服务器),然后通过配置 Upstart 来保证服务可以随系统启动并持续地运行下去。
  • Docker 是一种最近开始崭露头角并且威力强大的 Web 服务和 Web 应用部署方式。用户首先需要将被部署的 Go Web服务 Docker 化为容器,然后才能在本地 Docker 宿主或者云端的远程 Docker 宿主上部署这个容器。