提交 6705c513 编写于 作者: 写代码的明哥's avatar 写代码的明哥

update

上级 610c017b
# 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
3.5 编译流程:结合 Makefile 简化编译过程
========================================
.. image:: http://image.iswbm.com/20200607145423.png
在另一篇文章中(\ `使用 -ldflags
实现动态信息注入 <https://golang.iswbm.com/c06/c06_06.html>`__\ )
我详细介绍了如何利用 -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
# 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
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``
形式的值,该值就是实现信息动态注入的核心入口。
以下面一段例子来演示
- 先定义 versionbuildTimeosArch 三个变量
- 然后将这三个变量的值打印出来
.. 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
# 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
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
泛型的写法,给我的第一印象是,函数的定义变得更复杂,可读性也越来越差,一时间我也有点难以接受。
不过经过自己试用后,情况倒没有我想象的那么糟糕!新版没有改变原有函数的定义与调用,若你没有使用泛型,那么有没有泛型对你来说没有区别。
但即使你有想法需要用到泛型,我也相信这种的不适感会在时间的流逝中慢慢淡化。
# 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
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.goTest
函数如下
- 给定了三组数据
- 遍历这几组数据,将 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.goxxx 是原文件名)
- 测试用例函数名称一般命名为 ``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
# 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
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. 写在最后
-----------
模糊测试的存在,并不是为了替代原单元测试,而是为单元测试提供更好的保障,是一个补充方案,而非替代方案。
单元测试的局限性在于,你只能用预期的输入进行测试;模糊测试在发现暴露出奇怪行为的意外输入方面非常出色。一个好的模糊测试系统也会对被测试的代码进行分析,因此它可以有效地产生输入,从而扩大代码覆盖面。
同时模糊测试的适用场景也比较有限,如果函数的入参并不是像本例中的那样的简单(字符串),而是各种对象呢?可能它就无能为力了吧。
# 8.3 测试技巧:网络测试
8.3 测试技巧:网络测试
======================
# 8.4 测试技巧:性能测试
\ No newline at end of file
8.4 测试技巧:性能测试
======================
=============================
第八章:测试技巧
=============================
本章节,会持续更新,敬请关注…
-------------------
.. toctree::
:maxdepth: 1
:glob:
../c08/*
--------------------------------------------
.. image:: http://image.iswbm.com/20200607174235.png
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册