From 6705c513182330731fd60e44e44425be3f0291ef Mon Sep 17 00:00:00 2001 From: iswbm Date: Sat, 26 Mar 2022 13:57:24 +0800 Subject: [PATCH] update --- source/c03/c03_05.md | 58 ++++++++++ source/c03/c03_05.rst | 64 +++++++++++ source/c06/c06_06.md | 229 ++++++++++++++++++++++++++++++++++++++ source/c06/c06_06.rst | 241 ++++++++++++++++++++++++++++++++++++++++ source/c07/c07_04.md | 134 ++++++++++++++++++++++ source/c07/c07_04.rst | 153 +++++++++++++++++++++++++ source/c08/c08_01.md | 142 +++++++++++++++++++++++ source/c08/c08_01.rst | 152 +++++++++++++++++++++++++ source/c08/c08_02.md | 123 ++++++++++++++++++++ source/c08/c08_02.rst | 149 +++++++++++++++++++++++++ source/c08/c08_03.md | 2 + source/c08/c08_03.rst | 2 + source/c08/c08_04.md | 1 + source/c08/c08_04.rst | 2 + source/chapters/p08.rst | 17 +++ 15 files changed, 1469 insertions(+) create mode 100644 source/c03/c03_05.md create mode 100644 source/c03/c03_05.rst create mode 100644 source/c06/c06_06.md create mode 100644 source/c06/c06_06.rst create mode 100644 source/c07/c07_04.md create mode 100644 source/c07/c07_04.rst create mode 100644 source/c08/c08_01.md create mode 100644 source/c08/c08_01.rst create mode 100644 source/c08/c08_02.md create mode 100644 source/c08/c08_02.rst create mode 100644 source/c08/c08_03.md create mode 100644 source/c08/c08_03.rst create mode 100644 source/c08/c08_04.md create mode 100644 source/c08/c08_04.rst create mode 100644 source/chapters/p08.rst diff --git a/source/c03/c03_05.md b/source/c03/c03_05.md new file mode 100644 index 0000000..e7fad67 --- /dev/null +++ b/source/c03/c03_05.md @@ -0,0 +1,58 @@ +# 3.5 编译流程:结合 Makefile 简化编译过程 + +![](http://image.iswbm.com/20200607145423.png) + +在另一篇文章中([使用 -ldflags 实现动态信息注入](https://golang.iswbm.com/c06/c06_06.html)) 我详细介绍了如何利用 -ldflags 动态往程序中注入信息,但这种技巧需要指定一大串的参数,相信你已经崩溃了吧? + +更合理的做法,是将这些参数 Makefile 来管理维护,在 Makefile 中可以用 shell 命令去获取一些 git 的信息,比如下面这样子 + +``` bash +# gitTag +gitTag=$(git log --pretty=format:'%h' -n 1) + +# commitID +gitCommit=$(git rev-parse --short HEAD) + +# gitBranch +gitBranch=$(git rev-parse --abbrev-ref HEAD) +``` + +我先在该项目下初始化 Git 仓库 + +```bash +# 初始化 +git init . + +# 添加所有文件到暂存区 +git add -A + +# 提交 commit +git commit -m "init repo" +``` + +然后编写出如下的 Makefile 到项目的根目录 + +```makefile +BINARY="demo" +VERSION=0.0.1 +BUILD=`date +%F` +SHELL := /bin/bash + +versionDir="github.com/iswbm/demo/utils" +gitTag=$(shell git log --pretty=format:'%h' -n 1) +gitBranch=$(shell git rev-parse --abbrev-ref HEAD) +buildDate=$(shell TZ=Asia/Shanghai date +%FT%T%z) +gitCommit=$(shell git rev-parse --short HEAD) + +ldflags="-s -w -X ${versionDir}.version=${VERSION} -X ${versionDir}.gitBranch=${gitBranch} -X '${versionDir}.gitTag=${gitTag}' -X '${versionDir}.gitCommit=${gitCommit}' -X '${versionDir}.buildDate=${buildDate}'" + +default: + @echo "build the ${BINARY}" + @GOOS=linux GOARCH=amd64 go build -ldflags ${ldflags} -o build/${BINARY}.linux -tags=jsoniter + @go build -ldflags ${ldflags} -o build/${BINARY}.mac -tags=jsoniter + @echo "build done." +``` + +接下来就可以直接使用 make 命令,编译出 mac 和 linux 两个版本的二进制执行文件 + +![](https://image.iswbm.com/20220325225943.png) \ No newline at end of file diff --git a/source/c03/c03_05.rst b/source/c03/c03_05.rst new file mode 100644 index 0000000..af29ad2 --- /dev/null +++ b/source/c03/c03_05.rst @@ -0,0 +1,64 @@ +3.5 编译流程:结合 Makefile 简化编译过程 +======================================== + +.. image:: http://image.iswbm.com/20200607145423.png + +在另一篇文章中(\ `使用 -ldflags +实现动态信息注入 `__\ ) +我详细介绍了如何利用 -ldflags +动态往程序中注入信息,但这种技巧需要指定一大串的参数,相信你已经崩溃了吧? + +更合理的做法,是将这些参数 Makefile 来管理维护,在 Makefile 中可以用 +shell 命令去获取一些 git 的信息,比如下面这样子 + +.. code::  bash + + # gitTag + gitTag=$(git log --pretty=format:'%h' -n 1) + + # commitID + gitCommit=$(git rev-parse --short HEAD) + + # gitBranch + gitBranch=$(git rev-parse --abbrev-ref HEAD) + +我先在该项目下初始化 Git 仓库 + +.. code:: bash + + # 初始化 + git init . + + # 添加所有文件到暂存区 + git add -A + + # 提交 commit + git commit -m "init repo" + +然后编写出如下的 Makefile 到项目的根目录 + +.. code:: makefile + + BINARY="demo" + VERSION=0.0.1 + BUILD=`date +%F` + SHELL := /bin/bash + + versionDir="github.com/iswbm/demo/utils" + gitTag=$(shell git log --pretty=format:'%h' -n 1) + gitBranch=$(shell git rev-parse --abbrev-ref HEAD) + buildDate=$(shell TZ=Asia/Shanghai date +%FT%T%z) + gitCommit=$(shell git rev-parse --short HEAD) + + ldflags="-s -w -X ${versionDir}.version=${VERSION} -X ${versionDir}.gitBranch=${gitBranch} -X '${versionDir}.gitTag=${gitTag}' -X '${versionDir}.gitCommit=${gitCommit}' -X '${versionDir}.buildDate=${buildDate}'" + + default: + @echo "build the ${BINARY}" + @GOOS=linux GOARCH=amd64 go build -ldflags ${ldflags} -o build/${BINARY}.linux -tags=jsoniter + @go build -ldflags ${ldflags} -o build/${BINARY}.mac -tags=jsoniter + @echo "build done." + +接下来就可以直接使用 make 命令,编译出 mac 和 linux +两个版本的二进制执行文件 + +.. image:: https://image.iswbm.com/20220325225943.png diff --git a/source/c06/c06_06.md b/source/c06/c06_06.md new file mode 100644 index 0000000..3f8c27e --- /dev/null +++ b/source/c06/c06_06.md @@ -0,0 +1,229 @@ +# 6.6 使用 -ldflags 实现动态信息注入 + +![](http://image.iswbm.com/20200607145423.png) + +在查看一些工具的版本时,我们时常能看到版本信息非常多,连 git 的 commit id 都有 + +```bash +~ ➤ docker version +Client: + Cloud integration: v1.0.22 + Version: 20.10.11 + API version: 1.41 + Go version: go1.16.10 + Git commit: dea9396 + Built: Thu Nov 18 00:36:09 2021 + OS/Arch: darwin/arm64 + Context: default + Experimental: true +``` + +最值得关注的是很多信息在每次构建时都会发生变化,如果这些信息是写死在代码中的变量里的,那意味着每次构建都要修改代码,一般情况下都不允许随意代码,构建时的代码应与 git 版本分支上保持一致。 + +## 1. 实现动态信息注入 + +那 Go 程序又是如何实现这种个性化信息的动态注入呢? + +在 go build 命令里有一个 `-ldflags` 参数,该参数可以接收 `-X importpath.name=value` 形式的值,该值就是实现信息动态注入的核心入口。 + +以下面一段例子来演示 + +- 先定义 version,buildTime,osArch 三个变量 +- 然后将这三个变量的值打印出来 + +```go +package main + +import "fmt" + +var ( + version string + buildTime string + osArch string +) + +func main() { + fmt.Printf("Version: %s\nBuilt: %s\nOS/Arch: %s\n", version, buildTime, osArch) +} +``` + +由于我们只是声明了变量,但没有对其赋值,因为三个变量的值都是零值,也就是空字符串。 + +```bash +~ ➤ go run main.go +Version: +Built: +OS/Arch: +``` + +此时,我给 run 或者 build 加上如下的 -ldflags 参数,Go 的编译器就能接收到并赋值给我们指定的变量 + +``` +~ ➤ go run -ldflags "-X 'main.version=0.1' -X 'main.buildTime=2022-03-25' -X 'main.osArch=darwin/amd64'" main.go +Version: 0.1 +Built: 2022-03-25 +OS/Arch: darwin/amd64 +``` + + 我们只要编译一次,后续执行二进制文件就不用再指定这么长的一长参数了 + +``` +~ ➤ go build -ldflags "-X 'main.version=0.1' -X 'main.buildTime=2022-03-25' -X 'main.osArch=darwin/amd64'" main.go +~ ➤ +~ ➤ ./main +Version: 0.1 +Built: 2022-03-25 +OS/Arch: darwin/amd64 +``` + +## 2. 实际开发项目 + +上面为了方便学习,主程序直接将版本信息直接打印出来了,实际上应该指定 version 参数再打印。 + +有了前面的基础知识,下边就演示一下正常开发中如何来注入版本信息 + +首先,初始化项目 + +```go +go mod init github.com/iswbm/demo +``` + +然后创建 main.go + +```go +package main + +import ( + "fmt" + "os" + "github.com/iswbm/demo/utils" +) + +func main() { + + args := os.Args + if len(args) >= 2 && args[1] == "version" { + v := utils.GetVersion() + fmt.Printf("Version: %s\nGitBranch: %s\nCommitId: %s\nBuild Date: %s\nGo Version: %s\nOS/Arch: %s\n", v.Version, v.GitBranch, v.GitCommit, v.BuildDate, v.GoVersion, v.Platform) + } else { + fmt.Printf("Version(hard code): %s\n", "0.1") + } +} +``` + +再创建 utils/version.go + +```go +package utils + +import ( + "fmt" + "runtime" +) + +var ( + version string + gitBranch string + gitTag string + gitCommit string + gitTreeState string + buildDate string +) + +type Info struct { + Version string `json:"version"` + GitBranch string `json:"gitBranch"` + GitTag string `json:"gitTag"` + GitCommit string `json:"gitCommit"` + GitTreeState string `json:"gitTreeState"` + BuildDate string `json:"buildDate"` + GoVersion string `json:"goVersion"` + Compiler string `json:"compiler"` + Platform string `json:"platform"` +} + +func (info Info) String() string { + return info.GitCommit +} + +func GetVersion() Info { + return Info{ + Version: version, + GitBranch: gitBranch, + GitTag: gitTag, + GitCommit: gitCommit, + GitTreeState: gitTreeState, + BuildDate: buildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + } +} +``` + +最后,使用如下命令去编译 + +```bash +go build -ldflags "-X 'github.com/iswbm/demo/utils.version=0.1' -X 'github.com/iswbm/demo/utils.gitBranch=test' -X 'github.com/iswbm/demo/utils.gitTag=test' -X 'github.com/iswbm/demo/utils.gitCommit=test' -X 'github.com/iswbm/demo/utils.buildDate=2022-03-25' -X 'github.com/iswbm/demo/utils.osArch=darwin/amd64'" +``` + +编译好后,可以运行一下看效果 + +![](https://image.iswbm.com/image-20220324224811637.png) + +## 3. 使用 Makekfile + +上面在编译的时候,需要指定一大串的参数,相信你已经崩溃了吧? + +更合理的做法,是将这些参数 Makefile 来管理维护,在 Makefile 中可以用 shell 命令去获取一些 git 的信息,比如下面这样子 + +``` +# gitTag +gitTag=$(git log --pretty=format:'%h' -n 1) + +# commitID +gitCommit=$(git rev-parse --short HEAD) + +# gitBranch +gitBranch=$(git rev-parse --abbrev-ref HEAD) +``` + +我先在该项目下初始化 Git 仓库 + +```bash +# 初始化 +git init . + +# 添加所有文件到暂存区 +git add -A + +# 提交 commit +git commit -m "init repo" +``` + +然后编写出如下的 Makefile 到项目的根目录 + +```makefile +BINARY="demo" +VERSION=0.0.1 +BUILD=`date +%F` +SHELL := /bin/bash + +versionDir="github.com/iswbm/demo/utils" +gitTag=$(shell git log --pretty=format:'%h' -n 1) +gitBranch=$(shell git rev-parse --abbrev-ref HEAD) +buildDate=$(shell TZ=Asia/Shanghai date +%FT%T%z) +gitCommit=$(shell git rev-parse --short HEAD) + +ldflags="-s -w -X ${versionDir}.version=${VERSION} -X ${versionDir}.gitBranch=${gitBranch} -X '${versionDir}.gitTag=${gitTag}' -X '${versionDir}.gitCommit=${gitCommit}' -X '${versionDir}.buildDate=${buildDate}'" + +default: + @echo "build the ${BINARY}" + @GOOS=linux GOARCH=amd64 go build -ldflags ${ldflags} -o build/${BINARY}.linux -tags=jsoniter + @go build -ldflags ${ldflags} -o build/${BINARY}.mac -tags=jsoniter + @echo "build done." +``` + +接下来就可以直接使用 make 命令,编译出 mac 和 linux 两个版本的二进制执行文件 + +![](https://image.iswbm.com/20220325225943.png) \ No newline at end of file diff --git a/source/c06/c06_06.rst b/source/c06/c06_06.rst new file mode 100644 index 0000000..ee5ad77 --- /dev/null +++ b/source/c06/c06_06.rst @@ -0,0 +1,241 @@ +6.6 使用 -ldflags 实现动态信息注入 +================================== + +.. image:: http://image.iswbm.com/20200607145423.png + +在查看一些工具的版本时,我们时常能看到版本信息非常多,连 git 的 commit +id 都有 + +.. code:: bash + + ~ ➤ docker version + Client: + Cloud integration: v1.0.22 + Version: 20.10.11 + API version: 1.41 + Go version: go1.16.10 + Git commit: dea9396 + Built: Thu Nov 18 00:36:09 2021 + OS/Arch: darwin/arm64 + Context: default + Experimental: true + +最值得关注的是很多信息在每次构建时都会发生变化,如果这些信息是写死在代码中的变量里的,那意味着每次构建都要修改代码,一般情况下都不允许随意代码,构建时的代码应与 +git 版本分支上保持一致。 + +1. 实现动态信息注入 +------------------- + +那 Go 程序又是如何实现这种个性化信息的动态注入呢? + +在 go build 命令里有一个 ``-ldflags`` 参数,该参数可以接收 +``-X importpath.name=value`` +形式的值,该值就是实现信息动态注入的核心入口。 + +以下面一段例子来演示 + +- 先定义 version,buildTime,osArch 三个变量 +- 然后将这三个变量的值打印出来 + +.. code:: go + + package main + + import "fmt" + + var ( + version string + buildTime string + osArch string + ) + + func main() { + fmt.Printf("Version: %s\nBuilt: %s\nOS/Arch: %s\n", version, buildTime, osArch) + } + +由于我们只是声明了变量,但没有对其赋值,因为三个变量的值都是零值,也就是空字符串。 + +.. code:: bash + + ~ ➤ go run main.go + Version: + Built: + OS/Arch: + +此时,我给 run 或者 build 加上如下的 -ldflags 参数,Go +的编译器就能接收到并赋值给我们指定的变量 + +:: + + ~ ➤ go run -ldflags "-X 'main.version=0.1' -X 'main.buildTime=2022-03-25' -X 'main.osArch=darwin/amd64'" main.go + Version: 0.1 + Built: 2022-03-25 + OS/Arch: darwin/amd64 + +我们只要编译一次,后续执行二进制文件就不用再指定这么长的一长参数了 + +:: + + ~ ➤ go build -ldflags "-X 'main.version=0.1' -X 'main.buildTime=2022-03-25' -X 'main.osArch=darwin/amd64'" main.go + ~ ➤ + ~ ➤ ./main + Version: 0.1 + Built: 2022-03-25 + OS/Arch: darwin/amd64 + +2. 实际开发项目 +--------------- + +上面为了方便学习,主程序直接将版本信息直接打印出来了,实际上应该指定 +version 参数再打印。 + +有了前面的基础知识,下边就演示一下正常开发中如何来注入版本信息 + +首先,初始化项目 + +.. code:: go + + go mod init github.com/iswbm/demo + +然后创建 main.go + +.. code:: go + + package main + + import ( + "fmt" + "os" + "github.com/iswbm/demo/utils" + ) + + func main() { + + args := os.Args + if len(args) >= 2 && args[1] == "version" { + v := utils.GetVersion() + fmt.Printf("Version: %s\nGitBranch: %s\nCommitId: %s\nBuild Date: %s\nGo Version: %s\nOS/Arch: %s\n", v.Version, v.GitBranch, v.GitCommit, v.BuildDate, v.GoVersion, v.Platform) + } else { + fmt.Printf("Version(hard code): %s\n", "0.1") + } + } + +再创建 utils/version.go + +.. code:: go + + package utils + + import ( + "fmt" + "runtime" + ) + + var ( + version string + gitBranch string + gitTag string + gitCommit string + gitTreeState string + buildDate string + ) + + type Info struct { + Version string `json:"version"` + GitBranch string `json:"gitBranch"` + GitTag string `json:"gitTag"` + GitCommit string `json:"gitCommit"` + GitTreeState string `json:"gitTreeState"` + BuildDate string `json:"buildDate"` + GoVersion string `json:"goVersion"` + Compiler string `json:"compiler"` + Platform string `json:"platform"` + } + + func (info Info) String() string { + return info.GitCommit + } + + func GetVersion() Info { + return Info{ + Version: version, + GitBranch: gitBranch, + GitTag: gitTag, + GitCommit: gitCommit, + GitTreeState: gitTreeState, + BuildDate: buildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + } + } + +最后,使用如下命令去编译 + +.. code:: bash + + go build -ldflags "-X 'github.com/iswbm/demo/utils.version=0.1' -X 'github.com/iswbm/demo/utils.gitBranch=test' -X 'github.com/iswbm/demo/utils.gitTag=test' -X 'github.com/iswbm/demo/utils.gitCommit=test' -X 'github.com/iswbm/demo/utils.buildDate=2022-03-25' -X 'github.com/iswbm/demo/utils.osArch=darwin/amd64'" + +编译好后,可以运行一下看效果 + +.. image:: https://image.iswbm.com/image-20220324224811637.png + +3. 使用 Makekfile +----------------- + +上面在编译的时候,需要指定一大串的参数,相信你已经崩溃了吧? + +更合理的做法,是将这些参数 Makefile 来管理维护,在 Makefile 中可以用 +shell 命令去获取一些 git 的信息,比如下面这样子 + +:: + + # gitTag + gitTag=$(git log --pretty=format:'%h' -n 1) + + # commitID + gitCommit=$(git rev-parse --short HEAD) + + # gitBranch + gitBranch=$(git rev-parse --abbrev-ref HEAD) + +我先在该项目下初始化 Git 仓库 + +.. code:: bash + + # 初始化 + git init . + + # 添加所有文件到暂存区 + git add -A + + # 提交 commit + git commit -m "init repo" + +然后编写出如下的 Makefile 到项目的根目录 + +.. code:: makefile + + BINARY="demo" + VERSION=0.0.1 + BUILD=`date +%F` + SHELL := /bin/bash + + versionDir="github.com/iswbm/demo/utils" + gitTag=$(shell git log --pretty=format:'%h' -n 1) + gitBranch=$(shell git rev-parse --abbrev-ref HEAD) + buildDate=$(shell TZ=Asia/Shanghai date +%FT%T%z) + gitCommit=$(shell git rev-parse --short HEAD) + + ldflags="-s -w -X ${versionDir}.version=${VERSION} -X ${versionDir}.gitBranch=${gitBranch} -X '${versionDir}.gitTag=${gitTag}' -X '${versionDir}.gitCommit=${gitCommit}' -X '${versionDir}.buildDate=${buildDate}'" + + default: + @echo "build the ${BINARY}" + @GOOS=linux GOARCH=amd64 go build -ldflags ${ldflags} -o build/${BINARY}.linux -tags=jsoniter + @go build -ldflags ${ldflags} -o build/${BINARY}.mac -tags=jsoniter + @echo "build done." + +接下来就可以直接使用 make 命令,编译出 mac 和 linux +两个版本的二进制执行文件 + +.. image:: https://image.iswbm.com/20220325225943.png diff --git a/source/c07/c07_04.md b/source/c07/c07_04.md new file mode 100644 index 0000000..5d85583 --- /dev/null +++ b/source/c07/c07_04.md @@ -0,0 +1,134 @@ +# 7.4 一文掌握 Go 泛型的使用 + +泛型,可以说是 Go 这几年来最具争议的功能,应该没人有意见吧? + +其实 Go 在早前的 Beta 版本中,就提供了对泛型的支持,但还不够成熟,直到 Go 1.18 才是支持泛型的正式版本。 + +下面我学习了官方关于泛型的文档之后,将学习的心得总结分享给大家 + +## 1. 非泛型的写法 + +现有一个 map ,我们需要实现一个函数,来遍历该 map 然后将 value 的值全部相加并返回。 + +而由于这个 map 的 value 可以是任意类型的数值,比如 int64, float64 + +于是为了接收不同类型的 map,我们就得定义多个函数,这些函数 **除了入参类型及返回值类型不同外,没有任何不同** + +```go +func SumInts(m map[string]int64) int64 { + var s int64 + for _, v := range m { + s += v + } + return s +} + +func SumFloats(m map[string]float64) float64 { + var s float64 + for _, v := range m { + s += v + } + return s +} +``` + +## 2. 用泛型的写法 + +若是以代码行数来定义工作量,我可不希望泛型的出现,但从另一方面来讲,这种代码横看竖看都让人非常不舒服。 + +同样的需求,在有了泛型之后,写法就变得简洁许多 + +```go +func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s +} +``` + +在这个函数中,它比常规的函数多了 `[K comparable, V int64 | float64]` 这么一段代码,这便是 **泛型新增的语法**,位于函数名与形参之间。 + +我来解释下这段 “代码”: + +- K 和 V 你可以理解为类型别名,在中括号之间进行定义,作用域也只在此函数内,可以在形参、函数主体、返回值类型 里使用 +- comparable 是 Go 语言预声明的类型,是那些可以比较(可哈希)的类型的集合,通常用于定义 map 里的 key 类型 +- int64 | float64 意思是 V 可以是 int64 或 float64 中的任意一个 +- map[K]V 就是使用了 K 和 V 这两个别名类型的 map + +有了泛型函数的定义,那如何调用该函数? + +调用方式还是跟普通函数一样,只是在函数名和实参之间,可以再次使用中括号来指明上面的 K 和 V 分别是什么类型? + +```go +func main() { + // Initialize a map for the integer values + ints := map[string]int64{ + "first": 34, + "second": 12, + } + + // Initialize a map for the float values + floats := map[string]float64{ + "first": 35.98, + "second": 26.99, + } + + fmt.Printf("Generic Sums: %v and %v\n", + SumIntsOrFloats[string, int64](ints), + SumIntsOrFloats[string, float64](floats), + ) +} +``` + +最后使用 go run 去跑一下,结果正常输出 + +![](https://image.iswbm.com/image-20220321215708803.png) + +## 3. 简化泛型写法 + +### 3.1 类型自动推导 + + 在调用大部分的泛型函数时,中括号里的内容,是可以省略不写的,而这个不写的前提是,编译器有办法根据你的实参及形参来自动推导出泛型函数中 别名类型对应的类型(在上例中就是 K 和 V)。 + +而在上面的例子中,刚好是满足的,于是泛型函数的调用就可以简化成这样 + +```go + fmt.Printf("Generic Sums: %v and %v\n", + SumIntsOrFloats(ints), + SumIntsOrFloats(floats), + ) +``` + +### 3.2 使用类型别名 + +上面的 V 使用 `int64 | float64` 这样的写法来表示 V 可以是其中的任意一种类型。 + +若这个 V 用得比较多呢?可以考虑用 type 来事先定义别名 + +```go +type Number interface { + int64 | float64 +} +``` + +然后泛型函数的定义就可以简化成下面这样 + +```go +func SumNumbers[K comparable, V Number](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s +} +``` + +## 4. 写在最后 + +在去年,其实就通过其他人的文章中事先了解到了 Go 泛型的写法,给我的第一印象是,函数的定义变得更复杂,可读性也越来越差,一时间我也有点难以接受。 + +不过经过自己试用后,情况倒没有我想象的那么糟糕!新版没有改变原有函数的定义与调用,若你没有使用泛型,那么有没有泛型对你来说没有区别。 + +但即使你有想法需要用到泛型,我也相信这种的不适感会在时间的流逝中慢慢淡化。 \ No newline at end of file diff --git a/source/c07/c07_04.rst b/source/c07/c07_04.rst new file mode 100644 index 0000000..8128d34 --- /dev/null +++ b/source/c07/c07_04.rst @@ -0,0 +1,153 @@ +7.4 一文掌握 Go 泛型的使用 +========================== + +泛型,可以说是 Go 这几年来最具争议的功能,应该没人有意见吧? + +其实 Go 在早前的 Beta 版本中,就提供了对泛型的支持,但还不够成熟,直到 +Go 1.18 才是支持泛型的正式版本。 + +下面我学习了官方关于泛型的文档之后,将学习的心得总结分享给大家 + +1. 非泛型的写法 +--------------- + +现有一个 map ,我们需要实现一个函数,来遍历该 map 然后将 value +的值全部相加并返回。 + +而由于这个 map 的 value 可以是任意类型的数值,比如 int64, float64 + +于是为了接收不同类型的 map,我们就得定义多个函数,这些函数 +**除了入参类型及返回值类型不同外,没有任何不同** + +.. code:: go + + func SumInts(m map[string]int64) int64 { + var s int64 + for _, v := range m { + s += v + } + return s + } + + func SumFloats(m map[string]float64) float64 { + var s float64 + for _, v := range m { + s += v + } + return s + } + +2. 用泛型的写法 +--------------- + +若是以代码行数来定义工作量,我可不希望泛型的出现,但从另一方面来讲,这种代码横看竖看都让人非常不舒服。 + +同样的需求,在有了泛型之后,写法就变得简洁许多 + +.. code:: go + + func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s + } + +在这个函数中,它比常规的函数多了 ``[K comparable, V int64 | float64]`` +这么一段代码,这便是 **泛型新增的语法**\ ,位于函数名与形参之间。 + +我来解释下这段 “代码”: + +- K 和 V + 你可以理解为类型别名,在中括号之间进行定义,作用域也只在此函数内,可以在形参、函数主体、返回值类型 + 里使用 +- comparable 是 Go + 语言预声明的类型,是那些可以比较(可哈希)的类型的集合,通常用于定义 + map 里的 key 类型 +- int64 \| float64 意思是 V 可以是 int64 或 float64 中的任意一个 +- map[K]V 就是使用了 K 和 V 这两个别名类型的 map + +有了泛型函数的定义,那如何调用该函数? + +调用方式还是跟普通函数一样,只是在函数名和实参之间,可以再次使用中括号来指明上面的 +K 和 V 分别是什么类型? + +.. code:: go + + func main() { + // Initialize a map for the integer values + ints := map[string]int64{ + "first": 34, + "second": 12, + } + + // Initialize a map for the float values + floats := map[string]float64{ + "first": 35.98, + "second": 26.99, + } + + fmt.Printf("Generic Sums: %v and %v\n", + SumIntsOrFloats[string, int64](ints), + SumIntsOrFloats[string, float64](floats), + ) + } + +最后使用 go run 去跑一下,结果正常输出 + +.. image:: https://image.iswbm.com/image-20220321215708803.png + +3. 简化泛型写法 +--------------- + +3.1 类型自动推导 +~~~~~~~~~~~~~~~~ + +在调用大部分的泛型函数时,中括号里的内容,是可以省略不写的,而这个不写的前提是,编译器有办法根据你的实参及形参来自动推导出泛型函数中 +别名类型对应的类型(在上例中就是 K 和 V)。 + +而在上面的例子中,刚好是满足的,于是泛型函数的调用就可以简化成这样 + +.. code:: go + + fmt.Printf("Generic Sums: %v and %v\n", + SumIntsOrFloats(ints), + SumIntsOrFloats(floats), + ) + +3.2 使用类型别名 +~~~~~~~~~~~~~~~~ + +上面的 V 使用 ``int64 | float64`` 这样的写法来表示 V +可以是其中的任意一种类型。 + +若这个 V 用得比较多呢?可以考虑用 type 来事先定义别名 + +.. code:: go + + type Number interface { + int64 | float64 + } + +然后泛型函数的定义就可以简化成下面这样 + +.. code:: go + + func SumNumbers[K comparable, V Number](m map[K]V) V { + var s V + for _, v := range m { + s += v + } + return s + } + +4. 写在最后 +----------- + +在去年,其实就通过其他人的文章中事先了解到了 Go +泛型的写法,给我的第一印象是,函数的定义变得更复杂,可读性也越来越差,一时间我也有点难以接受。 + +不过经过自己试用后,情况倒没有我想象的那么糟糕!新版没有改变原有函数的定义与调用,若你没有使用泛型,那么有没有泛型对你来说没有区别。 + +但即使你有想法需要用到泛型,我也相信这种的不适感会在时间的流逝中慢慢淡化。 diff --git a/source/c08/c08_01.md b/source/c08/c08_01.md new file mode 100644 index 0000000..b920322 --- /dev/null +++ b/source/c08/c08_01.md @@ -0,0 +1,142 @@ +# 8.1 测试技巧:单元测试(Unit Test) + +单元测试(Unit Tests, UT) 是一个优秀项目不可或缺的一部分,特别是在一些频繁变动和多人合作开发的项目中尤为重要。 + +写单元测试代码是一件短期没什么用,但却能长期收益的事情,特别是在人比较多的大团队里。 + +很多初级开发者不愿意花时间写测试代码,因为写测试代码比功能代码少了一些创造性,没有个人成就感,况且迭代快、排期紧导致没有时间去安排写单元测试。 + +在以下这些场景中,没有养成写单元测试习惯的话,就是一个灾难 + +- 同事修改了某个之前由你编写的函数,但由于同事对这块函数理解上的不足,影响了某个异常场景的处理,你的同事没有测试到,把 bug 流到线上去 +- 某个函数的逻辑比较复杂,该函数的改动也很频繁,每一次的改过都要测试非常多的场景,费时费力 + +## 1. 如何写单元测试 + +在开始之前,先初始化项目 + +```bash +go mod init github.com/iswbm/fuzz +``` + +然后在该项目中添加 main.go,内容如下 + +```go +package main + +import "fmt" + +func Reverse(s string) string { + b := [] byte(s) + for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + return string(b) +} + +func main() { + input := "The quick brown fox jumped over the lazy dog" + rev := Reverse(input) + doubleRev := Reverse(rev) + fmt.Printf("original: %q\n", input) + fmt.Printf("reversed: %q\n", rev) + fmt.Printf("reversed again: %q\n", doubleRev) +} +``` + + 现在我们要为 Reverse 函数编写单元测试代码,放在 reverse_test.go,Test 函数如下 + +- 给定了三组数据 +- 遍历这几组数据,将 tc.in 做为 Reverses 函数的入参执行函数,其返回值跟预期的 tc.want 做对比 +- 若不相等,则测试不通过~ + +```go +package main + +import ( + "testing" +) + +func TestReverse(t *testing.T) { + testcases := []struct { + in, want string + }{ + {"Hello, world", "dlrow ,olleH"}, + {" ", " "}, + {"!12345", "54321!"}, + } + for _, tc := range testcases { + rev := Reverse(tc.in) + if rev != tc.want { + t.Errorf("Reverse: %q, want %q", rev, tc.want) + } + } +} +``` + +对于单元测试函数来说,它的编写有一些格式,需要提一下,不然上面的函数,你可能会有疑问: + +- 单元测试,要导入 testing 包 +- 承载测试用例的测试文件,固定以 xxx_test.go(xxx 是原文件名) +- 测试用例函数名称一般命名为 `Test` 加上待测试的方法名。 +- 测试用例函数的参数有且只有一个,在这里是 `t *testing.T` + +## 2. 执行测试用例 + +现在我们执行 go test 即是普通的单元测试,即执行该 package 下的所有函数的测试用例,输出 PASS 说明单元测试通过 + +![](https://image.iswbm.com/image-20220326130634024.png) + +要是加一个 `-v` 就可以查看显示每个测试用例的测试结果 + +![](https://image.iswbm.com/image-20220326130601941.png) + +## 3. 子测试用例 + +如果有很多测试用例,可以用 -run 指定某个某个测试用例 + +![](https://image.iswbm.com/image-20220326131019313.png) + +若一个测试用例还可以分为多个子测试用例,比如下边的测试用例分为 foo 和 bar 两个子测试用例 + +```go +package main + +import ( + "testing" +) + +func TestReverse(t *testing.T) { + t.Run("foo", func(t *testing.T) { + testcases := []struct { + in, want string + }{ + {"Hello, foo", "oof ,olleH"}, + } + for _, tc := range testcases { + rev := Reverse(tc.in) + if rev != tc.want { + t.Errorf("[foo test]Reverse: %q, want %q", rev, tc.want) + } + } + }) + + t.Run("bar", func(t *testing.T) { + testcases := []struct { + in, want string + }{ + {"Hello, bar", "rab ,olleH"}, + } + for _, tc := range testcases { + rev := Reverse(tc.in) + if rev != tc.want { + t.Errorf("[bar test] Reverse: %q, want %q", rev, tc.want) + } + } + }) +} +``` + +使用 `-run 主用例/子用例` 就可以执行对应的子用例 + +![](https://image.iswbm.com/image-20220326133200586.png) \ No newline at end of file diff --git a/source/c08/c08_01.rst b/source/c08/c08_01.rst new file mode 100644 index 0000000..61057a3 --- /dev/null +++ b/source/c08/c08_01.rst @@ -0,0 +1,152 @@ +8.1 测试技巧:单元测试(Unit Test) +=================================== + +单元测试(Unit Tests, UT) +是一个优秀项目不可或缺的一部分,特别是在一些频繁变动和多人合作开发的项目中尤为重要。 + +写单元测试代码是一件短期没什么用,但却能长期收益的事情,特别是在人比较多的大团队里。 + +很多初级开发者不愿意花时间写测试代码,因为写测试代码比功能代码少了一些创造性,没有个人成就感,况且迭代快、排期紧导致没有时间去安排写单元测试。 + +在以下这些场景中,没有养成写单元测试习惯的话,就是一个灾难 + +- 同事修改了某个之前由你编写的函数,但由于同事对这块函数理解上的不足,影响了某个异常场景的处理,你的同事没有测试到,把 + bug 流到线上去 +- 某个函数的逻辑比较复杂,该函数的改动也很频繁,每一次的改过都要测试非常多的场景,费时费力 + +1. 如何写单元测试 +----------------- + +在开始之前,先初始化项目 + +.. code:: bash + + go mod init github.com/iswbm/fuzz + +然后在该项目中添加 main.go,内容如下 + +.. code:: go + + package main + + import "fmt" + + func Reverse(s string) string { + b := [] byte(s) + for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { + b[i], b[j] = b[j], b[i] + } + return string(b) + } + + func main() { + input := "The quick brown fox jumped over the lazy dog" + rev := Reverse(input) + doubleRev := Reverse(rev) + fmt.Printf("original: %q\n", input) + fmt.Printf("reversed: %q\n", rev) + fmt.Printf("reversed again: %q\n", doubleRev) + } + +现在我们要为 Reverse 函数编写单元测试代码,放在 reverse_test.go,Test +函数如下 + +- 给定了三组数据 +- 遍历这几组数据,将 tc.in 做为 Reverses + 函数的入参执行函数,其返回值跟预期的 tc.want 做对比 +- 若不相等,则测试不通过~ + +.. code:: go + + package main + + import ( + "testing" + ) + + func TestReverse(t *testing.T) { + testcases := []struct { + in, want string + }{ + {"Hello, world", "dlrow ,olleH"}, + {" ", " "}, + {"!12345", "54321!"}, + } + for _, tc := range testcases { + rev := Reverse(tc.in) + if rev != tc.want { + t.Errorf("Reverse: %q, want %q", rev, tc.want) + } + } + } + +对于单元测试函数来说,它的编写有一些格式,需要提一下,不然上面的函数,你可能会有疑问: + +- 单元测试,要导入 testing 包 +- 承载测试用例的测试文件,固定以 xxx_test.go(xxx 是原文件名) +- 测试用例函数名称一般命名为 ``Test`` 加上待测试的方法名。 +- 测试用例函数的参数有且只有一个,在这里是 ``t *testing.T`` + +2. 执行测试用例 +--------------- + +现在我们执行 go test 即是普通的单元测试,即执行该 package +下的所有函数的测试用例,输出 PASS 说明单元测试通过 + +.. image:: https://image.iswbm.com/image-20220326130634024.png + +要是加一个 ``-v`` 就可以查看显示每个测试用例的测试结果 + +.. image:: https://image.iswbm.com/image-20220326130601941.png + +3. 子测试用例 +------------- + +如果有很多测试用例,可以用 -run 指定某个某个测试用例 + +.. image:: https://image.iswbm.com/image-20220326131019313.png + +若一个测试用例还可以分为多个子测试用例,比如下边的测试用例分为 foo 和 +bar 两个子测试用例 + +.. code:: go + + package main + + import ( + "testing" + ) + + func TestReverse(t *testing.T) { + t.Run("foo", func(t *testing.T) { + testcases := []struct { + in, want string + }{ + {"Hello, foo", "oof ,olleH"}, + } + for _, tc := range testcases { + rev := Reverse(tc.in) + if rev != tc.want { + t.Errorf("[foo test]Reverse: %q, want %q", rev, tc.want) + } + } + }) + + t.Run("bar", func(t *testing.T) { + testcases := []struct { + in, want string + }{ + {"Hello, bar", "rab ,olleH"}, + } + for _, tc := range testcases { + rev := Reverse(tc.in) + if rev != tc.want { + t.Errorf("[bar test] Reverse: %q, want %q", rev, tc.want) + } + } + }) + } + +使用 ``-run 主用例/子用例`` 就可以执行对应的子用例 + +.. image:: https://image.iswbm.com/image-20220326133200586.png diff --git a/source/c08/c08_02.md b/source/c08/c08_02.md new file mode 100644 index 0000000..f9a4c02 --- /dev/null +++ b/source/c08/c08_02.md @@ -0,0 +1,123 @@ +# 8.2 测试技巧:模糊测试(Fuzzing) + +## 1. 什么是模糊测试? + +单元测试,需要开发者根据函数逻辑,给定几组输入(入参)与输出(返回)的数据,然后 go test 根据这些数据集,调用函数,若返回值与预期相符,则说明函数的单元测试通过。 + +但单元测试的代码,也是由开发者写的一段一段代码,只要是代码,就会有 BUG,就会有遗漏的场景。 + +因此即使单元测试通过,也不代表你的程序没有问题。 + +可见,测试场景的数据集对于测试有多重要,而 Fuzzing 模糊测试就是一种用机器根据已知数据源,来自动生成测试数据的一种方案。 + +## 2. 简单的示例 + +接着前一篇文章的例子,我们再往 reverse_test.go 中加入 Fuzzing 模糊测试的代码 + +```go +// 记得前面导入 "unicode/utf8" 包 + +func FuzzReverse(f *testing.F) { + testcases := []string{"Hello, world", " ", "!12345"} + for _, tc := range testcases { + f.Add(tc) // Use f.Add to provide a seed corpus + } + f.Fuzz(func(t *testing.T, orig string) { + rev := Reverse(orig) + doubleRev := Reverse(rev) + if orig != doubleRev { + t.Errorf("Before: %q, after: %q", orig, doubleRev) + } + if utf8.ValidString(orig) && !utf8.ValidString(rev) { + t.Errorf("Reverse produced invalid UTF-8 string %q", rev) + } + }) +} +``` + +Fuzzing 模糊测试的代码格式与单元测试很像: + +- 函数名固定以 Fuzz 开头(单元测试是以 Test 开头) +- 函数固定以 *testing.F 类型做为入参(单元测试是以 *testing.T) + +不一样的是 Fuzzing 模糊测试,提供两个函数: + +- t.Add:用于开发者输入模糊测试的种子数据,fuzzing 根据这些种子数据,自动随机生成更多测试数据 +- t.Fuzz:开始运行模糊测试,t.Fuzz 的入参是一个 Fuzz Target 函数(官方这么叫的),这个 Fuzz Target 函数的编写逻辑跟单元测试就一样了 + +在本例子中,Fuzz Target 接收 类型为 string 的入参,做为 Reverse 的输入源,然后利用两次 Reverse 的结果应与原字符串相等的原理进行测试。 + +有了 FuzzReverse 函数后,就可以使用如下命令进行模糊测试 + +```go +go18 test -fuzz=Fuzz +``` + +通过输出发现测试并不顺利,Go 1.18 的 Fuzzing 会将导致测试异常的数据文件记录下来,使用 cat 可以查看该测试数据 + +![](https://image.iswbm.com/image-20220323214047119.png) + +记录下来后,该数据就可做为普通单元测试的数据,此时我们再执行 go test 就会引用该数据,当然了,在问题解决之前, go test 会一直报错 + +![](https://image.iswbm.com/image-20220323214900909.png) + +## 3. 问题排查与解决 + +模糊测试帮我们发现了一个出乎意料的 Bug 场景:在中文里的字符 `泃`其实是由3个字节组成的,如果按照字节反转,反转后得到的就是一个无效的字符串。 + +因此为了保证字符串反转后得到的仍然是一个有效的UTF-8编码的字符串,我们要按照`rune`进行字符串反转。 + +为了更好地方便大家理解中文里的字符 `泃`按照`rune`为维度有多少个`rune`,以及按照byte反转后得到的结果长什么样,我们对代码做一些修改。 + +![](https://image.iswbm.com/image-20220323214536938.png) + +改完之后,再次执行 go test 就会提示测试成功,说明我们已经修复上面的那个场景的 BUG + +![](https://image.iswbm.com/image-20220323215108358.png) + +当下我们已经发现并修复了一个 BUG,程序肯定还有更多 BUG 存在,要继续寻找可以再次进行模糊测试,重复上面的步骤即可,这里不再赘述。 + +## 4. 更多参数介绍 + +在支持了 Fuzzing 模糊测试后,go test 工具也有了一些新的命令,在这里一并记录下 + +**进行模糊测试** + +``` +go test -fuzz=Fuzz +``` + +**只对某个函数进行模糊测试**:使用 -run=Fuzzxxx 或者 -fuzz=Fuzzxxx 指定模糊测试函数,避免执行到其他测试函数 + +``` +go18 test -run=FuzzReverse +go18 test -fuzz=FuzzReverse +``` + +**测试某个失败数据**:使用 -run=file 指定数据文件 + +``` +go test -run=FuzzReverse/1fdd0160e6b3dd8f1e6b7a4179b4787e0c014cf9c46c67a863d71e3a0277c213 +``` + +**指定模糊测试的时间**:使用 -fuzztime 指定模糊测试时间或者迭代次数(默认无限期),避免一直在跑测试无法退出 + +还有一个 -fuzzminimizetime 参数,看官方文档的介绍,我没明白其作用,有知道的还请评论区分享下 + +``` +go test -fuzz=Fuzz -fuzztime 30s +``` + +**设置模糊测试进程数据**:默认值是 $GOMAXPROCS,可根据实际情况进行设置,避免太占用机器的资源 + +``` +go test -fuzz=Fuzz -parallel 4 +``` + +## 5. 写在最后 + +模糊测试的存在,并不是为了替代原单元测试,而是为单元测试提供更好的保障,是一个补充方案,而非替代方案。 + +单元测试的局限性在于,你只能用预期的输入进行测试;模糊测试在发现暴露出奇怪行为的意外输入方面非常出色。一个好的模糊测试系统也会对被测试的代码进行分析,因此它可以有效地产生输入,从而扩大代码覆盖面。 + +同时模糊测试的适用场景也比较有限,如果函数的入参并不是像本例中的那样的简单(字符串),而是各种对象呢?可能它就无能为力了吧。 \ No newline at end of file diff --git a/source/c08/c08_02.rst b/source/c08/c08_02.rst new file mode 100644 index 0000000..ace55ba --- /dev/null +++ b/source/c08/c08_02.rst @@ -0,0 +1,149 @@ +8.2 测试技巧:模糊测试(Fuzzing) +================================= + +1. 什么是模糊测试? +------------------- + +单元测试,需要开发者根据函数逻辑,给定几组输入(入参)与输出(返回)的数据,然后 +go test +根据这些数据集,调用函数,若返回值与预期相符,则说明函数的单元测试通过。 + +但单元测试的代码,也是由开发者写的一段一段代码,只要是代码,就会有 +BUG,就会有遗漏的场景。 + +因此即使单元测试通过,也不代表你的程序没有问题。 + +可见,测试场景的数据集对于测试有多重要,而 Fuzzing +模糊测试就是一种用机器根据已知数据源,来自动生成测试数据的一种方案。 + +2. 简单的示例 +------------- + +接着前一篇文章的例子,我们再往 reverse_test.go 中加入 Fuzzing +模糊测试的代码 + +.. code:: go + + // 记得前面导入 "unicode/utf8" 包 + + func FuzzReverse(f *testing.F) { + testcases := []string{"Hello, world", " ", "!12345"} + for _, tc := range testcases { + f.Add(tc) // Use f.Add to provide a seed corpus + } + f.Fuzz(func(t *testing.T, orig string) { + rev := Reverse(orig) + doubleRev := Reverse(rev) + if orig != doubleRev { + t.Errorf("Before: %q, after: %q", orig, doubleRev) + } + if utf8.ValidString(orig) && !utf8.ValidString(rev) { + t.Errorf("Reverse produced invalid UTF-8 string %q", rev) + } + }) + } + +Fuzzing 模糊测试的代码格式与单元测试很像: + +- 函数名固定以 Fuzz 开头(单元测试是以 Test 开头) +- 函数固定以 *testing.F 类型做为入参(单元测试是以*\ testing.T) + +不一样的是 Fuzzing 模糊测试,提供两个函数: + +- t.Add:用于开发者输入模糊测试的种子数据,fuzzing + 根据这些种子数据,自动随机生成更多测试数据 +- t.Fuzz:开始运行模糊测试,t.Fuzz 的入参是一个 Fuzz Target + 函数(官方这么叫的),这个 Fuzz Target + 函数的编写逻辑跟单元测试就一样了 + +在本例子中,Fuzz Target 接收 类型为 string 的入参,做为 Reverse +的输入源,然后利用两次 Reverse 的结果应与原字符串相等的原理进行测试。 + +有了 FuzzReverse 函数后,就可以使用如下命令进行模糊测试 + +.. code:: go + + go18 test -fuzz=Fuzz + +通过输出发现测试并不顺利,Go 1.18 的 Fuzzing +会将导致测试异常的数据文件记录下来,使用 cat 可以查看该测试数据 + +.. image:: https://image.iswbm.com/image-20220323214047119.png + +记录下来后,该数据就可做为普通单元测试的数据,此时我们再执行 go test +就会引用该数据,当然了,在问题解决之前, go test 会一直报错 + +.. image:: https://image.iswbm.com/image-20220323214900909.png + +3. 问题排查与解决 +----------------- + +模糊测试帮我们发现了一个出乎意料的 Bug 场景:在中文里的字符 +``泃``\ 其实是由3个字节组成的,如果按照字节反转,反转后得到的就是一个无效的字符串。 + +因此为了保证字符串反转后得到的仍然是一个有效的UTF-8编码的字符串,我们要按照\ ``rune``\ 进行字符串反转。 + +为了更好地方便大家理解中文里的字符 +``泃``\ 按照\ ``rune``\ 为维度有多少个\ ``rune``\ ,以及按照byte反转后得到的结果长什么样,我们对代码做一些修改。 + +.. image:: https://image.iswbm.com/image-20220323214536938.png + +改完之后,再次执行 go test +就会提示测试成功,说明我们已经修复上面的那个场景的 BUG + +.. image:: https://image.iswbm.com/image-20220323215108358.png + +当下我们已经发现并修复了一个 BUG,程序肯定还有更多 BUG +存在,要继续寻找可以再次进行模糊测试,重复上面的步骤即可,这里不再赘述。 + +4. 更多参数介绍 +--------------- + +在支持了 Fuzzing 模糊测试后,go test +工具也有了一些新的命令,在这里一并记录下 + +**进行模糊测试** + +:: + + go test -fuzz=Fuzz + +**只对某个函数进行模糊测试**\ :使用 -run=Fuzzxxx 或者 -fuzz=Fuzzxxx +指定模糊测试函数,避免执行到其他测试函数 + +:: + + go18 test -run=FuzzReverse + go18 test -fuzz=FuzzReverse + +**测试某个失败数据**\ :使用 -run=file 指定数据文件 + +:: + + go test -run=FuzzReverse/1fdd0160e6b3dd8f1e6b7a4179b4787e0c014cf9c46c67a863d71e3a0277c213 + +**指定模糊测试的时间**\ :使用 -fuzztime +指定模糊测试时间或者迭代次数(默认无限期),避免一直在跑测试无法退出 + +还有一个 -fuzzminimizetime +参数,看官方文档的介绍,我没明白其作用,有知道的还请评论区分享下 + +:: + + go test -fuzz=Fuzz -fuzztime 30s + +**设置模糊测试进程数据**\ :默认值是 +$GOMAXPROCS,可根据实际情况进行设置,避免太占用机器的资源 + +:: + + go test -fuzz=Fuzz -parallel 4 + +5. 写在最后 +----------- + +模糊测试的存在,并不是为了替代原单元测试,而是为单元测试提供更好的保障,是一个补充方案,而非替代方案。 + +单元测试的局限性在于,你只能用预期的输入进行测试;模糊测试在发现暴露出奇怪行为的意外输入方面非常出色。一个好的模糊测试系统也会对被测试的代码进行分析,因此它可以有效地产生输入,从而扩大代码覆盖面。 + +同时模糊测试的适用场景也比较有限,如果函数的入参并不是像本例中的那样的简单(字符串),而是各种对象呢?可能它就无能为力了吧。 diff --git a/source/c08/c08_03.md b/source/c08/c08_03.md new file mode 100644 index 0000000..f7d8ed1 --- /dev/null +++ b/source/c08/c08_03.md @@ -0,0 +1,2 @@ +# 8.3 测试技巧:网络测试 + diff --git a/source/c08/c08_03.rst b/source/c08/c08_03.rst new file mode 100644 index 0000000..abcca60 --- /dev/null +++ b/source/c08/c08_03.rst @@ -0,0 +1,2 @@ +8.3 测试技巧:网络测试 +====================== diff --git a/source/c08/c08_04.md b/source/c08/c08_04.md new file mode 100644 index 0000000..f92f346 --- /dev/null +++ b/source/c08/c08_04.md @@ -0,0 +1 @@ +# 8.4 测试技巧:性能测试 \ No newline at end of file diff --git a/source/c08/c08_04.rst b/source/c08/c08_04.rst new file mode 100644 index 0000000..32421d5 --- /dev/null +++ b/source/c08/c08_04.rst @@ -0,0 +1,2 @@ +8.4 测试技巧:性能测试 +====================== diff --git a/source/chapters/p08.rst b/source/chapters/p08.rst new file mode 100644 index 0000000..5ba597d --- /dev/null +++ b/source/chapters/p08.rst @@ -0,0 +1,17 @@ +============================= +第八章:测试技巧 +============================= + +本章节,会持续更新,敬请关注… + +------------------- + +.. toctree:: + :maxdepth: 1 + :glob: + + ../c08/* + +-------------------------------------------- + +.. image:: http://image.iswbm.com/20200607174235.png -- GitLab