1. 入门
学习新语言比较自然的方式,是使用新语言写一些你已经可以用其他语言实现的程序。本章主要通过使用Go来编写一些常见的程序(包括读写文件、格式化文本、图像处理、并发的Web客户端和服务器通信等)来快速入门Go语言,以编写这类应用作为学习一门语言的开始是一种高效的方式。
1.1 HelloWorld
package mainimport "fmt"func main() {fmt.Println("Hello World!")}//$ go run helloworld.go//output:Hello World!//$ go build helloworld.go//$ ./helloworld//output:Hello World!
go run helloworld.go,go run命令将一个或多个.go为后缀的源文件进行编译、链接,然后运行生成可执行文件。
go build将编译输出成一个可复用程序。
go get
gofmt:用来格式化指定包里的所有文件或者当前文件夹中的文件(默认),许多编辑器配置为每次在保存文件时自动运行go fmt。
1.2 命令行参数
程序的输入可以来自:文件、网络连接、其他程序的输出、键盘、命令行参数等。
os包提供了一些函数和变量,以与平台无关的方式和操作系统大交道。命令行参数以os包中的os.Args变量供程序访问,os.Args是一个字符串slice,os.Args的第一个元素Args[0]是命令本身,参数从Args[1]开始。
这里实现一个UNIX echo命令:
package mainimport ("fmt""os")func main() {s, sep := "", ""for _, arg := range os.Args[1:] {s += sep + argsep = ""}fmt.Println(s)}
如上所述,字符串是不可变对象,每次循环,+=语句都会生成一个新的字符串赋值给s,旧的字符串内容不再需要使用,会被回收。如果有大量的数据需要处理,使用+进行字符串拼接需要进行大量的内存分配和回收,这样的代价会比较大。一个简单和高效的方式是使用strings包中的Join函数:
func main() {fmt.Println(strings.Join(os.Args[1:], " "))}
func Join(s []string, sep string) string:将字符串切片中的每个元素拼接一个目标字符串,最后连接为单个字符串。在将一个数组或者slice的元素通过分隔符连接成字符串。一般的实现是通过成员+分隔符连接,然后在结果截掉最后一个分隔符,而Join函数提供了一种更高效的方式,它将传入的字符串转为切片,切片是可变对象,只需要分配一次内存,节省了很多资源和时间,所以效率高。
字符串拼接:**+**和**Join()**对比
将大小为50000的helloworld字符串切片分别用“+”以及string.Join()进行拼接,输出两种方法所用的时间。
func main() {strSlice := make([]string, 50000)for i := 0; i < 50000; i++ {strSlice = append(strSlice, "helloworld")}start1 := time.Now()var s, sep stringfor _, str := range strSlice {s += sep + strsep = " "}diff1 := time.Now().Sub(start1)start2 := time.Now()strings.Join(strSlice, " ")diff2 := time.Now().Sub(start2)fmt.Println(diff1)fmt.Println(diff2)}//output://2.3322002s//1.0004ms
1.3 找出重复行
输出标准输入中出现次数大于1的行
func main() {counts := make(map[string]int)input := bufio.NewScanner(os.Stdin)for input.Scan() {counts[input.Text()]++}for line, n := range counts {if n > 1 {fmt.Printf("%d %s\n", n, line)}}}
bufio包可以简便和高效地处理输入和输出。其中一个最有用的特性是称为扫描器(Scanner)的类型,它可以读取输入,以行或者单词为单位断开,这是处理以行为单位的输入内容的最简单方式。
input := bufio.NewScanner(os.Stdin)用来创建一个bufio.Scanner类型的input变量,扫描器从程序的标准输入进行读取。每一次调用input.Scan()读取下一行,并且将结尾的换行符去掉,在读到新行时返回true,没有更多内容的时候返回false;通过input.Text()来获取读到的内容。拓展:
从标准输入或者一个文件列表进行读取,输出出现次数大于1的行 ```go // 计数重复出现的行 func countLine(f *os.File, counts map[string]int) { input := bufio.NewScanner(f) for input.Scan() {
counts[input.Text()]++
} }
func main() { counts := make(map[string]int) files := os.Args[1:] // 文件列表 if len(files) == 0 { // 从标准输入进行读取 countLine(os.Stdin, counts) } else { // 从文件列表读取 for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, “%v\n”, err) continue } countLine(f, counts) f.Close() } } for line, n := range counts { if n > 1 { fmt.Println(n, line) } } }
该程序使用“流式”模式读取输入,然后按需拆分为行。3. **从指定文件一次读取整个输入到内存中,一次性地分割所有行,然后处理这些行**`io/ioutil`包中的`ReadFile()`函数用来读取整个命名文件的内容,返回一个可以转化成字符串的字节slice;`strings.Split()`函数,它按指定分割符将字符串分割为一个由子串组成的slice。(`Split()`是`Join()`的反操作)```gofunc main() {counts := make(map[string]int)for _, filename := range os.Args[1:] {data, err := ioutil.ReadFile(filename)if err != nil {fmt.Fprintf(os.Stderr, "%v\n", err)continue}for _, line := range strings.Split(string(data), "\n") {counts[line]++}}for line, n := range counts {if n > 1 {fmt.Println(n, line)}}}
1.4 获取一个URL
从一个指定URL获取内容,然后不加解析地输出:
func main() {for _, url := range os.Args[1:] {resp, err := http.Get(url)if err != nil {fmt.Fprintf(os.Stderr, "%v\n", err)os.Exit(1)}b, err := ioutil.ReadAll(resp.Body)resp.Body.Close()if err != nil {fmt.Fprintf(os.Stderr, "%v\n", err)os.Exit(1)}fmt.Printf("%s", b)}}
http.Get()产生一个HTTP请求,如果没有出错,返回结果存放在响应结构体resp里面,resp的Body域包含服务器端响应的一个可读取数据流,随后ioutil.ReadAll()读取整个响应结果并存入b。如果HTTP请求失败,无论出现哪种错误情况,os.Exit(1)会在进程退出时返回状态码1。
1.5 并发获取多个URL
并发获取多个URL内容,并报告每一个响应的大小和花费的时间:
func fetch(url string, ch chan<- string) {start := time.Now()resp, err := http.Get(url)if err != nil {ch <- fmt.Sprint(err) // 错误信息发送到通道return}nbytes, err := io.Copy(ioutil.Discard, resp.Body)resp.Body.Close()if err != nil {ch <- fmt.Sprintf("while reading %s: %v", url, err)return}secs := time.Since(start).Seconds()ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)}func main() {start := time.Now()ch := make(chan string)for _, url := range os.Args[1:] {go fetch(url, ch) // 启动一个goroutine}for range os.Args[1:] {fmt.Println(<-ch) // 从通道接收信息}fmt.Printf("%.2fs \n", time.Since(start).Seconds())}//$ go run main.go https://golang.org https://godoc.org// 0.84s 59834 https://golang.org// 1.56s 17461 https://godoc.org
goroutine是一个并发执行的函数,通道允许某一个协程向另一个协程传递指定类型的值的通信机制。main函数在一个goroutine中执行,然后go语言创建额外的goroutine。
io.Copy()读取响应的内容,然后通过写入ioutil.Discard输出流进行丢弃,返回读取的字节数和读取过程中的任何错误。
当一个goroutine试图向一个通道发送或者接受数据操作时,它会阻塞,直到另一个goroutine试图从该通道进行接收或发送数据时才传递值,并开始处理两个goroutine。本程序中,每一个fetch函数在通道ch上发送值,main函数进行接收。由main函数来处理所有的输出确保了每个goroutine作为一个整体单元处理,这样就避免了两个goroutine同时完成造成输出交织的情况,实现了同步。
1.6 一个Web服务器
实现一个迷你服务器,返回访问服务器的URL的路径部分。例如,如果请求的URL是http://localhost:8080/hello,响应是URL.Path = "/hello"。
package mainimport ("fmt""log""net/http")func main() {http.HandleFunc("/", handler)log.Fatal(http.ListenAndServe("localhost:8080", nil))}func handler(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)}
增加功能:实现一个迷你服务器,返回访问服务器的URL的路径部分和返回请求的数量。例如:访问http://localhost:8080/count返回到现在为止的请求个数。
package mainimport ("fmt""log""net/http""sync")var mutex sync.Mutexvar count intfunc main() {http.HandleFunc("/", handler)http.HandleFunc("/count", counter)log.Fatal(http.ListenAndServe("localhost:8080", nil))}func handler(w http.ResponseWriter, r *http.Request) {mutex.Lock()count++mutex.Unlock()fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)}func counter(w http.ResponseWriter, r *http.Request) {mutex.Lock()fmt.Fprintf(w, "请求个数为: %d\n", count)mutex.Unlock()}
·在后台,对于每个传入的请求,服务器在不同的goroutine中运行处理函数,这样可以同时处理多个请求。然而,如果两个并发的请求试图同时更新计数值count,它可能会不一致地增加,程序会产生一个严重的竞态bug。为了避免该问题,必须使用锁来确保最多只有一个goroutine在同一时间访问变量。
1.7 其他内容
Go允许使用一个简单的语句跟在if条件的后面,这在错误处理的时候特别有用:
if err := os.Open("test.txt"); err != nil {log.Print(err)}// 或者也这样写err := os.Open("test.txt")if err != nil {log.Print(err)}
但a是更推荐使用第一种方式,这种合并的语句更短而且可以缩小err变量的作用域,这是一个好的实践。
Go仅支持i++、i—这种后缀自增自减,不支持++i、—i前缀自增自减。
- Go不需要在语句或声明后面使用分号结尾,除非有多个语句或声明出现在同一行。事实上,跟在特定符号后面的换行符被转换为分号,在什么地方进行换行会影响对Go代码的解析。例如:“{”符号必须和关键字func、if等同一行,不能独自成行,并且在x + y这个表达式中,换行符可以在+操作符的后面,但是不能在+操作符的前面。
gofmt工具可以格式化指定包里的所有文件或者当前文件夹中的文件(默认),应该养成使用gofmt工具格式化自己的代码,增加可读性,也可以方便地使用各种自动化的源代码转换工具。
注:IDE可以配置为每次在保存文件时自动运行gofmt。
与for和if类似,switch可以包含一个可选的简单语句:一个短变量声明,一个递增或赋值语句,或者一个函数调用。case语句末尾不需要添加break,默认添加break,如果需要可以使用fallthrough贯穿执行。例如:
func main() {switch get() {case "a":fmt.Println("a") // 末尾默认添加breakcase "b":fmt.Println("b")fallthrough // 当匹配到case "b"时,case "c"也会执行case "c":fmt.Println("c")default:fmt.Println("default!")}}
switch语句可以不需要表达式,它就像一个case语句列表,每个case语句都是一个布尔表达式:
func Signum(x int) int {switch {case x > 0:return 1case x < 0:return -1default:return 0}}
这种形式称为无标签(tagless)选择,它等价于
switch true。通常可以用来代替多层if else,使代码更具可读性。指针:Go指针不支持算术运算,通常用来在需要修改值时,使用指针来提高效率。获取指针有两种方式,一种是使用&操作符获取一个变量的地址,另一种方式是使用new()返回一种类型的指针。
- 类型别名和类型定义:类型别名是Go 1.9版本中添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。
类型别名:
type 类型别名 = 已有类型,Go中的byte、rune、any类型都是类型别名。type byte = unit8type rune = unit32type any = interface{}
类型定义:
type 类型名 已有类型```go // 类型别名 // TypeAlias只是Type的别名,本质上两者是同一类型 type TypeAlias = Type
// 类型定义 // 类型定义是给已有类型命名,定义一种新的类型,因为结构体通常很长, // 所以它们基本都独立命名,相当于把坐标结构体类型定义为了一个Point类型 type Point struct{ X, Y int } var p Point
8. 输入和输出<a name="yBcgh"></a># 3. 基本数据Go的数据类型分为四大类:- 基础类型:数字、字符串、布尔型- 聚合类型:数组、结构体- 引用类型:指针、slice、map、函数、通道- 接口类型:**字符串**<br />字符串是不可变的字符序列,所以字符串内部的数据不允许修改。内置的len函数返回字符串的字节数(并非文字符号的数目,Go源文件总数以UTF-8编码,字符串内部实现也使用UTF-8编码),下标访问操作`s[i]`则取得第i个字节,字符串的第i个字节不一定是第i个字符。1. **子串操作**子串生成操作`s[i:j]`,内容取自原字符串的字节,因为字符串不可变,所以原字符串和子串可以安全地共用同一段底层内存,子串生成操作的开销很小。例如:<br />`s := "hello, world"`<br />`hello := s[:5]`<br />字符串可以通过比较运算符做比较,如`==`和`<`等,比较运算按字节进行。我们可以使用运算符直接判断某个字符串是否为另一个字符串的前缀:```go// 判断某个字符串是否为另一个字符串的前缀func HasPrefix(s, prefix string) bool {return len(s) >= len(prefix) && s[:len(prefix)] == prefix}// 判断是否为后缀func HasSuffix(s, suffix string) bool {return len(s) >= len(suffix) && s[len(s) - len(suffix):] == suffix}// 或者是否为子串func Contains(s, substr string) bool {for i := 0; i < len(s); i++ {if HasPrefix(s[i:], substr){return true}}return false}
以上函数取自strings包,其中Contains函数的具体实现使用了Hash让搜索更高效。
- 字符串拼接
字符串简单拼接可以使用+号,但是如果字符串较长,使用func Join(s []string, sep string) string更高效。
- 逐个处理字符
当我们需要逐个处理Unicode字符时,则必须进行相关的解码。使用unicode/utf8包中的utf8.RuneCountInString()统计字符个数:
import "unicode/utf8"s := "Hello, 世界"fmt.Println(len(s)) // 13fmt.Println(utf8.RuneCountInString(s)) // 9
GO的for...range循环也适用于字符串,它的底层实现按UTF-8对字符串进行隐式解码。
for i, r := range "Hello, 世界" {fmt.Printf("%d\t%q\n", i, r)}//i r//0 'H'//1 'e'//2 'l'//3 'l'//4 'o'//5 ','//6 ' '//7 '世'//10 '界'
返回的单个字符为字符类型rune,而不是字符串类型。字符串可以直接转为rune切片,rune切片也能转为string字符串进行输出:
func main() {s := "Hello, 世界"r := []rune(s)for i, v := range r {fmt.Printf("%d\t%v\t%c\n", i, v, v)}fmt.Println(string(r)) // "Hello, 世界"}

- 字符串字面量
字符串的值可以直接写成字符串字面量(string literal), 形式上就是带双引号的字节序列:"Hello, 世界"
原生的字符串字面量书写形式为:Hello,世界,使用反引号。原生的字符串字面量内,转义字符不起作用,实质内容和字面写法严格一致,因此,在源码中,原生的字符串字面量可以展开多行。唯一的特殊处理是回车符会被删除,换行符会保留,使得同一字符串在所有平台的值都相同。
正则表达式往往有大量反斜杠,可以方便地写成原生的字符串字面量。原生的字符串字面量也适用于HTML模板、JSON字母量、命令行提示信息,以及需要多行文本表达得场景。
- 字符串操作库函数
4个标准包对于字符串操作特别重要:bytes、strings、strconv和unicode。
strings包提供了许多函数,用于搜索、替换、比较、修整、切分与连接字符串。
bytes包也有类似的函数,用于操作字节slice,由于字符串不可变,因此按增量方式构建字符串会导致多次内存分配和复制。这种情况下,使用bytes.Buffer类型会更高效。
strconv包具备的函数,主要用来转换布尔值、整数、浮点数为与之对应的字符串形式,或者把字符串转换为布尔值、整数、浮点数,另外还有为字符串添加/,去除引号的函数。
unicode包具有判断文字符号值特性的函数,如IsDigit、IsLetter、IsUpper和IsLower等。
- 字符串可以和字节slice相互转换
为了避免转换和不必要的内存分配,bytes包和strings包都提供了许多实用函数,它们两两相对应。例如,strings包提供下面6个函数:s := "abc"b := []byte(s)s2 := string(b)
func Contains(s, substr string) boolfunc Count(s, sep string) intfunc Fields(s string) []stringfunc HasPrefix(s, prefix string) boolfunc Index(s, sep string) intfunc Join(a []string, sep string) string
bytes包里面对应的函数为:
func Contains(b, subslice []byte) boolfunc Count(b, sep []byte) intfunc Fields(s []byte) []stringfunc HasPrefix(s, prefix []byte) boolfunc Index(s, sep []byte) intfunc Join(s [][]byte, sep []byte) string
bytes包为高效处理字节slice提供了Buffer类型。Buffer起始为空,其大小随着各种类型数据的写入而增长,如string、byte和[]byte。bytes.Buffer变量无须初始化,其零值本身就有效。
func intsToString(values []int) string {var buf bytes.Bufferbuf.WriteByte('[') // 添加字节for i, v := range values {if i > 0 {buf.WriteString(", ") // 添加字符串}fmt.Fprintf(&buf, "%d", v) // 添加整数}buf.WriteByte(']')return buf.String() // 转化为字符串}func main() {fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"}
若要在bytes.Buffer变量后面添加任意文字符号的UTF-8编码,最好使用WriteRune()方法,而追加ASCII字符,则使用WriteByte()即可。
- 字符串和数字的相互转换
除了字符串和rune字符、字节之间的转换,常常也需要相互转换数值及其字符串表达形式,这些由strconv包的函数完成。
整数转为字符串:一种方式是使用
fmt.Sprintf(),另一种是使用strconv.Itoa(),Itoa():Integer to ASCII。x := 123y := fmt.Sprintf("%d", x)fmt.Println(y, strconv.Itoa(x)) // "123 123"
strconv.FormatInt()和strconv.FormatUint()可以按不同的进位制格式化数字:fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"fmt.Printf里的谓词%b、%d、%o、%x往往比Format函数方便,若有包含数字以外的附加信息,它就尤其有用:s := fmt.Printf("x=%b", x) // "x=1111011"整数字符串转为整数:
strconv.Atoi()或者strconv.ParseInt()x, err := strconv.Atoi("123") // x是整型123y, err := strconv.ParseInt("123", 10, 64) // 十进制,返回int64类型
ParseInt()的第三个参数指定结果必须匹配何种大小的类型:例如,16表示int16,而0作为特殊值表示int类型。
4. 复合数据类型
4.1 数组
4.2 切片
4.3 map
4.4 结构体
4.5 JSON
4.6 文本和HTML模板
5. 函数
本章通过一个网络爬虫示例,让我们更彻底地探究一下函数,爬虫是Web搜索引擎的组件之一,负责抓取网页并分析页面包含的链接,将链接指向的页面也抓取下来,循环往复。利用爬虫的实现,我们可以更充分地了解到Go语言的递归、匿名函数、错误处理等方面的函数特性。
5.1 函数声明
每一个函数都包含一个名字、一个形参列表、一个可选的返回列表和函数体:
func 函数名(形参列表) (返回列表) {// 函数体}
当函数返回一个未命名的返回值或者没有返回值的时候,返回列表的圆括号可以省略。返回值也可以像形参一样命名,这个时候,每一个命名的返回值会声明为一个局部变量,并根据变量类型初始化相应的0值。当函数存在返回列表时,必须显式地以return语句结束。
如果几个形参或者返回值的类型相同,那么类型只需要写一次:func f(i, j, k int, s, t string) (a, b int){}
可能偶尔能看到有些函数的声明没有函数体,那说明这个函数使用了除Go以外的语言实现。这样的声明定义了该函数的签名。package mathfunc Sin(x float64) float64 // 使用汇编语言实现
函数签名
函数的类型称作函数签名。当两个函数拥有相同的形参列表和返回列表时,认为这两个函数的类型或签名是相同的。而形参和返回值的名字不会影响到函数类型,采用简写(省略参数名字)也不会影响到函数的类型。
函数参数
Go语言没没有默认参数值的概念,也不能指定实参名,每一次调用函数都需要提供实参来对应函数的每一个形参,顺序也必须一致。
实参是按值传递的,所以函数接收到的是每一个实参的副本;修改函数的形参变量并不会影响到原来的实参。然而,如果提供的实参包含引用类型,比如指针、slice、map、函数或者通道,那么当函数使用形参变量时就有可能间接修改实参变量。5.2 递归
许多编程语言使用固定长度的函数调用栈;大小在64KB到2MB之间。递归的深度会受限于固定长度的栈大小,所以当进行深度递归调用时必须谨防栈溢出。固定长度的栈甚至会造成一定的安全隐患。相比固定长度的栈,Go语言的实现使用了可变长度的栈,栈的大小会随着使用而增长,可达到1GB左右的上限。这使得我们可以安全地使用递归而不用担心溢出问题。
5.3 多返回值
标准包内的许多函数返回两个值,一个期望得到的计算结果与一个错误值,或者一个表示函数调用是否正确的布尔值。
良好的名称可以使得返回值更加有意义,尤其在一个函数返回多个结果且类型相同时,名字的选择更加重要。但不必始终为每个返回值单独命名。比如,习惯上,最后一个布尔返回值表示成功与否,一个error结果通常都不需要特别说明。裸返回
一个函数如果有命名的返回值,可以省略return语句的操作数,这称为裸返回。
func div(a, b int) (res int, err error){if b == 0 {err = fmt.Errorf("Divide by zero")return}res = a / breturn}
裸返回是将每个命名返回结果按照顺序返回的快捷方法,在上面的函数中,每个return语句都等同于:
return res, err。在函数中存在许多返回语句且有多个返回结果时,使用裸返回可以消除重复代码,但是并不能使代码更加易于理解。比如,当b==0时,在第一眼看来,不能直观地看出第一个返回等同于return 0, err;当没有错误时,第二个返回等同于返回return res, nil。鉴于这个原因,应该保守地使用裸返回。5.4 错误
有一些函数总是成功返回的。 比如,
strings.Contains()和strconv.FormatBool()对所有可能对的参数变量都有好的定义结果,不会调用失败。对于许多其他函数,即使在高质量的代码中,也并不能保证一定能够成功返回,因为有些因素并不受程序设计者的掌控。比如任何IO操作的函数都一定会面对可能的错误,只有没有经验的程序员会认为一个简单的读写不后悔失败。事实上,这些地方是我们最需要关注的,很多可靠的操作都可能会毫无征兆地发生错误。
因此错误处理是包的API设计或者应用程序接口的重要部分,发生错误只是许多预料行为中的一种而已。
与其他许多语言不同 ,Go语言通过使用普通的值而非异常来报告错误。尽管Go有异常机制,但是Go的异常只是针对程序Bug导致的预料外的错误,而不能作为常规的错误处理方法出现在程序中。
这样做的原因是异常会陷入带有错误消息的控制流去处理它,通常会导致预期外的结果:异常会以难以理解的栈跟踪信息报告个最终用户,这些信息大都是关于程序结果方面的而不是简单明了的错误信息。
相比之下,Go程序使用通常的控制流机制,比如if和return语句应对错误。这种方式处理错误逻辑方面要求更加小心谨慎,但这恰恰是设计的要点。
如果当函数调用发生错误时返回一个附加的结果作为错误值,习惯上将错误值作为最后一个结果返回。如果错误只有一种情况,结果通常设置为布尔类型,就像下面这个查询缓存值的例子,往往都返回成功,只有不存在对应的键值对的时候返回错误:value, ok := cache.Lookup(key)if !ok {// cache[key]不存在}
更多时候,尤其对于IO操作,错误的原因可能多种多样,而调用者需要一些详细的信息,在这种情况下,错误的结果类型往往是error,error是内置的接口类型,更多关于error类型的深处含义可查阅7. 接口。
// 错误内置接口类型是常规接口// 表示错误条件,nil值表示没有错误。type error interface {Error() string}
一个错误可能是空值或者非空值,空值意味着成功而非空值意味着失败,且非空的错误类型有一个错误消息字符串,可以通过调用它的
Error()方法或者通过调用fmt.Println(err)或fmt.Printf("%v", err)直接输出错误信息:一般当函数返回一个非空错误时,该函数返回的其他结果都是不能确定的而且应该忽略。
- 然而,有一些函数在调用出错的情况下会返回部分有用的结果。比如,在读取一个文件时发生错误,调用Read函数能够返回能够成功读取的字节数与对应的错误值。正确的行为通常是在调用者处理错误前先处理这些不完整的返回结果。因此在文档中清晰地说明返回值的意义是很重要的。
错误处理策略
当一个函数调用返回一个错误时,调用者应当负责检查错误并采取合适的处理应对。根据情形,将有许多可能的处理场景。
①最常见的情形是将错误传递下去,使得在子例程中发生的错误变为主调例程的错误。
例如,在findLinks函数示例中调用http.Get()失败时,不做任何操作立即向调用者返回这个HTTP错误。
func FindLinks(url string) ([]string, error) {resp, err := http.Get(url)if err != nil {return nil, err // 错误直接返回给上一级调用者}// ...}
在某些情况下,我们需要构建一个新的错误信息,向调用者返回更加详细的关键错误信息。例如,在调用html.Parse对HTML进行解析出错时,findLinks不会直接返回HTML解析的错误信息,因为它缺少两个关键信息:解析器的出错信息与被解析文档的URL。在这种情况下,将构建一个新的错误信息:
doc, err := html.Parse(resp.Body)resp.Body.Close()if err != nil {return nil, fmt.Errorf("parsing %s as HTML: %v", url, err)}
fmt.Errorf使用fmt.Sprintf函数格式化一条错误消息并且返回一个新的错误值。我们为原始的错误信息不断添加额外的上下文信息来建立一个可读的错误描述。当错误最终被程序的main函数处理时,它应当能够提供一个从最根本问题到总体故障的清晰因果链。例如NASA的一次事故调查:genesis:crashed:no parachute:G-switch failed:bad relay orientation
因为错误消息频繁地串联起来,所以消息字符串首字母不应该大写而且应该避免换行。设计一个错误消息时应当慎重,确保每一条信息的描述都是有意义的,包含充足的相关信息,并且保持一致性,不论被同一个函数还是同一个包下面的一组函数调用返回时,这样的错误都可以保持统一的形式和错误处理方式。
比如,os包保证每一个文件操作,比如os.Open或针对打开的文件的Read、Write或Close方法返回的错误不仅包括错误信息(没有权限、路径不存在等),还包含文件的名称,因此调用者在构造错误信息的时候不需要再包含这些信息。
一般地,f(x)调用只负责报告函数的行为f和参数值x,因为它们和错误的上下文相关。调用者负责添加进一步的信息,但是f(x)本身并不会,就像上面函数中URL和html.Parse的关系。
②对于不固定或者不可预测的错误,在短暂的时间间隔后对操作进行重试是合乎情理的,超出一定的重试次数或者限定的时间后再报错退出。
// WaitForServer 尝试连接URL对应的服务器// 在一分钟内使用指数退避策略进行重试// 所有的尝试失败后返回错误func WaitForServer(url string) error {const timeout = 1 * time.Minutedeadline := time.Now().Add(timeout)for tries := 0; time.Now().Before(deadline); tries++ {_, err := http.Head(url)if err == nil {return nil // 成功}log.Printf("server not responding (%s); retrying...", err)time.Sleep(time.Second << uint(tries)) // 指数退避策略}return fmt.Errorf("server %s failed to respond after %s", url, timeout)}
③如果重试后依旧不能进行下去,调用者能够输出错误然后优雅地停止程序,但一般这样的处理应该留给主程序部分,通常库函数应当将错误传递给调用者。
// (In function main.)if err := WaitForServer(url); err != nil {fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)os.Exit(1)}
一个更加方便的方法是通过调用log.Fatalf()实现相同的效果。就和所有日志函数一样,它默认会将时间和日期作为前缀添加到错误消息的前面。
if err := WaitForServer(url); err != nil {log.Fatalf("Site is down: %v\n", err)}
2006/01/02 15:04:05 Site is down:no such domain:bad.gopl.io
④在一些错误情况下,只记录下错误信息然后程序继续运行。同样地,可以选择使用log包来增加日志的常用前缀:
if err := Ping(); err != nil {log.Printf("ping failed: %v; networking disabled", err)}
⑤在某些罕见情况下,我们可以直接安全地忽略掉整个日志:
dir, err := ioutil.TempDir("", "scratch")if err != nil {return fmt.Errof("failed to create tem dir: %v", err)}// 使用临时目录...os.RemoveAll(dir) // 忽略错误,操作系统会周期性清理临时目录
调用os.RemoveAll可能会失败,但程序忽略了这个错误,原因是操作系统会周期性地清理临时目录。在这个例子中,我们有意地抛弃了这个错误。要考虑到每一个函数调用可能发生的出错情况,当有意地忽略掉一个错误时,需要清楚地注释一下你的意图。
总结:Go语言的错误处理有特定的规律。进行错误检查之后,检测到失败的情况往往都在成功之前。如果检测到失败导致函数返回,成功的逻辑一般不会放在else块中而是在外层的作用域中。函数会有一种通常的形式,就是在开头有一连串的检查用来返回错误,之后跟着实际的函数体直到最后。
文件结束标识
通常,最终用户会对函数返回的多种错误感兴趣而不是中间涉及的程序逻辑。偶尔,一个程序必须针对不同的错误采取不同的措施。考虑要从一个文件中读取n个字节的数据。如果n是文件本身的长度,任何错误都代表操作失败。另一方面,如果调用者反复地尝试读取固定大小的块直到文件耗尽,调用者必须把读取到文件末尾的情况区别于遇到其他错误的操作。为此,io包保证任何由文件结束引起的读取错误,始终都会得到一个与众不同的错误——io.EOF,它的定义如下:
package ioimport "errors"// 当没有更多输入时,将会返回EOFvar EOF = errors.New("EOF")
调用者可以使用一个简单的比较操作来检测这种情况(下面的循环中,不断从标准输入中读取字符):
in := bufio.NewReader(os.Stdin)for {r, _, err := in.ReadRune()if err == io.EOF {break // 结束读取}if err != nil {return fmt.Errorf("read failed: %v", err)}}
对于因为文件结束而引起的错误,都会返回一个固定的错误信息io.EOF。而对于其他的io错误,我们可能需要得到更多的错误相关的本质原因和数量信息,因此一个固定的错误值并不能满足我们的需求,在7. 11节——使用类型断言来识别错误将会呈现一个更加系统的方式以区分某个错误值。
5.5 函数变量
5.6 匿名函数
5.7 变长函数
5.8 延迟函数调用
5.9 宕机
5.10 恢复
6. 方法
7. 接口
7.11 使用类型断言来识别错误
8. goroutine和通道
Go有两种并发编程风格。这一章展示goroutine和通道, 它们支持通信顺序进程(Communicating Sequential Process,CSP),CSP是一个并发模式,在不同的执行体(goroutine)之间传递值。另一种并发编程模式是使用共享内存多线程的传统模型。
8.1 goroutine
在Go里,每一个并发执行的活动称为goroutine。可以假设goroutine类似于线程,但goroutine和线程在数量上有非常大的区别。
当一个程序启动时,只有一个goroutine来调用main函数,称它为为主goroutine。新的goroutine通过go语句进行创建。语法上,一个go语句是在普通的函数或者方法调用前加上go关键字前缀,go语句使函数在新创建的goroutine中调用。
8.2 示例:并发时钟服务器
8.3 示例:并发回声服务器
8.4 通道
如果说goroutine是Go程序并发的执行体,通道就是它们之间的连接。通道是可以让一个goroutine发送特定值到另一个goroutine的通信机制。每一个通道是一个具体类型的导管,叫作通道的元素类型。一个int类型的通道写为chan int。
使用内置函数make来创建一个通道:ch := make(chan int) // ch的类型是'chan int'
像map一样,通道是一个使用make创建的数据结构的引用。当复制或者作为参数传递到一个函数时,复制的是引用,这样调用者和被调用者引用同一份数据结构。
和其他引用类型一样,通道的零值是nil。同种类型的通道可以使用==符号进行比较。当二者都是同一通道数据的引用时,比较值为true,通道也可以和nil进行比较。
通道有两个主要操作:发送(send)和接收(receive),两者统称为通信。send语句从一个goroutine传输一个值到另一个在执行接收表达式的goroutine。两个操作都使用<-操作符书写。
- 发送语句中,通道和值分别在
<-的左右两边:chan <- x - 接收语句中,通道在
<-的后边:x <- chan,在接收语句中,其结果未被使用也是合法的,即<- chan丢弃结果。
通道支持第三个操作:关闭(close),它设置一个标志位来指示值当前已经发送完毕,这个通道后面没有值了;关闭后的发送操作将导致宕机。在一个已经关闭的通道上进行接收操作,将获取所有已经发送的值,直到通道为空;这时任何接收操作会立即完成,同时获取到一个通道元素类型对应的零值。
调用内置close函数来关闭通道:close(chan)
使用简单的make调用创建的通道叫无缓冲(unbuffered)通道,但make还可以接收第二个可选参数,表示通道的容量。如果容量是0,则创建一个无缓冲通道。
ch = make(chan int) // 无缓冲通道ch = make(chan int, 0) // 无缓冲通道ch = make(chan int, 3) // 容量为3的缓冲通道
8.4.1 无缓冲通道
无缓冲通道上的发送操作将会阻塞,直到另一个goroutine在对应的通道上执行接收操作,这时值才传送完成,两个goroutine都可以继续执行。相反,如果接收操作先执行,接收方goroutine将阻塞,直到另一个goroutine在同一个通道上发送一个值。
使用无缓冲通道进行的通信导致发送和接收goroutine同步化。因此,无缓冲通道也称为同步通道。
在讨论并发的时候,当我们说x早于y发生时,不仅仅是说x发生的时间早于y,而是说保证x在y之前执行完成,执行顺序是可预期的;当x即不比y早也不比y晚时,我们说x和y并发。这并不意味着x和y一定同时发生,只说明我们不能保证它们的顺序。当多个goroutine并发地访问同一个变量的时候,有必要对这样的事件进行排序,避免程序的执行发生问题。
示例:为了让主goroutine等待后台的goroutine在完成后再退出,使用一个通道来同步两个goroutine。
func main() {ch := make(chan struct{})go func() {for i := 0; i < 10; i++ {fmt.Println(i)}time.Sleep(3 * time.Second) // 等待3秒ch <- struct{}{}}()<-ch // 等待后台goroutine完成后主goroutine才会才退出fmt.Println("主goroutine退出...")}
通过通道发送消息有两个重要的方面需要考虑。每一条消息有一个值,但有时候通信本身以及通信发生的时间也很重要。当我们强调这方面的时候,把消息叫作**事件**(event)。当事件没有携带额外的信息时,它单纯的目的是进行同步。我们通过使用一个`struct{}`元素类型的通道来强调它,尽管通常使用bool或int类型的通道来做相同的事情,因为`ch <- 1`比`ch <- struct{}{}`要短。
8.4.2 管道
通道可以用来连接goroutine,这样一个协程的输出是另一个协程的输入,这时通道叫作管道(pipeline)。下面的程序由三个goroutine组成,它们被两个通道连接起来,如下图所示:
func main() {naturals := make(chan int) // 传递自然数squares := make(chan int) // 传递平方数// counter生成自然数go func() {for x := 0; ; x++ {naturals <- x}}()// squarer对自然数求平方go func() {for {x := <-naturalssquares <- x * x}}()// printer打印结果(在主goroutine中)for {fmt.Println(<-squares)}}
在这个程序中,counter产生的自然数通过管道发送给squarer计算平方,再通过另一个管道发送给printer。
管道(Pipeline)通信是一种通信机制,它可以将前一行代码的输出传递给后一行代码作为输入,从而将原本相互独立的两行代码连接在一起。而通过不断地使用管道,最终可以将多行代码写成“流”的形式。使用管道既可以简化代码,又可以使代码间的逻辑关系更加清晰,还可以省去中间变量的输出。
9. 并发编程
10. 包和go工具
Go自带100多个包,可以为大多数应用程序提供基础。Go还有配套的go工具,一个复杂但是容易使用的命令行工具,用来管理Go包的工作空间。
10.1 引言
任何包管理系统的目的都是通过对关联的特性进行分类,组织成便于理解和修改的单元,使其与程序的其他包保持独立,从而有助于设计和维护大型的程序。模块化允许包在不同的项目中共享、复用,在组织中发布,或者在全世界范围内使用。
每个包定义了一个不同的命名空间作为它的标识符。每个名字关联一个具体的包,它让我们在为类型、函数等选取短小且清晰地名字地同时,不与程序的其他部分冲突。
包通过控制名字大小写来控制是否对包外提供可见性,提供了封装能力。限制包成员的可见性,从而隐藏API背后的辅助函数和类型,允许包的维护者修改包的实现而不影响包外部的代码。限制变量的可见性也可以隐藏变量,使用者仅可以通过导出的函数来对其进行访问和更新。
10.2 导入路径
每一个包都通过一个唯一的字符串进行标识,它称为导入路径,它们用在import声明中。Go语言规范没有定义字符串的含义或如何确定一个包的导入路径,它通过工具来解决这些问题。本章将详细讨论go工具如何理解它们。
对于准备共享或者公开的包,导入路径需要全局唯一。为了避免冲突,除了标准库中的包之外,其他包的导入路径应该以互联网域名作为路径开始。
10.3 包的声明
在每一个Go源文件的开头都需要进行包声明,主要的目的是当该包被其他包引入时作为其默认的标识符(称为包名)。例如,math/rand包中每一个文件的开头都是package rand,这样当你导入这个包时,可以访问它的成员变量,如rand.Int、rand.Float64等。
通常,包名是导入路径的最后一段,于是,导入路径不同的两个包,二者也可以拥有同样的包名。例如,两个包的导入路径分别是math/rand和crypto/rand,而包的名字都是rand。
关于“最后一段”的惯例,这里有三个例外:
- 不管包的导入路径是什么,如果该包定义一条命令(可执行的Go程序),那么它总是使用名称
main。这是告诉go build的信号,它必须调用连接器生成可执行文件。 - 目录中可能有一些文件以
_test.go结尾,包名中会出现以_test结尾。这样一个目录中有两个包:一个普通的,加上一个外部测试包。_test后缀告诉go test两个包都需要构建,并且指明文件属于哪个包。外部测试包用来避免测试所依赖的导入中包含循环依赖。 - 有一些依赖管理工具会在包导入路径的尾部追加版本号后缀,如
gopkg.in/yaml.v2。包名不包含后缀,因此这个情况下包名为yaml。10.4 导入声明
```go import “fmt” import “os”一个Go源文件可以在package声明的后面包含零个或多个import声明。每一个导入可以单独指定一条导入路径,也可以通过圆括号一次导入多个包。下面的两种形式是等价的,但第二种形式更常见。
import ( “fmt” “os” )
导入的包可以通过空行进行分组,这类分组通常表示不同领域和方面的包。导入顺序不重要,但按照惯例每一组都按照字母进行排序(gofmt和goimports工具都会自动进行分组并排序)。```goimport ("fmt""os""golang.org/x/net.html""golang.org/x/net.ipv4")
如果需要把两个名字一样的包(如math/rand和crypto/rand)导入到第三个包中,导入的声明就必须至少为其中的一个指定一个替代名字来避免冲突。这叫作**重命名导入**。
import ("crypto/rand"mrand "math/rand" // 通过指定一个不同的名称mrand来避免同名包的冲突)
替代名字仅影响当前文件。其他文件(即使是同一个包中的文件)可以使用默认名字来导入包,或者一个替代名字也可以。
重命名导入在没有冲突时也是非常有用的。如果有时导入的包名非常冗长,使用一个替代名字可能更方便,同样的缩写要一直用下去,以避免产生混淆。使用一个替代名字有助于规避常见的局部变量冲突,例如,一个文件可以包含许多以path命名的变量,我们就可以使用pathpkg这个名字导入一个标准的path包。
10.5 空导入
如果导入的包没有在文件中引用,就会产生一个编译错误。但是,有时候我们必须导入一个包,这仅仅是为了利用其副作用:对包级别的变量执行初始化表达式求值,并执行它的init函数。为了防止“未使用的导入”错误,我们必须使用_来进行空导入。import _ "image/png" // 注册png解码器,这称为空白导入。多数情况下,它用来实现一个编译时的机制,使用空白引用导入额外的包,来开启主程序中可选的特性。
database/sql包使用此机制让用户按需加入想要的数据库驱动,例如:
import ("database/sql"_ "github.com/lib/pq" // 添加Postgres支持_ "github.com/go-sql-driver/mysql" // 添加Mysql支持)
10.6 包及其命名
本节将提供一些建议,指出如何遵从Go的习惯来给包及其成员进行命名。
- 创建一个包时,使用简短的名字,但不用短到像是加密了一样。
- 尽可能保持可读性和无歧义。例如,不要把一个辅助工具包命名为util,而是使用imageutil或ioutil等更加具体和清晰的名字。
- 避免选择经常用于相关的局部变量的包名,或者迫使使用者重命名导入,例如使用以path命名的包。
- 包名通常使用统一的形式。标准包bytes、errors和strings使用复数来避免覆盖响应的预声明类型,使用go/types这种形式,来避免和关键字的冲突。
- 当为一个包成员命名时,要考虑包名和成员名这两个有意义的部分如何一起工作,而不是只考虑成员名。例如,
bytes.Equal,http.Get,json.Marshal,strings.Index。包名和成员名联合起来,含义非常清晰。 - 从上面的例子中可以识别一些通用的命名格式:将该包的类型主体作为包名,对应的操作名称作为函数名,例如:strings作为包名,关于字符串的操作直接命名,不需要再带string这个词,使用者通过
strings.Index,strings.Replacer等来引用它们。 - 其他的一些包可以描述为单一类型包,例如html/template和math/rand,这些包导出一个数据类型及其方法,通常有一个New函数用来创建该类型对应的实例。 ```go package rand // “math/rand”
type Rand struct{ / … / } func New(source Source) *Rand
- 在其他极端情况下 ,像net/http这样的包有很多的成员,但是没有很多的结构,因为它们执行复杂的任务。尽管有超过20种类型和更多的函数,但是包中最重要的成员使用最简单的命名:Get、Post、Handle、Client、Server等。<a name="GBJso"></a>## 10.7 go工具Go工具,它用来下载、查询、格式化、构建、测试以及安装Go代码包。<br />Go工具将不同种类的工具集合并为一个命名集。它是一个包管理器(类似于apt或rpm),它可以查询包的作者,计算它们的依赖关系,从远程版本控制系统下载它们。它是一个构建系统,可计算文件依赖,调用编译器、汇编器和链接器,尽管它没有UNIX make命令完备。它还是一个测试驱动程序。<br />为了让配置操作最小化,go工具非常依赖惯例。例如,给定一个Go源文件,该工具可以找到它所在的包,因为每一个目录包含一个包,并且包的导入路径对应于工作空间的目录结构。给定一个包的导入路径,该工具可以找到存放目标文件的对应目录。它也可以找到存储源代码仓库的服务器的URL。<a name="otZGD"></a>### 10.7.1 工作空间的组织大部分用户必须进行的唯一配置是GOPATH环境变量,它指定工作空间的根。当需要切换到不同的工作空间时,更新GOPATH变量的值即可。例如:```goGOPATH/src/gopl.io/.git/ch1/helloworld/main.godup/main.go...golang.org/x/net/.git/html/parse.gonode.go...bin/helloworldduppkg/...
GOPATH有三个子目录。src子目录包含源文件。每一个包放在一个目录中,该目录相对于$GOPATH/src的名字是包的导入路径,如gopl.io/ch1/helloworld。注意,一个GOPATH工作空间在src下包含多个源代码版本控制仓库,例如gopl.io或golang.org。pkg子目录是构建工具存储编译后的包的位置(编译文件存放位置,它对应于存放源文件的src目录),bin子目录放置可执行程序。
10.7.2 包的下载
go get命令可以下载单一的包,也可以使用...符号来下载子树或仓库,如果go get gopl.io/...将下载gopl.io包下的所有文件及其依赖。go get命名已经支持多个流行的代码托管站点,如GitHub、Bitbucket和Launchpad,并且可以向版本控制系统发出合适的请求。go get创建的目录是远程仓库的真实客户端,而不仅仅是文件的副本,这样可以使用版本控制命令来查看本地编辑的差异或者更新到不同的版本。例如,golang.org/x/net目录是一个Git客户端:
$ cd $GOPATH/src/golang.org/x/net$ git remote -vorigin https://go.googlesource.com/net (fetch)origin https://go.googlesource.com/net (push)
注意,包导入路径中明显的域名golang.org不同于Git服务器的实际域名go.googlesource.com。这是go工具的一个特性,如果位置由诸如googlesource.com或github.com之类的通用托管平台,包可以在其导入路径中使用自定义域名。在https://golang.org/x/net/html下面的HTML页面中包含如下元数据,它重定向go工具到实际托管地址的Git仓库:
<meta name="go-import"content="golang.org/x/net git https://go.googlesource.com/net">
如果指定指定-u参数,go get将确保它访问的所有包(包括它们的依赖性)更新到最新版本,然后再构建。如果没有这个标记,已经存在于本地的包不会更新。go get -u命令通常获取每个包的最新版本,这在刚开始的时候是很方便的;但是在需要部署的项目中(发布版本需要精准的版本控制),就不太适合使用它。通常的解决方案是加一层vendor目录,构建一个关于所有必需依赖的本地副本,然后非常小心地更新这个副本。
10.7.5 内部包
包是用来封装Go程序最重要的机制,没有导出的标识符只能在同一个包内访问,导出的标识符可以在任何地方访问。
有时,定义一个中间地带是很有帮助的,以这种方式定义的标识符可以被一个小的可信任的包集合访问,但不是所有人都能访问。例如,当我们将一个大包分解为多个小包时,我们不想对其他包暴露这些包之间的关系;或者我们想在不进行导出的情况下,在项目的一些包中间共享一些工具函数;或者我们只是想试验一个新的包,而不是永久地提交给它的API,这些都可以通过给这个包加上一个允许访问的有限客户列表来实现。
为了解决这些需求,go bulid工具会特殊对待导入路径中包含路径片段internal的情况,这些包叫做内部包。内部包只能被另一个包导入,这个包位于以internal目录的父目录为根目录的树中。例如,给定下面的包:
net/httpnet/http/internal/chunked // 内部包,只能被net/http为根目录的包导入net/http/httputilnet/url
net/http/internal/chunked可以被net/http/httputil或net/http导入,但是不能从net/url进行导入。然而,net/url可以导入net/http/httputil(普通导入)。
10.7.6 包的查询
go list工具上报可用包的信息。通过最简单的形式,go list判断一个包是否存在于工作空间中,如果存在输出它的导入路径:
$ go list github.com/go-sql-driver/mysqlgithub.com/go-sql-driver/mysql
go list命令的参数可以包含“...”通配符,可以使用它枚举一个Go工作空间中的所有包或者一个指定的子树中的所有的包:
$ go list // 枚举工作空间所有的包$ go list gopl.io/ch3/... // ,枚举一个子树下的所有包
或者枚举和一个具体的主题相关的包:go list ...xml...go list命令获取每一个包的完整元数据,而不仅仅是导入路径,并且提供各种对于用户或者其他工具可访问的格式。-json标记使go list以JSON格式输出每一个包的完整记录:
$ go list -json hash{"Dir": "/home/gopher/go/src/hash","ImportPath": "hash","Name": "hash","Doc": "Package hash provides interfaces for hash functions","Target": "/home/gopher/go/pkg/darwin_amd64/hash.a",...}
-f标记可以让用户通过text/template包提供的模板语言来定制输出格式。go list工具对于一次性的交互查询和构建、测试脚本都非常有用。
11. 测试
我强烈地意识到我余生很大一部分的时间都将用来寻找我程序中的错误。 ——1949,莫里斯·威尔克斯
今天的软件项目要庞大、复杂得多,并且在使软件复杂度可以控制的技术上面,人们投入了大量的精力。其中有两种技术尤其有效,第一是软件发布之前的例行同行评审,另一个就是测试。
测试是自动化测试的简称,即编写简单的程序来确保程序(产品代码)在该测试中针对特定输入产生预期的输出。这些测试要么是经过精心设计用来检测某种功能,要么是随机性的,用来扩大测试的覆盖面。
软件测试领域很广泛。测试任务几乎占据了所有程序员的一部分时间,有时候甚至是一些程序员所有的时间。在每一门主流的程序设计语言中,都有很多软件包专门用来构建测试,测试是程序员必须掌握的一种新技能。
Go的测试方法看上去相对比较低级。它依赖于命令go test和一些能用go test运行的测试函数的编写约定。这个相对轻量级的机制对单纯的测试很有效,并且这种方式也很自然地扩展到基准测试和文档系统的示例。
实际上,编写测试代码和编写原始程序并没有什么不同。我们聚焦于任务的部分功能的简单函数。我们必须谨防条件边界,思考数据结构,并且合理地设计如何根据合适的输入得到输出。这和编写常规的Go代码没有区别,这不需要新的注解、约定和工具。
11.1 go test工具
go test子命令是Go语言包的测试驱动程序,这些包根据某些约定组织在一起。在一个包目录中,以_test.go结尾的文件不是go build命令编译的目标,而是go test编译的目标。
在*_test.go文件中,三种函数需要特殊对待,即功能测试函数、基准测试函数和示例函数。功能测试函数是以Test前缀命名的函数,用来检测一些程序逻辑的正确性,go test运行测试函数,并且报告结果是PASS还是FAIL。基准测试函数的名称以Benchmark开头,用来测试某些操作的性能,go test汇报操作的平均执行时间。示例函数的名称以Example开头,用来提供机器检查过的文档。go test工具扫描*_test.go文件来寻找特殊函数,并生成一个临时的main包来调用它们,然后编译和运行,并汇报结果,最后清空临时文件。
11.2 Test函数
每一个测试文件必须导入testing包。这些函数的签名如下:
func TestName(t *testing.T) {// ...}
功能测试函数必须以Test开头,可选的后缀名称必须以大写字母开头,参数t提供了汇报测试失败和日志记录的功能:func TestSin(t _testing.T) { /* ._.. */}
