点击标题下「异步图书」可快速关注
在进行Web应用开发的时候,使用成熟并且复杂的Web应用框架通常会使开发变得更加迅速和简便,但这也意味着开发者必须接受框架自身的一套约定和模式。虽然很多框架都认为自己提供的约定和模式是最佳实践(best practice),但是如果开发者没有正确地理解这些最佳实践,那么对最佳实践的应用就可能会发展为货物崇拜编程(cargo cult programming):开发者如果不了解这些约定和模式的用法,就可能会在不必要甚至有害的情况下盲目地使用它们。
货物崇拜编程
第二次世界大战期间,盟军为了对战事提供支援,在太平洋的多个岛屿上设立了空军基地,以空投的方式向部队以及支援部队的岛民投送了大量生活用品以及军事设备,从而极大地改善了部队以及岛民的生活,岛民也因此第一次看到了人工生产的衣物、罐头食品以及其他物品。在战争结束之后,这些空军基地便被废弃了,货物空投自然也停止了。此时,岛民做了一件非常符合其本性的事情——他们把自己打扮成空管员、士兵以及水手,使用机场上的指挥棒挥舞着着陆信号,进行地面阅兵演习,试图让飞机继续空投货物,货物崇拜一词也因此而诞生。
尽管货物崇拜程序员并没有像岛民一样挥舞指挥棒,但他们却大量地复制和粘贴从StackOverflow这类网站上找来的代码,这些代码虽然能够运行,但是他们却对这些代码的工作原理一点也不了解。这样做的结果是,他们通常无法扩展和修改这些代码。与此类似,货物崇拜程序员通常会在既不了解框架为什么使用特定的模式或约定,也不知道框架做了何种取舍的情况下,盲目地使用Web框架。
举个例子来说,因为HTTP是一种无连接协议(connection-less protocol),通过这种协议发送给服务器的请求对服务器之前处理过的请求一无所知,所以应用程序才会以cookie的方式在客户端实现数据持久化,并以会话的方式在服务器上实现数据持久化,而不了解这一点的人是很难理解为什么要在不同连接之间使用cookie和会话实现信息持久化的。为了降低使用cookie和会话带来的复杂性,Web应用框架通常都会提供一个统一的接口(uniform interface),用于在连接之间实现持久化。这样做的结果是,很多新手程序员都会想当然地假设在连接之间进行持久化唯一要做的就是使用框架提供的接口。但是由于这类接口通常都是根据框架自身的习惯制定的,因此不同框架提供的接口可能会有所不同。更糟糕的是,不同的框架可能会提供一些名字相同的接口,但是这些同名接口之间的实现却又千差万别、各不相同,因此给开发者带来不必要的困惑。通过这个例子可以看出,使用框架进行Web应用开发意味着将框架与应用进行绑定,之后无论是将应用迁移至另一个框架,还是对应用进行扩展,又或者为应用添加新的特性,都需要对框架本身有深入的了解,在某些情况下可能还需要对框架进行定制。
本文的目的并不是让大家抛弃框架、约定和模式——一个好的框架通常是快速构建可扩展且健壮的Web应用的最好方法,但理解那些隐藏在框架之下的底层概念和基础设施也是非常重要的。只要对框架的实现原理有了正确的认识,我们就可以更加清晰地了解到这些约定和模式是如何形成的,从而避免陷阱、理清思路,不再盲目地使用模式。
对Go语言来说,隐藏在框架之下的通常是net/http
和html/template
这两个标准库,如图1所示,net/http
标准库可以分为客户端和服务器两个部分,库中的结构和函数有些只支持客户端和服务器这两者中的一个,而有些则同时支持客户端和服务器:
Client
、Response
、Header
、Request
和Cookie
对客户端进行支持;
Server
、ServeMux
、Handler/HandleFunc
、ResponseWriter
、Header
、Request
和Cookie
则对服务器进行支持。
本文接下来将会展示如何把net/http
标准库用作服务器以及如何使用Go语言接收客户端发送的HTTP请求。在之后的第4章,我们还会继续使用net/http
标准库,但焦点会放在如何处理请求上面。
在Go Web编程中,我们主要关注的是如何使用net/http
标准库的服务器功能而非客户端功能。
图1 net/http
标准库的各个组成部分
使用Go构建服务器
如图2所示,通过net/http
标准库,我们可以启动一个HTTP服务器,然后让这个服务器接收请求并向请求返回响应。除此之外,net/http
标准库还提供了一个连接多路复用器(multiplexer)的接口以及一个默认的多路复用器。
图2 通过Go服务器处理请求
跟其他编程语言里面的绝大多数标准库不一样,Go提供了一系列用于创建Web服务器的标准库。正如代码清单3-1所示,创建一个服务器的步骤非常简单,只要调用ListenAndServe
并传入网络地址以及负责处理请求的处理器(handler)作为参数就可以了。如果网络地址参数为空字符串,那么服务器默认使用80端口进行网络连接;如果处理器参数为nil
,那么服务器将使用默认的多路复用器DefaultServeMux
。
代码清单3-1 最简单的Web服务器
package main
import (
"net/http"
)
func main() {
http.ListenAndServe("", nil)
}
用户除了可以通过ListenAndServe
的参数对服务器的网络地址和处理器进行配置之外,还可以通过Server
结构对服务器进行更详细的配置,其中包括为请求读取操作设置超时时间、为响应写入操作设置超时时间以及为Server
结构设置错误日志记录器等。
代码清单3-2和代码清单3-1的作用基本上是相同的,它们之间的唯一区别在于代码清单3-2可以通过Server
结构对服务器进行更多的配置。
代码清单3-2 带有附加配置的Web服务器
package main
import (
"net/http"
)
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil,
}
server.ListenAndServe()
}
代码清单3-3展示了Server
结构所有可选的配置选项。
代码清单3-3 Server
结构的配置选项
type Server struct {
Addr string
Handler Handler
ReadTimeout time.Duration
WriteTimeout time.Duration
MaxHeaderBytes int
TLSConfig *tls.Config
TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
ConnState func(net.Conn, ConnState)
ErrorLog *log.Logger
}
通过HTTPS提供服务
当客户端和服务器需要共享密码或者信用卡信息这样的私密信息时,大多数网站都会使用HTTPS对客户端和服务器之间的通信进行加密和保护。在一些情况下,这种保护甚至是强制性的。比如说,如果一个网站提供了信用卡支付功能,那么按照支付卡行业数据安全标准(Payment Card Industry Data Security Standard),这个网站就必须对客户端和服务器之间的通信进行加密。像Gmail和Facebook这样带有隐私性质的网站甚至在整个网站上都启用了HTTPS。如果你打算开发一个网站,而这个网站又需要提供用户登录功能,那么你也需要在这个网站上启用HTTPS。
HTTPS实际上就是将HTTP通信放到SSL之上进行。通过使用ListenAndServeTLS
函数,我们可以让之前展示过的简单Web应用也提供HTTPS服务,代码清单3-4展示了具体的实现代码。
代码清单3-4 通过HTTPS提供服务
package main
import (
"net/http"
)
func main() {
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: nil,
}
server.ListenAndServeTLS("cert.pem", "key.pem")
}
这段代码中的cert.pem
文件是SSL证书,而key.pem
则是服务器的私钥(private key)。在生产环境中使用的SSL证书需要通过VeriSign、Thawte或者Comodo SSL这样的CA取得,但如果是出于测试目的才使用证书和私钥,那么使用自行生成的证书就可以了。生成证书的办法有很多种,其中一种就是使用Go标准库中的crypto
包群(library group)。
SSL、TLS和HTTPS
SSL(Secure Socket Layer,安全套接字层)是一种通过公钥基础设施(Public Key Infrastructure,PKI)为通信双方提供数据加密和身份验证的协议,其中通信的双方通常是客户端和服务器。SSL最初由Netscape公司开发,之后由IETF(Internet Engineering Task Force,互联网工程任务组)接手并将其改名为TLS(Transport Layer Security,传输层安全协议)。HTTPS,即SSL之上的HTTP,实际上就是在SSL/TLS连接的上层进行HTTP通信。
HTTPS需要使用SSL/TLS证书来实现数据加密以及身份验证。SSL证书存储在服务器之上,它是一种使用X.509格式进行格式化的数据,这些数据包含了公钥以及其他一些相关信息。为了保证证书的可靠性,证书一般由证书分发机构(Certificate Authority,CA)签发。服务器在接收到客户端发送的请求之后,会将证书和响应一并返回给客户端,而客户端在确认证书的真实性之后,就会生成一个随机密钥(random key),并使用证书中的公钥对随机密钥进行加密,此次加密产生的对称密钥(symmetric key)就是客户端和服务器在进行通信时,负责对通信实施加密的实际密钥(actual key)。
虽然我们不会在生产环境中使用自行生成的证书和私钥,但了解SSL证书和私钥的生成方法,并学会如何在开发和测试的过程中使用证书和私钥,也是一件非常有意义的事情。代码清单3-5展示了生成SSL证书以及服务器私钥的具体代码。
代码清单3-5 生成个人使用的SSL证书以及服务器私钥
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"time"
)
func main() {
max := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, _ := rand.Int(rand.Reader, max)
subject := pkix.Name{
Organization: []string{"Manning Publications Co."},
OrganizationalUnit: []string{"Books"},
CommonName: "Go Web Programming",
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
pk, _ := rsa.GenerateKey(rand.Reader, 2048)
derBytes, _ := x509.CreateCertificate(rand.Reader, &template,
➥&template, &pk.PublicKey, pk)
certOut, _ := os.Create("cert.pem")
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
keyOut, _ := os.Create("key.pem")
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes:
➥x509.MarshalPKCS1PrivateKey(pk)})
keyOut.Close()
}
生成SSL证书和密钥的步骤并不是特别复杂。因为SSL证书实际上就是一个将扩展密钥用法(extended key usage)设置成了服务器身份验证操作的X.509证书,所以程序在生成证书时使用了crypto/x509
标准库。此外,因为创建证书需要用到私钥,所以程序在使用私钥成功创建证书之后,会将私钥单独保存在一个存放服务器私钥的文件里面。
让我们来仔细分析一下代码清单3-5中的主要代码吧。首先,程序使用一个Certificate
结构来对证书进行配置:
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(365*24*time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
结构中的证书序列号(SerialNumber
)用于记录由CA分发的唯一号码,为了能让我们的Web应用运行起来,程序在这里生成了一个非常长的随机整数来作为证书序列号。之后,程序创建了一个专有名称(distinguished name),并将它设置成了证书的标题(subject)。此外,程序还将证书的有效期设置成了一年,而结构中KeyUsage
字段和ExtKeyUsage
字段的值则表明了这个X.509证书是用于进行服务器身份验证操作的。最后,程序将证书设置成了只能在IP地址127.0.0.1之上运行。
SSL证书
X.509是国际电信联盟电信标准化部门(ITU-T)为公钥基础设施制定的一个标准,这个标准包含了公钥证书的标准格式。
一个X.509证书(简称SSL证书)实际上就是一个经过编码的ASN.1(Abstract Syntax Notation One,抽象语法表示法/1)格式的电子文档。ASN.1既是一个标准,也是一种表示法,它描述了表示电信以及计算机网络数据的规则和结构。
X.509证书可以使用多种格式编码,其中一种编码格式是BER(Basic Encoding Rules,基本编码规则)。BER格式指定了一种自解释并且自定义的格式用于对ASN.1数据结构进行编码,而DER格式则是BER的一个子集。DER只提供了一种编码ASN.1值的方法,这种方法被广泛地应用于密码学当中,尤其是对X.509证书进行加密。
SSL证书可以以多种不同的格式保存,其中一种是PEM(Privacy Enhanced Email,隐私增强邮件)格式,这种格式会对DER格式的X.509证书实施Base64编码,并且这种格式的文件都以
-----BEGIN CERTIFICATE-----
开头,以-----END CERTIFICATE-----
结尾(除了用作文件格式之外,PEM和此处讨论的SSL证书关系并不大)。
在此之后,程序通过调用crypto/rsa
标准库中的GenerateKey
函数生成了一个RSA私钥:
pk, _ := rsa.GenerateKey(rand.Reader, 2048)
程序创建的RSA私钥的结构里面包含了一个能够公开访问的公钥(public key),这个公钥在使用x509.CreateCertificate
函数创建SSL证书的时候就会用到:
derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template,
➥&pk.PublicKey, pk)
CreateCertificate
函数接受Certificate
结构、公钥和私钥等多个参数,创建出一个经过DER编码格式化的字节切片。后续代码的意图也非常简单明了,它们首先使用encoding/pem
标准库将证书编码到cert.pem
文件里面:
certOut, _ := os.Create("cert.pem")
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
然后继续以PEM编码的方式把之前生成的密钥编码并保存到key.pem文件
里面:
keyOut, _ := os.Create("key.pem")
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes:
➥x509.MarshalPKCS1PrivateKey(pk)})
keyOut.Close()
最后需要提醒的是,如果证书是由CA签发的,那么证书文件中将同时包含服务器签名以及CA签名,其中服务器签名在前,CA签名在后。
本文摘自《Go Web编程》
点击封面试读更多内容
无论是经验老到的gopher,还是刚开始接触Go语言的Web开发者,这都是必不可少的一本书,本书囊括了关于Go Web应用的开发和部署的全部知识
本书目录:(滑动手机查看)
第一部分 Go与Web应用
第1章 Go与Web应用 3
1.1 使用Go语言构建Web应用 3
1.1.1 Go与可扩展Web应用 4
1.1.2 Go与模块化Web应用 4
1.1.3 Go与可维护的Web应用 5
1.1.4 Go与高性能Web应用 5
1.2 Web应用的工作原理 6
1.3 HTTP简介 7
1.4 Web应用的诞生 8
1.5 HTTP请求 9
1.5.1 请求方法 10
1.5.2 安全的请求方法 11
1.5.3 幂等的请求方法 11
1.5.4 浏览器对请求方法的支持 11
1.5.5 请求首部 12
1.6 HTTP响应 13
1.6.1 响应状态码 13
1.6.2 响应首部 14
1.7 URI 15
1.8 HTTP/2简介 16
1.9 Web应用的各个组成部分 16
1.9.1 处理器 17
1.9.2 模板引擎 18
1.10 Hello Go 18
1.11 小结 21
第2章 ChitChat论坛 23
2.1 ChitChat简介 23
2.2 应用设计 24
2.3 数据模型 26
2.4 请求的接收与处理 27
2.4.1 多路复用器 27
2.4.2 服务静态文件 29
2.4.3 创建处理器函数 29
2.4.4 使用cookie进行访问控制 30
2.5 使用模板生成HTML响应 33
2.6 安装PostgreSQL 38
2.6.1 在Linux或FreeBSD系统上安装 38
2.6.2 在Mac OS X系统上安装 39
2.6.3 在Windows系统上安装 39
2.7 连接数据库 39
2.8 启动服务器 44
2.9 Web应用运作流程回顾 45
2.10 小结 46
第二部分 Web应用的基本组成部分
第3章 接收请求 49
3.1 Go的net/http标准库 49
3.2 使用Go构建服务器 51
3.2.1 Go Web服务器 51
3.2.2 通过HTTPS提供服务 53
3.3 处理器和处理器函数 56
3.3.1 处理请求 56
3.3.2 使用多个处理器 58
3.3.3 处理器函数 59
3.3.4 串联多个处理器和处理器函数 61
3.3.5 ServeMux和DefaultServeMux 65
3.3.6 使用其他多路复用器 66
3.4 使用HTTP/2 68
3.5 小结 70
第4章 处理请求 72
4.1 请求和响应 72
4.1.1 Request结构 73
4.1.2 请求URL 73
4.1.3 请求首部 74
4.1.4 请求主体 76
4.2 Go与HTML表单 77
4.2.1 Form字段 79
4.2.2 PostForm字段 80
4.2.3 MultipartForm字段 81
4.2.4 文件 83
4.2.5 处理带有JSON主体的POST请求 85
4.3 ResponseWriter 86
4.4 cookie 91
4.4.1 Go与cookie 91
4.4.2 将cookie发送至浏览器 92
4.4.3 从浏览器里面获取cookie 94
4.4.4 使用cookie实现闪现消息 96
4.5 小结 99
第5章 内容展示 100
5.1 模板引擎 100
5.2 Go的模板引擎 102
5.2.1 对模板进行语法分析 104
5.2.2 执行模板 105
5.3 动作 106
5.3.1 条件动作 106
5.3.2 迭代动作 108
5.3.3 设置动作 109
5.3.4 包含动作 111
5.4 参数、变量和管道 113
5.5 函数 114
5.6 上下文感知 116
5.6.1 防御XSS攻击 119
5.6.2 不对HTML进行转义 121
5.7 嵌套模板 122
5.8 通过块动作定义默认模板 126
5.9 小结 127
第6章 存储数据 128
6.1 内存存储 128
6.2 文件存储 131
6.2.1 读取和写入CSV文件 133
6.2.2 gob包 135
6.3 Go与SQL 137
6.3.1 设置数据库 138
6.3.2 连接数据库 140
6.3.3 创建帖子 142
6.3.4 获取帖子 144
6.3.5 更新帖子 145
6.3.6 删除帖子 145
6.3.7 一次获取多篇帖子 146
6.4 Go与SQL的关系 147
6.4.1 设置数据库 147
6.4.2 一对多关系 150
6.5 Go与关系映射器 152
6.5.1 Sqlx 152
6.5.2 Gorm 154
6.6 小结 157
第三部分 实战演练
第7章 Go Web服务 161
7.1 Web服务简介 161
7.2 基于SOAP的Web服务简介 163
7.3 基于REST的Web服务简介 166
7.3.1 将动作转换为资源 168
7.3.2 将动作转换为资源的属性 169
7.4 通过Go分析和创建XML 169
7.4.1 分析XML 169
7.4.2 创建XML 177
7.5 通过Go分析和创建JSON 180
7.5.1 分析JSON 181
7.5.2 创建JSON 184
7.6 创建Go Web服务 187
7.7 小结 194
第8章 应用测试 196
8.1 Go与测试 196
8.2 使用Go进行单元测试 197
8.2.1 跳过测试用例 201
8.2.2 以并行方式运行测试 202
8.2.3 基准测试 203
8.3 使用Go进行HTTP测试 206
8.4 测试替身以及依赖注入 210
8.5 第三方Go测试库 217
8.5.1 Gocheck测试包简介 217
8.5.2 Ginkgo测试框架简介 223
8.6 小结 228
第9章 发挥Go的并发优势 230
9.1 并发与并行的区别 230
9.2 goroutine 232
9.2.1 使用goroutine 232
9.2.2 goroutine与性能 235
9.2.3 等待goroutine 238
9.3 通道 239
9.3.1 通过通道实现同步 240
9.3.2 通过通道实现消息传递 242
9.3.3 有缓冲通道 243
9.3.4 从多个通道中选择 244
9.4 在Web应用中使用并发 247
9.4.1 创建马赛克图片 248
9.4.2 马赛克图片Web应用 251
9.4.3 并发版马赛克图片生成Web应用 254
9.5 小结 262
第10章 Go的部署 263
10.1 将应用部署到独立的 服务器 264
10.2 将应用部署到 Heroku 270
10.3 将应用部署到Google App Engine 273
10.4 将应用部署到 Docker 278
10.4.1 什么是Docker 278
10.4.2 安装Docker 279
10.4.3 Docker的理念与 组件 280
10.4.4 Docker化一个Go Web 应用 281
10.4.5 将Docker容器推送至 互联网 283
10.5 部署方法之间的 对比 286
10.6 小结 287
附录 安装和设置Go 288
延伸推荐
京东质量测试技术论坛:大会技术亮点探秘!
你所不了解的深度神经网络
《Go Web编程》这一本不可错过!
2017优秀图书和作译者评选-进行中赢取kindle等技术图书礼!
AI经典书单| 入门人工智能该读哪些书?
聊聊Python
2017内核大会看点云集
你所不了解的Kafka
Kotlin最佳项目实战——欧瑞天气App
点击关键词阅读更多新书:
Python|机器学习|Kotlin|Java|移动开发|机器人|有奖活动|Web前端|书单
点击图片参与活动
在“异步图书”后台回复“关注”,即可免费获得2000门在线视频课程;推荐朋友关注根据提示获取赠书链接,免费得异步图书一本。赶紧来参加哦!
点击标题下「异步图书」可快速关注
在进行Web应用开发的时候,使用成熟并且复杂的Web应用框架通常会使开发变得更加迅速和简便,但这也意味着开发者必须接受框架自身的一套约定和模式。虽然很多框架都认为自己提供的约定和模式是最佳实践(best practice),但是如果开发者没有正确地理解这些最佳实践,那么对最佳实践的应用就可能会发展为货物崇拜编程(cargo cult programming):开发者如果不了解这些约定和模式的用法,就可能会在不必要甚至有害的情况下盲目地使用它们。
货物崇拜编程
第二次世界大战期间,盟军为了对战事提供支援,在太平洋的多个岛屿上设立了空军基地,以空投的方式向部队以及支援部队的岛民投送了大量生活用品以及军事设备,从而极大地改善了部队以及岛民的生活,岛民也因此第一次看到了人工生产的衣物、罐头食品以及其他物品。在战争结束之后,这些空军基地便被废弃了,货物空投自然也停止了。此时,岛民做了一件非常符合其本性的事情——他们把自己打扮成空管员、士兵以及水手,使用机场上的指挥棒挥舞着着陆信号,进行地面阅兵演习,试图让飞机继续空投货物,货物崇拜一词也因此而诞生。
尽管货物崇拜程序员并没有像岛民一样挥舞指挥棒,但他们却大量地复制和粘贴从StackOverflow这类网站上找来的代码,这些代码虽然能够运行,但是他们却对这些代码的工作原理一点也不了解。这样做的结果是,他们通常无法扩展和修改这些代码。与此类似,货物崇拜程序员通常会在既不了解框架为什么使用特定的模式或约定,也不知道框架做了何种取舍的情况下,盲目地使用Web框架。
举个例子来说,因为HTTP是一种无连接协议(connection-less protocol),通过这种协议发送给服务器的请求对服务器之前处理过的请求一无所知,所以应用程序才会以cookie的方式在客户端实现数据持