本文涵盖的内容来自于一本<<go web编程>>的书,作者:郑兆雄。
go编写http服务端
net/http 库
标准库可以分为客户端和服务器两部分:
- 支持客户端的Client、Request、Header、Response、Cookie
- 支持服务器的Server、ServeMux、Handler/HandleFunc、ResponseWriter、Header、Request和Cookie
构建web服务器的方法
- 传入空的网络地址,和空的处理器。
package main
import (
"net/http"
)
func main() {
// 将会使用80当作web服务器的监听端口,使用http.DefaultServeMux为默认的处理器
http.ListenAndServe("", nil)
}
- 对服务器配置的结构体http.Server{}
package main
import (
"crypto/tls"
"log"
"net/http"
)
func main() {
// 创建一个web服务器对象,通过server结构体对服务器对象进行相关配置
server := http.Server{
Addr: "", // 服务的监听地址和端口信息
Handler: nil, // 处理客户端请求的处理器
TLSConfig: &tls.Config{}, // https相关配置
ReadTimeout: 0, // 读取客户端请求的超时时间
ReadHeaderTimeout: 0, // 读取客户端请求头超时时间
WriteTimeout: 0, // 写入客户端响应超时时间
IdleTimeout: 0, // http连接超时时间
MaxHeaderBytes: 0, // 头部最大字节
ErrorLog: &log.Logger{}, // 日志对象配置
}
server.ListenAndServe()
}
- 构建https服务器
package main
import (
"net/http"
)
func main() {
server := http.Server{
Addr: "",
Handler: nil,
}
// cert.pem 证书, key.pem 私钥
server.ListenAndServeTLS("cert.pem", "key.pem")
}
- 创建证书和私钥(适合内网测试环境,少用可大概了解创建流程和x509标准,证书编码方式相关内容)
package main
import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"os"
"time"
"crypto/rsa"
)
func main() {
max := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, _ := rand.Int(rand.Reader, max) // 生成的唯一序列号长串
subject := pkix.Name{ // 证书的标题信息
Organization: []string{"cert organization"},
OrganizationalUnit: []string{"cert ou"},
CommonName: "testCert",
}
// x509是国际电信连门电信标准化部门为公钥制定的一个标准,包含了公钥证书的标准格式
// x.509证书是一个经过编码的ASN.1格式的电子文档
// 证书的编码有BER(Baic Encoding Rules)和DER方式编码(DER主要用于密码学,尤其是对x.509证书进行加密)
template := &x509.Certificate{
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365), // 证书的有效期为1年
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, // 证书的用途,服务器身份验证
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, // 证书只能工作在127.0.0.1IP上
}
// 生成密钥对
pk, _ := rsa.GenerateKey(rand.Reader, 2048)
// 创建证书
derBytes, _ := x509.CreateCertificate(rand.Reader, template, template, pk.PublicKey, pk)
certOut, _ := os.Create("cert.pem")
// 对证书进行DER编码,并写入到"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()
}
处理http请求的处理器和处理器函数
正常的http请你去流程为client -> http请求 -> server -> serveMux -> handler/handleFunc -> response to client。
- 处理器和处理器函数都处理客户端不同url的的请求,将处理的结果响应给客户端。
处理器
- 在net/http库中,处理器是实现了handler接口的对象。
- handler接口只有一个方法为ServeHttp(w http.ResponseWriter, r *http.Request)。
- 定义处理器的示例:
package main
import (
"fmt"
"net/http"
)
// 定义一个对象
type Hello struct {
}
// 该对象实现了handler接口中的ServHTTP方法
func (h *Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello, %s", r.Host)
}
func main() {
// 实例化一个对象
handler := &Hello{}
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: handler, // 使用实例化的对象代替http.DefaultServeMux默认的多路选择器
}
server.ListenAndServe()
}
// 这里所有的url请求都会走handler这个处理器,肯定不能满足业务需求的。
// 但是我们可以多定义几个对象,然后使用http.Handle("url string",handler)方法将它加入到我们的服务器中。
处理器函数
- 处理器函数则是func(w http.ResponseWriter, r *http.Request)类型的函数即可。
- http.handlerFunc类型同http.DefaultServeMux都实现了handler接口,都可以当作一个handler的实例。
串联多个处理器
- 处理器传入处理器函数中,返回一个http.Handler,处理器执行的时机由处理器函数中决定。意在为多个处理器加上相同的处理器函数(比如日志记录,安全检查等我们需要的函数)。
- 处理器串联示例代码
package main
import (
"fmt"
"net/http"
)
type Hello struct {
}
func (h *Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello, %s", r.Host)
}
func log(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// log 函数执行代码
fmt.Println("收到客户端的请求", "客户端的IP地址为", r.Host)
// handler处理器的执行时机
h.ServeHTTP(w, r)
})
}
func main() {
handler := &Hello{}
server := http.Server{
Addr: "127.0.0.1:8080",
Handler: log(handler), // 使用串联的处理器
}
server.ListenAndServe()
}
http.ServeMux和http.DefaultServeMux
- 都是net/http包中提供的多路选择器
- DefaultServeMux是ServeMux的一个实例
- ServeMux可以将多个路由和handler绑定起来(一个路由和处理器的映射表),当客户端相应的路由时后,再将请求转发到相应的处理器上。
对于路由/1/2的请求,多路复用器只绑定了/1的路由为/HANDLER1处理器和/路由为/HANDLER,/1/2的请求路由将会被导向/HANDLER来处理,而如果/1/绑定为/HANDLER1那么相同的请求会被HANDLER1来处理。这点是在使用ServeMux需要特别注意的一点
- 标准库中多路复用器的弊端
- 解析url中的数据,需要自行添加语法分析相关代码
- 推荐三方多路复用器github.com/julienschmidt/httprouter(多路复用器实现http.ServeHTTP方法+对url做语法分析)
- 上面多路复用器在处理器函数中添加了httprouter.Params参数,可以从中获取url获取具名参数,比如/url/:name,name就是一个具名参数。
http请求和响应
- 报文格式
- 请求/响应行
- 请求/响应头部
- 一个或多个空行
- 请求/响应主体
html表单
- 一般post请求由html表单发送,html中get方法也是可以提交表单的。
- 提交表单可以用的applicatioon/x-www-form-urlencoded这种编码方式,来提交少量数据,而multipart/form-data则多用于大资源的提交。其次还有h5常用的text/plain编码方式。
go http Request结构组成
- URL 字段
- Scheme、Opaque、User、Host、Path、RawQuery(查询参数)、Fragment(分段,#后面的字符串,浏览器输入url的分段会被浏览器去除)
- Header字段
- 一个或者多个键值对,key为string的切片
- header有提供添加(追加key)、删除、设置(将key切片设置为空,然后再向key第一个索引位置设置key)、获取这四个方法;
- 获取header
r.Header["Accept-Encoding"]
或者使用get方法h := r.Header.Get("Accept-Encoding")
;第一个是字符串切片,第二个是字符串
- Body字段
- 请求和响应都包含这个字段。
- 这个字段是一个io.ReadCloser接口,所以body字段包含了read和close方法,read接受一个字节切片,执行read方法会返回一个读取的字节数和一个可选的错误。
- Form字段、PostForm、MultipartForm字段
- ParseForm和ParseMultipartForm可以对表单和url中url.RawQuery进行语法分析(一个对表单数据解码的过程,如url提交的查询参数或表单提交的查询参数中包含%20将解析为空格)
- MultipartForm用来获取form-data编码的表单,而Form(url+body)和PostForm(body)用来获取url编码表单数据。
- FormValue或者PostFormValue可以自动调用parseForm或者ParseMultipartForm方法。
- 文件上传
- 服务器获取文件上传的两种方式如下代码所示:
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func upload(w http.ResponseWriter, r *http.Request) {
// file, _, err := r.FormFile("upload") // 和下面三行功能类似,但r.FormFile()只能获取第一个文件
r.ParseMultipartForm(1024)
fileHeader := r.MultipartForm.File["upload"][0]
file, err := fileHeader.Open()
if err != nil {
fmt.Println(err)
return
}
data, _ := ioutil.ReadAll(file)
fmt.Fprintln(w, string(data))
}
func main() {
http.HandleFunc("/", upload)
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("lintenAndServe error:", err)
}
}
# curl 模拟上传文本文件
echo upload text > file.txt
curl -F 'upload=@./file.txt' 127.0.0.1:8080/
upload text
- 处理json格式的请求:
- 客户端json使用的编码格式只能使application/x-www-form-urlencoded,否则无法使用parseForm方法获取数据。
ResponseWriter接口
- 如果响应内容没有设置响应主体的类型,将使用主体的前512字节推断响应主体类型并返回给客户端。
- 实现ResponseWriter接口的对象为reponse非导出结构体的指针。
- ReponseWriter有三个方法:Write(向响应主体写入内容)、WriteHeader(写入响应码)、Header(添加或修改响应头)。
- 编写响应头实现重定向示例代码:
package main
import (
"fmt"
"net/http"
)
func redirect(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Location", "http://www.baidu.com")
w.WriteHeader(http.StatusFound)
fmt.Fprintln(w, "redirect to baidu")
}
func main() {
http.HandleFunc("/redirect", redirect)
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("lintenAndServe error:", err)
}
}
# curl -i 127.0.0.1:8080/redirect
HTTP/1.1 302 Found
Location: http://www.baidu.com
Date: Sat, 14 Aug 2021 02:26:02 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8
cookie
cookie 大致可以分为会话cookie和持久化cookie。
- 会话
- 不设置过期时间。浏览器关闭自动移除。
- 持久化
- 设置过期时间。expires字段和MaxAge字段都可以指定过期时间,MaxAge设置的是多少秒后过期。
- 设置cookie的方法:
- http.SetCookie()
- w.Header.Add("Set-Cookie",http.Cookie{})
- 服务端获取客户端发送的Cookie
- r.Header["Cookie"] // 获取cookie字典
- r.Cookie("CookieName") // 通过cookie名获取单独的某一个cookie
- 实例:用cookie实现闪现消息,当有某一cookie时,显示cookie的内容,并将cookie设置成过期cookie:
package main
import (
"encoding/base64"
"fmt"
"net/http"
"time"
)
func setCookie(w http.ResponseWriter, r *http.Request) {
c := http.Cookie{
Name: "flash",
Value: base64.URLEncoding.EncodeToString([]byte("hello cookie")),
HttpOnly: true,
}
w.Header().Set("Set-Cookie", c.String())
// http.SetCookie(w, &c)
}
func showCookie(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("flash")
if err != nil {
fmt.Fprintln(w, "no found cookie")
} else {
rc := http.Cookie{
Name: "flash",
Value: c.Value,
Expires: time.Unix(1, 0),
MaxAge: -1,
HttpOnly: false,
}
http.SetCookie(w, &rc)
// c.Vaule 是经过编码后的,要获取字符串需要使用UrlEncoding.DecodeString来解码
val, _ := base64.URLEncoding.DecodeString(c.Value)
fmt.Fprint(w, string(val))
}
}
func main() {
http.HandleFunc("/set_cookie", setCookie)
http.HandleFunc("/show_cookie", showCookie)
if err := http.ListenAndServe("0.0.0.0:8080", nil); err != nil {
fmt.Println("lintenAndServe error:", err)
}
}
模版引擎语法
- 执行过程
- 处理器调用模版引擎,传入一个或多个模版文件,对模版文件进行语法分析后生成语法分析后的模版;
- 然后再传入模版需要的动态数据,模版在接收到参数后生成相应的html,并将这些html写入到ResponseWriter返回给客户端。
- tips: 向parseFiles传入多个文件,会创建跟模板文件名同名的模板;传入的是多个模板文件,则会返回一个模板集合。
/* 创建一个或多个模板 */
// 方式1:创建模版引擎,并解析模版文件,隐式声明模板名称
t := template.ParseFiles("tmpl.html")
// 方式2:创建模版引擎,显式声明模板名称
t := template.New("tmpl.html)
// 方式2:解析模版文件
t ,_ := t.ParseFiles("tmpl.html")
// 方式3:匹配模版文件(如下匹配以.html结尾的模版文件,返回一个模板集合)
t ,_ := template.ParseGlob("*.html")
/* 处理模版报错 */
// Must捕获解析模版的报错直接抛出panic错误
t := template.Must(template.ParseFiles("tmpl.html))
/* 执行模板 */
# 1.调用模板的Execute方法,传入ResponseWriter和模板的数据
# 2.调用模板ExecuteTemplate方法,对模版集合中指定模板执行
- 模版组成:
- 动作 :{{}}包裹,或者自定义的定界符。
- 参数、变量、管道、函数
- 文本
/* 动作 */
// 1. 条件动作
{{ if arg }}
some Content
{{ end }}
// arg:任意单个值
// ------
{{ if arg }}
some Content
{{else}}
other Content
{{ end }}
// 例子
package main
import (
"html/template"
"math/rand"
"net/http"
)
var html = `
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>web programming</title>
</head>
<body>
{{ if . }}
Num is greater than 5!
{{ else }}
Num is 5 or less!
{{ end }}
</body>
</html>
`
func handlerFunc(w http.ResponseWriter, r *http.Request) {
t := template.New("tmpl.html")
t, _ = t.Parse(html)
t.Execute(w, rand.Intn(10) > 5)
}
func main() {
http.HandleFunc("/", handlerFunc)
http.ListenAndServe(":8080", nil)
}
// 2. 迭代动作
{{ range array }}
Loop body {{ . }}
{{ end }}
// -----如果array为空,执行else代码块
{{ range array }}
Loop body {{ . }}
{{ else }}
nothing to show
{{ end }}
// 3. 设置动作:在指定范围内为.设置新值(设置动作块外的值还是处理器传入的值)
{{ arg1 }}
{{ with arg2 }}
arg1 set arg2
{{ end }}
// -----如果arg2为空,则执行else块代码
{{ arg1 }}
{{ with arg2 }}
arg1 set arg2
{{ else }}
dot set
{{ end }}
// 4. 包含动作:一个模板嵌套另一个模板
{{ template "name" }}
// ------向嵌套模板传递数据
{{ template "name" arg }}
// 5. 显式定义模板
特点1:同一个模板文件内可以定义多个模板,调用ExecuteTemplate()执行指定的模板,不同的页面有了公共的布局
特点2:不同的文件可以定义同名的模板,template.ParseFiles()时区分传入即可调用不同的模板
{{ define "Name" }} // 定义一个名字为Name的模板
{{ end }}
// 6. 动作块
应用场景:当模板中引用了其他模板,但处理器在解析模板时没有传入定义的模板文件,动作块可充当默认的模板(防止执行模板报错)
{{ block arg }}
arg
{{ end }}
// 1. 参数
模板中的一个值,可以是布尔、整数、字符串、结构体、数组、变量、方法(返回值最多两个:1个值+1个错误)、函数等
// 2. 变量
// ------动作中设置变量
$var := Value
// ------变量+迭代
{{ range $key,$value := . }}
key is $key, value is $value.
{{ end }}
// 3. 管道
和unix中的管道类似,如下:
{{ 1.11143 | printf "%.2f" }}
// 4. 函数(入参可以任意个,出参最多2个:一个值,一个错误)
// ------模板内部函数
fmt.Sprint ...
// ------自定义函数
步骤1:创建映射关系:template.FuncMap{}映射关系,key为函数名(模板文件中调用它,大写开头),value为最多两个返回值、任意参数的函数
步骤2:创建模板调用.Funcs方法把函数和模板绑定
示例代码:
package main
import (
"fmt"
"html/template"
"net/http"
"time"
)
var html = `
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>web programming</title>
</head>
<body>
now Time: {{ Pdate }}
</body>
</html>
`
func printDate() string {
return time.Now().Format("2006-01-02 15:04:05")
}
func handlerFunc(w http.ResponseWriter, r *http.Request) {
var err error
funcMap := template.FuncMap{"Pdate": printDate}
t := template.New("html").Funcs(funcMap)
t, err = t.Parse(html)
if err != nil {
fmt.Print("parse error:", err)
return
}
t.Execute(w, "")
}
func main() {
http.HandleFunc("/", handlerFunc)
http.ListenAndServe(":8080", nil)
}
- 模版引擎库
- text/template:通用的模版引擎。
- html/template:专门为html格式设定的模版引擎库。
- 模板特性
- 上下感知:模板对传入的数据,根据模板定义显示的类型进行转义
- 上下文感知可以防御一定的xss夸站点攻击,但不是信条,也可以调用template.HTML()方法不对传入数据转义
// 跨站点攻击模拟
package main
import (
"fmt"
"html/template"
"net/http"
)
var html = `
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>web programming</title>
</head>
<body>
{{ if . }}
{{ . }}
{{ else }}
null
{{ end }}
</body>
</html>
`
var form = `
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>web programming</title>
</head>
<body>
<form action="/form" method="post">
Comment: <input name="comment" type="text">
<hr/>
<button id="submit">Submit</button>
</body>
</html>
`
var formData string
func fromHandler(w http.ResponseWriter, r *http.Request) {
var err error
t := template.New("from")
t, err = t.Parse(form)
if err != nil {
fmt.Print("parse error:", err)
return
}
formData = r.FormValue("comment")
t.Execute(w, nil)
}
func printHandler(w http.ResponseWriter, r *http.Request) {
t := template.New("html")
t.Parse(html)
// 设置头,关闭浏览器夸站点防御
w.Header().Set("X-XSS-Protection", "0")
// 不对显示内容进行转义
t.Execute(w, template.HTML(formData))
}
func main() {
// 访问/form用户输入模拟攻击代码:<script>alert('Pwnd!');</script>
http.HandleFunc("/form", fromHandler)
// 显示攻击效果
http.HandleFunc("/print", printHandler)
http.ListenAndServe(":8080", nil)
}
数据存储
存放于内存
将数据存放在:数组、切片、map、栈、队列、树等数据结构
优势:性能好、速度快
缺点:非持久化、程序重启数据丢失
文件存储
- 原样存储
io/iotuil包
os包
- CSV格式存储(comma-separated value,逗号分隔值文本格式): 处理表格数据
相关的包:encoding/csv
应用场景:用户提供大量数据、无法让用户把数据填入提供的表单
用法:用户使用表格填入所有数据导出为CSV格式文件,并上传CSV文件,程序根据需要解析CSV文件获取数据;也可以是程序将数据打包成CSV文件发送给用户
csv 部分方法示例:
// 创建csv文件
csvFile,_ := os.Create("test.csv")
// 创建一个writer
writer := csv.NewWriter(csvFile)
// 写入csv内容
writer.Write([]string{})
// 缓冲内容写入到磁盘
writer.Flush()
// 打开csv文件
file ,_ := os.Open("test.csv")
// 创建一个reader
reader := csv.NewReader(file)
// 设置字段数量,-1表示无字段不报错
reader.FieldsPerRecord = -1
// 读取所有到d
d,_ := reader.ReadAll()
- gob包
encoding/gob包
介绍:将内容存放成二进制文件
特点:快速将内存数据写入到一个或多个文件中
示例代码:
file := "test"
// 写入二进制内容到文件
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
encoder.Encode("test gob")
ioutil.WriteFile(file, buf.Bytes(), 0644)
// 从二进制文件中读取
result := ""
d, _ := ioutil.ReadFile(file)
buf = bytes.NewBuffer(d)
decoder := gob.NewDecoder(buf)
decoder.Decode(&result)
关系型数据库
将二维数据存放在:mysql、sqlserver、postgresql、oracle等关系性数据库服务中。
- go sql包
- 提供了对数据库的CRUD(Create、Retrieve、Update、Delete)操作。
- 对数据库操作之前需要先对数据库进行连接。
import _ "ithub.com/go-sql-driver/mysql" // 只使用mysql包的初始化方法(sql.Register("mysql", &MySQLDriver{})方法,注册mysql CRUD驱动),不使用包内提供的方法
var Db *sql.Db // 数据库handle,代表一个包含0个或任意多个数据库连接的连接池
func init(){ // 初始化Db数据库句柄
var err error
Db,err = sql.Open("mysql","user:password@/dbname") // 惰性连接,在要执行CRUD操作时才建立连接
if err !=nil{
panic("sql.Open error")
}
}
- CRUD操作示例
Db.QueryRow() // 查询一行
row.Scan() // 获取查询结果,并映射到程序中的结构体字段中
Db.Query() // 查询多行
row.Next() // 遍历多行结果
Db.Exec() // 执行更新或删除语句
- 关系映射器ORM(三方库提供的支持:将关系型数据库中的表映射为程序中的对象)
// 1. sqlx:github.com/jmoiron/sqlx
// 2. GROM: github.com/jinzhu/gorm // 最常用
go web服务
基于SOAP(Simple Object Access Protocol)的web服务
- 使用xml格式数据;
- 出现较早、w3c标准化、文档资料丰富、扩展丰富(WS-*开头),企业级系统常用;
- 使用WSDL对客户端和服务端提供坚实的契约(如:服务的操作、输入、输出都需要定义契约);
- RPC风格,一般用在内部应用集成。
- go提供的内置相关库
encoding/xml : 分析xml
xml.Unmarshal() // 反序列化为go中的结构体
xml.NewDecoder() // 根据给定的xml数据,生成相应的解码器
xml.Marshal() // 根据go中结构体生成xml数据
...
基于REST(Representational State Transfer:具象状态传输)的web服务
- 灵活,使用如json格式数据;
- 用少许动作(GET、POST、PUT、DELETE等),操作资源;
- 实现简单,一般用来提供三方接口。
- go提供的内置相关库
encoding/json : 分析json数据内置库
json.Marshal()
json.Unmarshal()
json.NewDecoder()
...
应用测试
内置测试库: testing
- 文件名*_test.go结尾
- 测试方法以Test开头,签名为func Test(t *testing.T){...}
- 常用方法:
// 日志相关
t.Log()
t.Logf()
t.Error()
t.Errorf()
// 指令
go test
-v
-count=1
-short TestFuncName // 只执行单个测试方法
-parallel 3 // 并行执行多个测试方法
- 基准测试
- 测试方法以Benchmark头,签名为Benchmark(*testing.B){...}
- 常用方法:
go test 选项 -test.count 定义基准测试次数 -run 运行单个基准测试的方法
内置测试库:net/http/httptest(基于内置testing包实现)
// ----简易http测试流程----
// 1. 创建多路复用器
mux := http.NewServeMux()
// 2. 将被测试的handler绑定到多路复用器
mux.HandleFunc("url",handle)
// 3. 创建记录器
httptest.NewRecorder()
// 4. 创建请求
httptest.NewRequest(writer,request)
// 5. 向被测试的handler发送请求
mux.ServeHTTP()
// 6. 分析记录器中的响应
if writer.Code... {}
替身测试
- 一种依赖注入的设计模式,需要在被测试代码中添加和修改;
- 不希望用到实际的对象、结构体、函数,使用替身模拟它们;
- 替身一般是一个接口,实现了替换对象相关的所有方法;
- 再创建新的替身(对象、结构体、函数等)实现接口即可。
第三方库
- gocheck
- ginkgo
并发
- gorouting
- channel
- 并发相关的包sync
部署go应用
- 二进制
- docker