您的位置 首页 golang

GO 编程:Golang标准命令详解(二)

go vet与go tool vet

命令 go vet 是一个用于检查Go语言源码中静态错误的简单工具。与大多数Go命令一样, go vet 命令可以接受 -n 标记和 -x 标记。 -n 标记用于打印流程中执行的命令而不真正执行它们。 -n 标记也用于打印流程中执行的命令,但不会取消这些命令的执行。示例如下:

 hc@ubt:~$ go vet -n pkgtool
/usr/local/go/pkg/tool/linux_386/vet golang/goc2p/src/pkgtool/envir.go golang/goc2p/src/pkgtool/envir_test.go golang/goc2p/src/pkgtool/fpath.go golang/goc2p/src/pkgtool/ipath.go golang/goc2p/src/pkgtool/pnode.go golang/goc2p/src/pkgtool/util.go golang/goc2p/src/pkgtool/util_test.go  

go vet 命令的参数既可以是代码包的导入路径,也可以是Go语言源码文件的绝对路径或相对路径。但是,这两种参数不能混用。也就是说, go vet 命令的参数要么是一个或多个代码包导入路径,要么是一个或多个Go语言源码文件的路径。

go vet 命令是 go tool vet 命令的简单封装。它会首先载入和分析指定的代码包,并把指定代码包中的所有Go语言源码文件和以“.s”结尾的文件的相对路径作为参数传递给 go tool vet 命令。其中,以“.s”结尾的文件是汇编语言的源码文件。如果 go vet 命令的参数是Go语言源码文件的路径,则会直接将这些参数传递给 go tool vet 命令。

如果我们直接使用 go tool vet 命令,则其参数可以传递任意目录的路径,或者任何Go语言源码文件和汇编语言源码文件的路径。路径可以是绝对的也可以是相对的。

实际上, vet 属于Go语言自带的特殊工具,也是比较底层的命令之一。Go语言自带的特殊工具的存放路径是$GOROOT/pkg/tool/$GOOS $GOARCH/,我们暂且称之为Go工具目录。我们再来复习一下,环境变量GOROOT的值即Go语言的安装目录,环境变量GOOS的值代表程序构建环境的目标操作系统的标识,而环境变量$GOARCH的值则为程序构建环境的目标计算架构。另外,名为$GOOS $GOARCH的目录被叫做平台相关目录。Go语言允许我们通过执行 go tool 命令来运行这些特殊工具。在Linux 32bit的环境下,我们的Go语言安装目录是/usr/local/go/。因此, go tool vet 命令指向的就是被存放在/usr/local/go/pkg/tool/linux_386目录下的名为 vet 的工具。

go tool vet 命令的作用是检查Go语言源代码并且报告可疑的代码编写问题。比如,在调用 Printf 函数时没有传入格式化字符串,以及某些不标准的方法签名,等等。该命令使用试探性的手法检查错误,因此并不能保证报告的问题确实需要解决。但是,它确实能够找到一些编译器没有捕捉到的错误。

go tool vet 命令程序在被执行后会首先解析标记并检查标记值。 go tool vet 命令支持的所有标记如下表。

表0-16 go tool vet 命令的标记说明

标记名称

标记描述

-all

进行全部检查。如果有其他检查标记被设置,则命令程序会将此值变为false。默认值为true。

-asmdecl

对汇编语言的源码文件进行检查。默认值为false。

-assign

检查赋值语句。默认值为false。

-atomic

检查代码中对代码包sync/atomic的使用是否正确。默认值为false。

-buildtags

检查编译标签的有效性。默认值为false。

-composites

检查复合结构实例的初始化代码。默认值为false。

-compositeWhiteList

是否使用复合结构检查的白名单。仅供测试使用。默认值为true。

-methods

检查那些拥有标准命名的方法的签名。默认值为false。

-printf

检查代码中对打印函数的使用是否正确。默认值为false。

-printfuncs

需要检查的代码中使用的打印函数的名称的列表,多个函数名称之间用英文半角逗号分隔。默认值为空字符串。

-rangeloops

检查代码中对在 range 语句块中迭代赋值的变量的使用是否正确。默认值为false。

-structtags

检查结构体类型的字段的标签的格式是否标准。默认值为false。

-unreachable

查找并报告不可到达的代码。默认值为false。

在阅读上面表格中的内容之后,读者可能对这些标签的具体作用及其对命令程序检查步骤的具体影响还很模糊。不过没关系,我们下面就会对它们进行逐一的说明。

-all标记

如果标记 -all 有效(标记值不为 false ),那么命令程序会对目标文件进行所有已知的检查。实际上,标记 -all 的默认值就是 true 。也就是说,在执行 go tool vet 命令且不加任何标记的情况下,命令程序会对目标文件进行全面的检查。但是,只要有一个另外的标记( -compositeWhiteList -printfuncs 这两个标记除外)有效,命令程序就会把标记 -all 设置为false,并只会进行与有效的标记对应的检查。

-assign标记

如果标记 -assign 有效(标记值不为 false ),则命令程序会对目标文件中的赋值语句进行自赋值操作检查。什么叫做赋值呢?简单来说,就是将一个值或者实例赋值给它本身。像这样:

 var s1 string = "S1"
s1 = s1 // 自赋值  

或者

 s1, s2 := "S1", "S2"
s2, s1 = s2, s1 // 自赋值  

检查程序会同时遍历等号两边的变量或者值。在抽象语法树的语境中,它们都被叫做表达式节点。检查程序会检查等号两边对应的表达式是否相同。判断的依据是这两个表达式节点的字符串形式是否相同。在当前的场景下,这种相同意味着它们的变量名是相同的。如前面的示例。

有两种情况是可以忽略赋值检查的。一种情况是短变量声明语句。根据Go语言的语法规则,当我们在函数中要在声明局部变量的同时对其赋值,就可以使用 := 形式的变量赋值语句。这也就意味着 := 左边的变量名称在当前的上下文环境中应该还未曾出现过(否则不能通过编译)。因此,在这种赋值语句中不可能出现自赋值的情况,忽略对它的检查也是合理的。另一种情况是等号左右两边的表达式个数不相等的变量赋值语句。如果在等号的右边是对某个函数或方法的调用,就会造成这种情况。比如:

 file, err := os.Open(wp)  

很显然,这个赋值语句肯定不是自赋值语句。因此,不需要对此种情况进行检查。如果等号右边并不是对函数或方法调用的表达式,并且等号两边的表达式数量也不相等,那么势必会在编译时引发错误,也不必检查。

-atomic标记

如果标记 -atomic 有效(标记值不为 false ),则命令程序会对目标文件中的使用代码包 sync/atomic 进行原子赋值的语句进行检查。原子赋值语句像这样:

 var i32 int32
i32 = 0
newi32 := atomic.AddInt32(&i32, 3)
fmt.Printf("i32: %d, newi32: %d.\n", i32, newi32)  

函数 AddInt32 会原子性的将变量 i32 的值加 3 ,并返回这个新值。因此上面示例的打印结果是:

 i32: 3, newi32: 3  

在代码包 sync/atomic 中,与 AddInt32 类似的函数还有 AddInt64 AddUint32 AddUint64 AddUintptr 。检查程序会对上述这些函数的使用方式进行检查。检查的关注点在破坏原子性的使用方式上。比如:

 i32 = 1
i32 = atomic.AddInt32(&i32, 3)
_, i32 = 5, atomic.AddInt32(&i32, 3)
i32, _ = atomic.AddInt32(&i32, 1), 5   

上面示例中的后三行赋值语句都属于原子赋值语句,但它们都破坏了原子赋值的原子性。以第二行的赋值语句为例,等号左边的 atomic.AddInt32(&i32, 3) 的作用是原子性的将变量 i32 的值增加 3 。但该语句又将函数的结果值赋值给变量 i32 ,这个二次赋值属于对变量 i32 的重复赋值,也使原本拥有原子性的赋值操作被拆分为了两个步骤的非原子操作。如果在对变量 i32 的第一次原子赋值和第二次非原子的重复赋值之间又有另一个程序对变量 i32 进行了原子赋值,那么当前程序中的这个第二次赋值就破坏了那两次原子赋值本应有的顺序性。因为,在另一个程序对变量 i32 进行原子赋值后,当前程序中的第二次赋值又将变量 i32 的值设置回了之前的值。这显然是不对的。所以,上面示例中的第二行代码应该改为:

 atomic.AddInt32(&i32, 3)  

并且,对第三行和第四行的代码也应该有类似的修改。检查程序如果在目标文件中查找到像上面示例的第二、三、四行那样的语句,就会打印出相应的错误信息。

另外,上面所说的导致原子性被破坏的重复赋值语句还有一些类似的形式。比如:

 i32p := &i32
*i32p = atomic.AddUint64(i32p, 1)  

这与之前的示例中的代码的含义几乎是一样。另外还有:

 var counter struct{ N uint32 }
counter.N = atomic.AddUint64(&counter.N, 1)   

 ns := []uint32{10, 20}
ns[0] = atomic.AddUint32(&ns[0], 1)
nps := []*uint32{&ns[0], &ns[1]}
*nps[0] = atomic.AddUint32(nps[0], 1)  

在最近的这两个示例中,虽然破坏原子性的重复赋值操作因结构体类型或者数组类型的介入显得并不那么直观了,但依然会被检查程序发现并及时打印错误信息。

顺便提一句,对于原子赋值语句和普通赋值语句,检查程序都会忽略掉对等号两边的表达式的个数不相等的赋值语句的检查。

-buildtags标记

前文已提到,如果标记 -buildtags 有效(标记值不为 false ),那么命令程序会对目标文件中的编译标签(如果有的话)的格式进行检查。什么叫做条件编译?在实际场景中,有些源码文件中包含了平台相关的代码。我们希望只在某些特定平台下才编译它们。这种有选择的编译方法就被叫做条件编译。在Go语言中,条件编译的配置就是通过编译标签来完成的。编译器需要依据源码文件中编译标签的内容来决定是否编译当前文件。编译标签可必须出现在任何源码文件(比如扩展名为“.go”,“.h”,“.c”,“.s”等的源码文件) 的头部的单行注释中,并且在其后面需要有空行。

至于编译标签的具体写法,我们就不在此赘述了。读者可以参看Go语言官方的相关文档。我们在这里只简单罗列一下 -buildtags 有效时命令程序对编译标签的检查内容:

  1. 若编译标签前导符“+build”后没有紧随空格,则打印格式错误信息。
  2. 若编译标签所在行与第一个多行注释或代码行之间没有空行,则打印错误信息。
  3. 若在某个单一参数的前面有两个英文叹号“!!”,则打印错误信息。
  4. 若单个参数包含字母、数字、“_”和“.”以外的字符,则打印错误信息。
  5. 若出现在文件头部单行注释中的编译标签前导符“+build”未紧随在单行注释前导符“//”之后,则打印错误信息。

如果一个在文件头部的单行注释中的编译标签通过了上述的这些检查,则说明它的格式是正确无误的。由于只有在文件头部的单行注释中编译标签才会被编译器认可,所以检查程序只会查找和检查源码文件中的第一个多行注释或代码行之前的内容。

-composites标记和-compositeWhiteList标记

如果标记 -composites 有效(标记值不为 false ),则命令程序会对目标文件中的复合字面量进行检查。请看如下示例:

 type counter struct {
    name   string
    number int
}
...
c := counter{name: "c1", number: 0}  

在上面的示例中,代码 counter{name: “c1”, number: 0} 是对结构体类型 counter 的初始化。如果复合字面量中涉及到的类型不在当前代码包内部且未在所属文件中被导入,那么检查程序不但会打印错误信息还会将退出代码设置为1,并且取消后续的检查。退出代码为1意味着检查程序已经报告了一个或多个问题。这个问题比仅仅引起错误信息报告的问题更加严重。

在通过上述检查的前提下,如果复合字面量中包含了对结构体类型的字段的赋值但却没有指明字段名,像这样:

 var v = flag.Flag{
    "Name",
    "Usage",
    nil, // Value
    "DefValue",
}  

那么检查程序也会打印错误信息,以提示在复合字面量中包含有未指明的字段赋值。

这有一个例外,那就是当标记 -compositeWhiteList 有效(标记值不为 false )的时候。只要类型在白名单中,即使其初始化语句中含有未指明的字段赋值也不会被提示。这是出于什么考虑呢?先来看下面的示例:

 type sliceType []string
...
st1 := sliceType{"1", "2", "3"}  

上面示例中的 sliceType{“1”, “2”, “3”} 也属于复合字面量。但是它初始化的类型实际上是一个切片值,只不过这个切片值被别名化并被包装为了另一个类型而已。在这种情况下,复合字面量中的赋值不需要指明字段,事实上这样的类型也不包含任何字段。白名单中所包含的类型都是这种情况。它们是在标准库中的包装了切片值的类型。它们不需要被检查,因为这种情况是合理的。

在默认情况下,标记 -compositeWhiteList 是有效的。也就是说,检查程序不会对它们的初始化代码进行检查,除非我们在执行 go tool vet 命令时显示的将 -compositeWhiteList 标记的值设置为false。

-methods标记

如果标记 -methods 有效(标记值不为 false ),则命令程序会对目标文件中的方法定义进行规范性的进行检查。这里所说的规范性是狭义的。

在检查程序内部存有一个规范化方法字典。这个字典的键用来表示方法的名称,而字典的元素则用来描述方法应有的参数和结果的类型。在该字典中列出的都是Go语言标准库中使用最广泛的接口类型的方法。这些方法的名字都非常通用。它们中的大多数都是它们所属接口类型的唯一方法。我们在第4章中提到过,Go语言中的接口类型实现方式是非侵入式的。只要结构体类型实现了某一个接口类型中的所有方法,就可以说这个结构体类型是该接口类型的一个实现。这种判断方式被称为动态接口检查。它只在运行时进行。如果我们想让一个结构体类型成为某一个接口类型的实现,但又写错了要实现的接口类型中的方法的签名,那么也不会引发编译器报错。这里所说的方法签名包括方法的参数声明列表和结果声明列表。虽然动态接口检查失败时并不会报错,但是它却会间接的引发其它错误。而这些被间接引发的错误只会在运行时发生。示例如下:

 type MySeeker struct {
    // 忽略字段定义
}

func (self *MySeeker) Seek(whence int, offset int64) (ret int64, err error) { 
    // 想实现接口类型io.Seeker中的唯一方法,但是却把参数的顺序写颠倒了。
    // 忽略实现代码
}

func NewMySeeker io.Seeker {
    return &MySeeker{/* 忽略字段初始化 */} // 这里会引发一个运行时错误。
                                           //由于MySeeker的Seek方法的签名写错了,所以MySeeker不是io.Seeker的实现。
}  

这种运行时错误看起来会比较诡异,并且错误排查也会相对困难,所以应该尽量避免。 -methods 标记所对应的检查就是为了达到这个目的。检查程序在发现目标文件中某个方法的名字被包含在规范化方法字典中但其签名与对应的描述不对应的时候,就会打印错误信息并设置退出代码为1。

我在这里附上在规范化方法字典中列出的方法的信息:

表0-17 规范化方法字典中列出的方法

方法名称

参数类型

结果类型

所属接口

唯一方法

Format

“fmt.State”, “rune”

<无>

fmt.Formatter

GobDecode

“[]byte”

“error”

gob.GobDecoder

GobEncode

<无>

“[]byte”, “error”

gob.GobEncoder

MarshalJSON

<无>

“[]byte”, “error”

json.Marshaler

Peek

“int”

“[]byte”, “error”

image.reader

ReadByte

“int”

“[]byte”, “error”

io.ByteReader

ReadFrom

“io.Reader”

“int64”, “error”

io.ReaderFrom

ReadRune

<无>

“rune”, “int”, “error”

io.RuneReader

Scan

“fmt.ScanState”, “rune”

“error”

fmt.Scanner

Seek

“int64”, “int”

“int64”, “error”

io.Seeker

UnmarshalJSON

“[]byte”

“error”

json.Unmarshaler

UnreadByte

<无>

“error”

io.ByteScanner

UnreadRune

<无>

“error”

io.RuneScanner

WriteByte

“byte”

“error”

io.ByteWriter

WriteTo

“io.Writer”

“int64”, “error”

io.WriterTo

-printf标记和-printfuncs标记

标记 -printf 旨在目标文件中检查各种打印函数使用的正确性。而标记 -printfuncs 及其值则用于明确指出需要检查的打印函数。 -printfuncs 标记的默认值为空字符串。也就是说,若不明确指出检查目标则检查所有打印函数。可被检查的打印函数如下表:

表0-18 格式化字符串中动词的格式要求

函数全小写名称

支持格式化

可自定义输出

自带换行

error

fatal

fprint

fprintln

panic

panicln

print

println

sprint

sprintln

errorf

fatalf

fprintf

panicf

printf

sprintf

以字符串格式化功能来区分,打印函数可以分为可打印格式化字符串的打印函数(以下简称格式化打印函数)和非格式化打印函数。对于格式化打印函数来说,其第一个参数必是格式化表达式,也可被称为模板字符串。而其余参数应该为需要被填入模板字符串的变量。像这样:

 fmt.Printf("Hello, %s!\n", "Harry") 
// 会输出:Hello, Harry!  

而非格式化打印函数的参数则是一个或多个要打印的内容。比如:

 fmt.Println("Hello,", "Harry!") 
// 会输出:Hello, Harry!  

以指定输出目的地功能区分,打印函数可以被分为可自定义输出目的地的的打印函数(以下简称自定义输出打印函数)和标准输出打印函数。对于自定义输出打印函数来说,其第一个函数必是其打印的输出目的地。比如:

 fmt.Fprintf(os.Stdout, "Hello, %s!\n", "Harry")
// 会在标准输出设备上输出:Hello, Harry!  

上面示例中的函数 fmt.Fprintf 既能够让我们自定义打印的输出目的地,又能够格式化字符串。此类打印函数的第一个参数的类型应为 io.Writer 接口类型。只要某个类型实现了该接口类型中的所有方法,就可以作为函数 Fprintf 的第一个参数。例如,我们还可以使用代码包 bytes 中的结构体 Buffer 来接收打印函数打印的内容。像这样:

 var buff bytes.Buffer
fmt.Fprintf(&buff, "Hello, %s!\n", "Harry")
fmt.Print("Buffer content:", buff.String())
// 会在标准输出设备上输出:Buffer content: Hello, Harry!  

而标准输出打印函数则只能将打印内容到标准输出设备上。就像函数 fmt.Printf fmt.Println 所做的那样。

检查程序会首先关注打印函数的参数数量。如果参数数量不足,则可以认为在当前调用打印函数的语句中并不会出现用法错误。所以,检查程序会忽略对它的检查。检查程序中对打印函数的最小参数是这样定义的:对于可以自定义输出的打印函数来说,最小参数数量为2,其它打印函数的最小参数数量为1。如果打印函数的实际参数数量小于对应的最小参数数量,就会被判定为参数数量不足。

对于格式化打印函数,检查程序会进行如下检查:

  1. 如果格式化字符串无法被转换为基本字面量(标识符以及用于表示int类型值、float类型值、char类型值、string类型值的字面量等),则检查程序会忽略剩余的检查。如果 -v 标记有效,则会在忽略检查前打印错误信息。另外,格式化打印函数的格式化字符串必须是字符串类型的。因此,如果对应位置上的参数的类型不是字符串类型,那么检查程序会立即打印错误信息,并设置退出代码为1。实际上,这个问题已经可以引起一个编译错误了。
  2. 如果格式化字符串中不包含动词(verbs),而格式化字符串后又有多余的参数,则检查程序会立即打印错误信息,并设置退出代码为1,且忽略后续检查。我现在举个例子。我们拿之前的一个示例作为基础,即:
  3. fmt.Printf(“Hello, %s!\n”, “Harry”)

在这个示例中,格式化字符串中的“%s”就是我们所说的动词,“%”就是动词的前导符。它相当于一个需要被填的空。一般情况下,在格式化字符串中被填的空的数量应该与后续参数的数量相同。但是可以出现在格式化字符串中没有动词并且在格式化字符串之后没有额外参数的情况。在这种情况下,该格式化打印函数就相当于一个非格式化打印函数。例如,下面这个语句会导致此步检查不通过:

 fmt.Printf("Hello!\n", "Harry")   
  1. 检查程序还会检查动词的格式。这部分检查会非常严格。检查程序对于格式化字符串中动词的格式要求如表0-19。表中对每个动词只进行了简要的说明。读者可以查看标准库代码包 fmt 的文档以了解关于它们的详细信息。命令程序会按照表5-19中的要求对格式化及其后续参数进行检查。如上表所示,这部分检查分为两步骤。第一个步骤是检查格式化字符串中的动词上是否附加了不合法的标记,第二个步骤是检查格式化字符串中的动词与后续对应的参数的类型是否匹配。只要检查出问题,检查程序就会打印出错误信息并且设置退出代码为1。
  2. 如果格式化字符串中的动词不被支持,则检查程序同样会打印错误信息后,并设置退出代码为1。

表0-19 格式化字符串中动词的格式要求

动词

合法的附加标记

允许的参数类型

简要说明

b

“ ”,“-”,“+”,“.”和“0”

int或float

用于二进制表示法。

c

“-”

rune或int

用于单个字符的Unicode表示法。

d

“ ”,“-”,“+”,“.”和“0”

int

用于十进制表示法。

e

“ ”,“-”,“+”,“.”和“0”

float

用于科学记数法。

E

“ ”,“-”,“+”,“.”和“0”

float

用于科学记数法。

f

“ ”,“-”,“+”,“.”和“0”

float

用于控制浮点数精度。

F

“ ”,“-”,“+”,“.”和“0”

float

用于控制浮点数精度。

g

“ ”,“-”,“+”,“.”和“0”

float

用于压缩浮点数输出。

G

“ ”,“-”,“+”,“.”和“0”

float

用于动态选择浮点数输出格式。

o

“ ”,“-”,“+”,“.”,“0”和“#”

int

用于八进制表示法。

p

“-”和“#”

pointer

用于表示指针地址。

q

“ ”,“-”,“+”,“.”,“0”和“#”

rune,int或string

用于生成带双引号的字符串形式的内容。

s

“ ”,“-”,“+”,“.”和“0”

rune,int或string

用于生成字符串形式的内容。

t

“-”

bool

用于生成与布尔类型对应的字符串值。(“true”或“false”)

T

“-”

任何类型

用于用Go语法表示任何值的类型。

U

“-”和“#”

rune或int

用于针对Unicode的表示法。

v

“”,“-”,“+”,“.”,“0”和“#”

任何类型

以默认格式格式化任何值。

x

“”,“-”,“+”,“.”,“0”和“#”

rune,int或string

以十六进制、全小写的形式格式化每个字节。

X

“”,“-”,“+”,“.”,“0”和“#”

rune,int或string

以十六进制、全大写的形式格式化每个字节。

对于非格式化打印函数,检查程序会进行如下检查:

  1. 如果打印函数不是可以自定义输出的打印函数,那么其第一个参数就不能是标准输出 os.Stdout 或者标准错误输出 os.Stderr 。否则,检查程序将打印错误信息并设置退出代码为1。这主要是为了防止程序编写人员的笔误。比如,他们可能会把函数 fmt.Println 当作函数 fmt.Printf 来用。
  2. 如果打印函数是不自带换行的,比如 fmt.Printf fmt.Print ,则它必须只少有一个参数。否则,检查程序将打印错误信息并设置退出代码为1。像这样的调用打印函数的语句是没有任何意义的。并且,如果这个打印函数还是一个格式化打印函数,那么这还会引起一个编译错误。需要注意的是,函数名称为 Error 的方法不会在被检查之列。比如,标准库代码包 testing 中的结构体类型 T B 的方法 Error 。这是因为它们可能实现了接口类型 Error 。这个接口类型中唯一的方法 Error 无需任何参数。
  3. 如果第一个参数的值为字符串类型的字面量且带有格式化字符串中才应该有的动词的前导符“%”,则检查程序会打印错误信息并设置退出代码为1。因为非格式化打印函数中不应该出现格式化字符串。
  4. 如果打印函数是自带换行的,那么在打印内容的末尾就不应该有换行符“\n”。否则,检查程序会打印错误信息并设置退出代码为1。换句话说,检查程序认为程序中如果出现这样的代码:
  5. fmt.Println(“Hello!\n”)

常常是由于程序编写人员的笔误。实际上,事实确实如此。如果我们确实想连续输入多个换行,应该这样写:

 fmt.Println("Hello!")
fmt.Println()  

至此,我们详细介绍了 go tool vet 命令中的检查程序对打印函数的所有步骤和内容。打印函数的功能非常简单,但是 go tool vet 命令对它的检查却很细致。从中我们可以领会到一些关于打印函数的最佳实践。

-rangeloops标记

如果标记 -rangeloop 有效(标记值不为 false ),那么命令程序会对使用 range 进行迭代的 for 代码块进行检查。我们之前提到过,使用 for 语句需要注意两点:

  1. 不要在 go 代码块中处理在迭代过程中被赋予值的迭代变量。比如:
  2. mySlice := []string{“A”, “B”, “C”} for index, value := range mySlice {
    1. go func () {
    2. fmt . Printf ( “Index: %d, Value: %s\n” , index , value )
    3. }()
  3. }

在Go语言的并发编程模型中,并没有线程的概念,但却有一个特有的概念——Goroutine。Goroutine也可被称为Go例程或简称为Go程。关于Goroutine的详细介绍在第6章和第7章。我们现在只需要知道它是一个可以被并发执行的代码块。

  1. 不要在 defer 语句的延迟函数中处理在迭代过程中被赋予值的迭代变量。比如:
  2. myDict := make(map[string]int) myDict[“A”] = 1 myDict[“B”] = 2 myDict[“C”] = 3 for key, value := range myDict {
    1. defer func () {
    2. fmt . Printf ( “Key: %s, Value: %d\n” , key , value )
    3. }()
  3. }

其实,上述两点所关注的问题是相同的,那就是不要在可能被延迟处理的代码块中直接使用迭代变量。 go 代码块和 defer 代码块都有这样的特质。这是因为等到go函数(跟在 go 关键字之后的那个函数)或延迟函数真正被执行的时候,这些迭代变量的值可能已经不是我们想要的值了。

另一方面,当检查程序发现在带有 range 子句的 for 代码块中迭代出的数据并没有赋值给标识符所代表的变量时,则会忽略对这一代码块的检查。比如像这样的代码:

 func nonIdentRange(slc []string) {
    l := len(slc)
    temp := make([]string, l)
    l--
    for _, temp[l] = range slc {
        // 忽略了使用切片值temp的代码。
        if l > 0 {
            l--
        }
    }
}  

就不会受到检查程序的关注。另外,当被迭代的对象的大小为 0 时, for 代码块也不会被检查。

据此,我们知道如果在可能被延迟处理的代码块中直接使用迭代中的临时变量,那么就可能会造成与编程人员意图不相符的结果。如果由此问题使程序的最终结果出现偏差甚至使程序报错的话,那么看起来就会非常诡异。这种隐晦的错误在排查时也是非常困难的。这种不正确的代码编写方式应该彻底被避免。这也是检查程序对迭代代码块进行检查的最终目的。如果检查程序发现了上述的不正确的代码编写方式,就会打印出错误信息以提醒编程人员。

-structtags标记

如果标记“-structtags 有效(标记值不为 false“`),那么命令程序会对结构体类型的字段的标签进行检查。我们先来看下面的代码:

 type Person struct {
    XMLName    xml.Name    `xml:"person"`
    Id            int        `xml:"id,attr"`
    FirstName    string    `xml:"name>first"`
    LastName    string    `xml:"name>last"`
    Age            int        `xml:"age"`
    Height        float32    `xml:"height,omitempty"`
    Married        bool
    Address
    Comment        string    `xml:",comment"`
}  

在上面的例子中,在结构体类型的字段声明后面的那些字符串形式的内容就是结构体类型的字段的标签。对于Go语言本身来说,结构体类型的字段标签就是注释,它们是可选的,且会被Go语言的运行时系统忽略。但是,这些标签可以通过标准库代码包 reflect 中的程序访问到。因此,不同的代码包中的程序可能会赋予这些结构体类型的字段标签以不同的含义。比如上面例子中的结构体类型的字段标签就对代码包 encoding/xml 中的程序非常有用处。

严格来讲,结构体类型的字段的标签应该满足如下要求:

  1. 标签应该包含键和值,且它们之间要用英文冒号分隔。
  2. 标签的键应该不包含空格、引号或冒号。
  3. 标签的值应该被英文双引号包含。
  4. 如果标签内容符合了第3条,那么标签的全部内容应该被反引号“`”包含。否则它需要被双引号包含。
  5. 标签可以包含多个键值对,其它们之间要用空格“ ”分隔。例如: key:”value” _gofix:”_magic”

检查程序首先会对结构体类型的字段标签的内容做去引号处理,也就是把最外面的双引号或者反引号去除。如果去除失败,则检查程序会打印错误信息并设置退出代码为1,同时忽略后续检查。如果去引号处理成功,检查程序则会根据前面的规则对标签的内容进行检查。如果检查出问题,检查程序同样会打印出错误信息并设置退出代码为1。

-unreachable标记

如果标记“-unreachable 有效(标记值不为 false“`),那么命令程序会在函数或方法定义中查找死代码。死代码就是永远不会被访问到的代码。例如:

 func deadCode1() int {
    print(1)
    return 2
    println() // 这里存在死代码
}  

在上面示例中,函数 deadCode1 中的最后一行调用打印函数的语句就是死代码。检查程序如果在函数或方法中找到死代码,则会打印错误信息以提醒编码人员。我们把这段代码放到命令源码文件deadcode_demo.go中,并在main函数中调用它。现在,如果我们编译这个命令源码文件会马上看到一个编译错误:“missing return at end of function”。显然,这个错误侧面的提醒了我们,在这个函数中存在死代码。实际上,我们在修正这个问题之前它根本就不可能被运行,所以也就不存在任何隐患。但是,如果在这个函数不需要结果的情况下又会如何呢?我们稍微改造一下上面这个函数:

 func deadCode1() {
    print(1)
    return
    println() // 这里存在死代码
}  

好了,我们现在把函数 deadcode1 的声明中的结果声明和函数中 return 语句后的数字都去掉了。不幸的是,当我们再次编译文件时没有看到任何报错。但是,这里确实存在死代码。在这种情况下,编译器并不能帮助我们找到问题,而 go tool vet 命令却可以。

 hc@ubt:~$ go tool vet deadcode_demo.go
deadcode_demo.go:10: unreachable code  

go tool vet 命令中的检查程序对于死代码的判定有几个依据,如下:

  1. 在这里,我们把 return 语句、 goto 语句、 break 语句、 continue 语句和 panic 函数调用语句都叫做流程中断语句。如果在当前函数、方法或流程控制代码块的分支中的流程中断语句的后面还存在其他语句或代码块,比如:
 func deadCode2() {
print(1)
panic(2)
println() // 这里存在死代码
}
或
func deadCode3() { L:
{
print(1)
goto L
}
println() // 这里存在死代码
}
或
func deadCode4() {
print(1)
return
{ // 这里存在死代码
}
}  

则后面的语句或代码块就会被判定为死代码。但检查程序仅会在错误提示信息中包含第一行死代码的位置。

  1. 如果带有 else if 代码块中的每一个分支的最后一条语句均为流程中断语句,则在此流程控制代码块后的代码都被判定为死代码。比如:
 func deadCode5(x int) {
print(1)
if x == 1 {
panic(2)
} else {
return
}
println() // 这里存在死代码
}  

注意,只要其中一个分支不包含流程中断语句,就不能判定后面的代码为死代码。像这样:

 func deadCode5(x int) {
    print(1)
    if x == 1 {
        panic(2)
    } else if x == 2 {
        return
    } 
    println() // 这里并不是死代码
}  
  1. 如果在一个没有显式中断条件或中断语句的 for 代码块后面还存在其它语句,则这些语句将会被判定为死代码。比如:
 func deadCode6() {
for {
for {
break
}
}
println() // 这里存在死代码
}
或  
 func deadCode7() {
    for {
        for {
        }
        break // 这里存在死代码
    }
    println()
}  

而我们对这两个函数稍加改造后,就会消除 go tool vet 命令发出的死代码告警。如下:

 func deadCode6() {
    x := 1
    for x == 1 {
        for {
            break
        }
    }
    println() // 这里存在死代码
}  

以及

 func deadCode7() {
    x := 1
    for {
        for x == 1 {
        }
        break // 这里存在死代码
    }
    println()
}  

我们只是加了一个显式的中断条件就能够使之通过死代码检查。但是,请注意!这两个函数中在被改造后仍然都包含死循环代码!这说明检查程序并不对中断条件的逻辑进行检查。

  1. 如果 select 代码块的所有 case 中的最后一条语句均为流程中断语句( break 语句除外),那么在 select 代码块后面的语句都会被判定为死代码。比如:
 func deadCode8(c chan int) {
print(1)
select {
case <-c:
print(2)
panic(3)
}
println() // 这里存在死代码
}  

 func deadCode9(c chan int) {
L:
    print(1)
    select {
    case <-c:
        print(2)
        panic(3)
    case c <- 1:
        print(4)
        goto L
    }
    println() // 这里存在死代码
}  

另外,在空的 select 语句块之后的代码也会被认为是死代码。比如:

 func deadCode10() {
    print(1)
    select {}
    println() // 这里存在死代码
}  

 func deadCode11(c chan int) {
    print(1)
    select {
    case <-c:
        print(2)
        panic(3)
    default:
        select {}
    }
    println() // 这里存在死代码
}  

上面这两个示例中的语句 select {} 都会引发一个运行时错误:“fatal error: all goroutines are asleep – deadlock!”。这就是死锁!关于这个错误的详细说明在第7章。

  1. 如果 switch 代码块的所有 case default case 中的最后一条语句均为流程中断语句(除了 break 语句),那么在 switch 代码块后面的语句都会被判定为死代码。比如:
 func deadCode14(x int) {
print(1)
switch x {
case 1:
print(2)
panic(3)
default:
return
}
println(4) // 这里存在死代码
}  

我们知道,关键字 fallthrough 可以使流程从 switch 代码块中的一个 case 转移到下一个 case default case 。死代码也可能由此产生。例如:

 func deadCode15(x int) {
    print(1)
    switch x {
    case 1:
        print(2)
        fallthrough
    default:
        return
    }
    println(3) // 这里存在死代码
}  

在上面的示例中,第一个case总会把流程转移到第二个case,而第二个case中的最后一条语句为return语句,所以流程永远不会转移到语句 println(3) 上。因此, println(3) 语句会被判定为死代码。如果我们把 fallthrough 语句去掉,那么就可以消除这个死代码判定。实际上,只要某一个 case 或者 default case 中的最后一条语句是break语句,就不会有死代码的存在。当然,这个 break 语句本身不能是死代码。另外,与 select 代码块不同的是,空的 switch 代码块并不会使它后面的代码成为死代码。

综上所述,死代码的判定虽然看似比较复杂,但其实还是有原则可循的。我们应该在编码过程中就避免编写可能会造成死代码的代码。如果我们实在不确定死代码是否存在,也可以使用 go tool vet 命令来检查。不过,需要提醒读者的是,不存在死代码并不意味着不存在造成死循环的代码。当然,造成死循环的代码也并不一定就是错误的代码。但我们仍然需要对此保持警觉。

-asmdecl标记

如果标记“-asmdecl 有效(标记值不为 false“`),那么命令程序会对汇编语言的源码文件进行检查。对汇编语言源码文件及相应编写规则的解读已经超出了本书的范围,所以我们并不在这里对此项检查进行描述。如果读者有兴趣的话,可以查看此项检查的程序的源码文件asmdecl.go。它在Go语言安装目录的子目录src/cmd/vet下。

至此,我们对 go vet 命令和 go tool vet 命令进行了全面详细的介绍。之所以花费如此大的篇幅来介绍这两个命令,不仅仅是为了介绍此命令的使用方法,更是因为此命令程序的检查工作涉及到了很多我们在编写Go语言代码时需要避免的“坑”。由此我们也可以知晓应该怎样正确的编写Go语言代码。同时,我们也应该在开发Go语言程序的过程中经常使用 go tool vet 命来检查代码。

go env

命令 go env 用于打印Go语言的环境信息。其中的一些信息我们在之前已经多次提及,但是却没有进行详细的说明。在本小节,我们会对这些信息进行深入介绍。我们先来看一看 go env 命令情况下都会打印出哪些Go语言通用环境信息。

表0-25 go env 命令可打印出的Go语言通用环境信息


名称

说明

CGO_ENABLED

指明cgo工具是否可用的标识。

GOARCH

程序构建环境的目标计算架构。

GOBIN

存放可执行文件的目录的绝对路径。

GOCHAR

程序构建环境的目标计算架构的单字符标识。

GOEXE

可执行文件的后缀。

GOHOSTARCH

程序运行环境的目标计算架构。

GOOS

程序构建环境的目标操作系统。

GOHOSTOS

程序运行环境的目标操作系统。

GOPATH

工作区目录的绝对路径。

GORACE

用于数据竞争检测的相关选项。

GOROOT

Go语言的安装目录的绝对路径。

GOTOOLDIR

Go工具目录的绝对路径。

下面我们对这些环境信息进行逐一说明。

CGO_ENABLED

通过上一小节的介绍,相信读者对cgo工具已经很熟悉了。我们提到过,标准go命令可以自动的使用cgo工具对导入了代码包C的代码包和源码文件进行处理。这里所说的“自动”并不是绝对的。因为当环境变量CGO_ENABLED被设置为0时,标准go命令就不能处理导入了代码包C的代码包和源码文件了。请看下面的示例:

 hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=0
hc@ubt:~/golang/goc2p/src/basic/cgo$ go build -x
WORK=/tmp/go-build775234613  

我们临时把环境变量CGO_ENABLED的值设置为0,然后执行 go build 命令并加入了标记 -x 。标记 -x 会让命令程序将运行期间所有实际执行的命令都打印到标准输出。但是,在执行命令之后没有任何命令被打印出来。这说明对代码包 basic/cgo 的构建操作并没有被执行。这是因为,构建这个代码包需要用到cgo工具,但cgo工具已经被禁用了。下面,我们再来运行调用了代码包 basic/cgo 中函数的命令源码文件cgo_demo.go。也就是说,命令源码文件cgo_demo.go间接的导入了代码包 C 。还记得吗?这个命令源码文件被存放在goc2p项目的代码包 basic/cgo 中。示例如下:

 hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=0
hc@ubt:~/golang/goc2p/src/basic/cgo$ go run -work cgo_demo.go
WORK=/tmp/go-build856581210
# command-line-arguments
./cgo_demo.go:4: can't find import: "basic/cgo/lib"  

在上面的示例中,我们在执行 go run 命令时加入了两个标记—— -a -work 。标记 -a 会使命令程序强行重新构建所有的代码包(包括涉及到的标准库),即使它们已经是最新的了。标记 -work 会使命令程序将临时工作目录的绝对路径打印到标准输出。命令程序输出的错误信息显示,命令程序没有找到代码包 basic/cgo 。其原因是由于代码包 basic/cgo 无法被构建。所以,命令程序在临时工作目录和工作区中都找不到代码包basic/cgo对应的归档文件cgo.a。如果我们使用命令 ll /tmp/go-build856581210 查看临时工作目录,也找不到名为basic的目录。

不过,如果我们在环境变量CGO_ENABLED的值为1的情况下生成代码包 basic/cgo 对应的归档文件cgo.a,那么无论我们之后怎样改变环境变量CGO_ENABLED的值也都可以正确的运行命令源码文件cgo_demo.go。即使我们在执行 go run 命令时加入标记 -a 也是如此。因为命令程序依然可以在工作区中找到之前在我们执行 go install 命令时生成的归档文件cgo.a。示例如下:

 hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=1
hc@ubt:~/golang/goc2p/src/basic/cgo$ go install ../basic/cgo
hc@ubt:~/golang/goc2p/src/basic/cgo$ export CGO_ENABLED=0
hc@ubt:~/golang/goc2p/src/basic/cgo$ go run -a -work cgo_demo.go
WORK=/tmp/go-build130612063
The square root of 2.330000 is 1.526434.
ABC
CFunction1() is called.
GoFunction1() is called.  

由此可知,只要我们事先成功安装了引用了代码包C的代码包,即生成了对应的代码包归档文件,即使cgo工具在之后被禁用,也不会影响到其它Go语言代码对该代码包的使用。当然,命令程序首先会到临时工作目录中寻找需要的代码包归档文件。

关于cgo工具还有一点需要特别注意,即:当存在交叉编译的情况时,cgo工具一定是不可用的。在标准go命令的上下文环境中,交叉编译意味着程序构建环境的目标计算架构的标识与程序运行环境的目标计算架构的标识不同,或者程序构建环境的目标操作系统的标识与程序运行环境的目标操作系统的标识不同。在这里,我们可以粗略认为交叉编译就是在当前的计算架构和操作系统下编译和构建Go语言代码并生成针对于其他计算架构或/和操作系统的编译结果文件和可执行文件。

GOARCH

GOARCH的值的含义是程序构建环境的目标计算架构的标识,也就是程序在构建或安装时所对应的计算架构的名称。在默认情况下,它会与程序运行环境的目标计算架构一致。即它的值会与GOHOSTARCH的值是相同。但如果我们显式的设置了环境变量GOARCH,则它的值就会是这个环境变量的值。

GOBIN

GOBIN的值为存放可执行文件的目录的绝对路径。它的值来自环境变量GOBIN。在我们使用 go tool install 命令安装命令源码文件时生成的可执行文件会存放于这个目录中。

GOCHAR

GOCHAR的值是程序构建环境的目标计算架构的单字符标识。它的值会根据GOARCH的值来设置。当GOARCH的值为386时,GOCHAR的值就是8。当GOARCH的值为amd64时GOCHAR的值就是6。而GOCHAR的值5与GOARCH的值arm相对应。

GOCHAR主要有两个用途,列举如下:

  1. Go语言官方的平台相关的工具的名称会以它的值为前缀。的名称会以GOCHAR的值为前缀。比如,在amd64计算架构下,用于编译Go语言代码的编译器的名称是6g,链接器的名称是6l。用于编译C语言代码的编译器的名称是6c。而用于编译汇编语言代码的编译器的名称为6a。
  2. Go语言的官方编译器生成的结果文件会以GOCHAR的值作为扩展名。Go语言的官方编译器6g在对命令源码文件编译之后会把结果文件 go .6存放到临时工作目录的相应位置中。

GOEXE

GOEXE的值会被作为可执行文件的后缀。它的值与GOOS的值存在一定关系,即只有GOOS的值为“windows”时GOEXE的值才会是“.exe”,否则其值就为空字符串“”。这与在各个操作系统下的可执行文件的默认后缀是一致的。

GOHOSTARCH

GOHOSTARCH的值的含义是程序运行环境的目标计算架构的标识,也就是程序在运行时所在的计算机系统的计算架构的名称。在通常情况下,它的值不需要被显式的设置。因为用来安装Go语言的二进制分发文件和MSI(Microsoft软件安装)软件包文件都是平台相关的。所以,对于不同计算架构的Go语言环境来说,它都会是一个常量。

GOHOSTOS

GOHOSTOS的值的含义是程序运行环境的目标操作系统的标识,也即程序在运行时所在的计算机系统的操作系统的名称。与GOHOSTARCH类似,它的值在不同的操作系统下是固定不变的,同样不需要显式的设置。

GOPATH

这个环境信息我们在之前已经提到过很多次。它的值指明了Go语言工作区目录的绝对路径。我们需要显式的设置环境变量GOPATH。如果有多个工作区,那么多个工作区的绝对路径之间需要用分隔符分隔。在windows操作系统下,这个分隔符为“;”。在其它操作系统下,这个分隔符为“:”。注意,GOPATH的值不能与GOROOT的值相同。

GORACE

GORACE的值包含了用于数据竞争检测的相关选项。数据竞争是在并发程序中最常见和最难调试的一类bug。数据竞争会发生在多个Goroutine争相访问相同的变量且至少有一个Goroutine中的程序在对这个变量进行写操作的情况下。

数据竞争检测需要被显式的开启。还记得标记 -race 吗?我们可以通过在执行一些标准go命令时加入这个标记来开启数据竞争检测。在这种情况下,GORACE的值就会被使用到了。支持 -race 标记的标准go命令包括: go test 命令、 go run 命令、 go build 命令和 go install 命令。

GORACE的值形如“option1=val1 option2=val2”,即:选项名称与选项值之间以等号“=”分隔,多个选项之间以空格“ ”分隔。数据竞争检测的选项包括log_path、exitcode、strip_path_prefix和history_size。为了设置GORACE的值,我们需要设置环境变量GORACE。或者,我们也可以在执行go命令时临时设置它,像这样:

 hc@ubt:~/golang/goc2p/src/cnet/ctcp$ GORACE="log_path=/home/hc/golang/goc2p /race/report strip_path_prefix=home/hc/golang/goc2p/" go test -race  

关于数据竞争检测的更多细节我们将会在本书的第三部分予以说明。

GOROOT

GOROOT会是我们在安装Go语言时第一个碰到Go语言环境变量。它的值指明了Go语言的安装目录的绝对路径。但是,只有在非默认情况下我们才需要显式的设置环境变量GOROOT。这里所说的默认情况是指:在Windows操作系统下我们把Go语言安装到c:\Go目录下,或者在其它操作系统下我们把Go语言安装到/usr/local/go目录下。另外,当我们不是通过二进制分发包来安装Go语言的时候,也不需要设置环境变量GOROOT的值。比如,在Windows操作系统下,我们可以使用MSI软件包文件来安装Go语言。

GOTOOLDIR

GOTOOLDIR的值指明了Go工具目录的绝对路径。根据GOROOT、GOHOSTOS和GOHOSTARCH来设置。其值为$GOROOT/pkg/tool/$GOOS_$GOARCH。关于这个目录,我们在之前也提到过多次。

除了上面介绍的这些通用的Go语言环境信息,还两个针对于非Plan 9操作系统的环境信息。它们是CC和GOGCCFLAGS。环境信息CC的值是操作系统默认的C语言编译器的命令名称。环境信息GOGCCFLAGS的值则是Go语言在使用操作系统的默认C语言编译器对C语言代码进行编译时加入的参数。

如果我们要有针对性的查看上述的一个或多个环境信息,可以在 go env 命令的后面加入它们的名字并执行之。在 go env 命令和环境信息名称之间需要用空格分隔,多个环境信息名称之间也需要用空格分隔。示例如下:

 hc@ubt:~$ go env GOARCH GOCHAR
386
8  

上例的 go env 命令的输出信息中,每一行对一个环境信息的值,且其顺序与我们输入的环境信息名称的顺序一致。比如,386为环境信息GOARCH,而8则是环境信息GOCHAR的值。

go env 命令能够让我们对当前的Go语言环境进行简要的了解。通过它,我们也可以对当前安装的Go语言的环境设置进行简单的检查。

文章来源:智云一二三科技

文章标题:GO 编程:Golang标准命令详解(二)

文章地址:https://www.zhihuclub.com/86361.shtml

关于作者: 智云科技

热门文章

网站地图