目录
2.1变量
2.1.1变量声明
2.1.2 变量初始化
2.1.3 变量赋值
2.1.4 匿名变量
2.2 常量
2.2.1 字面常量
2.2.2 常量定义
2.2.3 预定义常量
2.2.4 枚举
2.3 类型
2.3.1 布尔类型
2.3.2 整型
1. 类型表示
2. 数值运算
3. 比较运算
4. 位运算
2.3.3 浮点型
1. 浮点数表示
2. 浮点数比较
2.3.4 复数类型
1. 复数表示
2. 实部与虚部
2.3.5 字符串
1. 字符串操作
2. 字符串遍历
2.3.6 字符类型
2.3.7 数组
1. 元素访问
2. 值类型
2.3.8 数组切片
1. 创建数组切片
2. 元素遍历
3. 动态增减元素
4. 基于数组切片创建数组切片
5. 内容复制
2.3.9 map
1. 变量声明
2. 创建
3. 元素赋值
4. 元素删除
5. 元素查找
2.4 流程控制
2.4.1 条件语句
2.4.2 选择语句
2.4.3 循环语句
2.4.4 跳转语句
2.5 函数
2.5.1 函数定义
2.5.2 函数调用
2.5.3 不定参数
1. 不定参数类型
2. 不定参数的传递
3. 任意类型的不定参数
2.5.4 多返回值
2.5.5 匿名函数与闭包
1. 匿名函数
2. 闭包
2.6 错误处理
2.6.1 error接口
2.6.2 defer
2.6.3 panic()和recover()
2.7 完整示例
2.7.1 程序结构
2.7.2 主程序
1. 命令行参数
2. 读取输入文件
3. 写到输出文件
2.7.3 算法实现
1. 冒泡排序
2. 快速排序
2.7.4 主程序
2.7.5 构建与执行
2.8 小结
变量是几乎所有编程语言的组成元素,从根本上来说,变量相当于是对一块数据储存控件的命名,程序可以通过定义一个变量来申请一块数据储存控件,之后可以通过医用变量名来使用这块储存空间。
GO语言中的变量使用方式语C语言接近,但具备更大的灵活性。
GO语言的变量声明方式与C和C++语言有明显的不同。对于纯粹的变量声明,GO语言引入了关键字var,而类型信息放在变量名之后,
var v1 int
var v2 string
var v3 [10]int // 数组
var v4 []int // 数组切片
var v5 struct {
f int
}
var v6 *int // 指针
var v7 map[string]int // map,key为string类型,value为int类型
var v8 func(a int) int
变量声明语句不需要使用分号作为结束符。与C语言相比,Go语言摒弃了语句必须以分号作 为语句结束标记的习惯。
var关键字的另一种用法是可以将若干个需要声明的变量放置在一起,免得程序员需要重复 写var关键字,如下所示:
var (
v1 int
v2 string
)
对于声明变量时需要进行初始化的场景,var关键字可以保留,但不再是必要的元素,如下 所示:
var v1 int = 10 // 正确的使用方式1
var v2 = 10 // 正确的使用方式2,编译器可以自动推导出v2的类型
v3 := 10 // 正确的使用方式3,编译器可以自动推导出v3的类型
以上三种用法的效果是完全一样的。与第一种用法相比,第三种用法需要输入的字符数大大 减少,是懒程序员和聪明程序员的最佳选择。这里Go语言也引入了另一个C和C++中没有的符号 (冒号和等号的组合:=),用于明确表达同时进行变量声明和初始化的工作。
指定类型已不再是必需的,Go编译器可以从初始化表达式的右值推导出该变量应该声明为 哪种类型,这让Go语言看起来有点像动态类型语言,尽管Go语言实际上是不折不扣的强类型语 言(静态类型语言)
当然,出现在:=左侧的变量不应该是已经被声明过的,否则会导致编译错误,比如下面这个 写法:
var i int
i := 2
会导致类似如下的编译错误:
no new variables on left side of :=
在Go语法中,变量初始化和变量赋值是两个不同的概念。下面为声明一个变量之后的赋值 过程:
var v10 int
v10 = 123
Go语言的变量赋值与多数语言一致,但Go语言中提供了C/C++程序员期盼多年的多重赋值功 能,比如下面这个交换i和j变量的语句:
i, j = j, i
在不支持多重赋值的语言中,交互两个变量的内容需要引入一个中间变量:
t = i; i = j; j = t;
多重赋值的特性在Go语言库的实现中也被使用得相当充分,在介绍函数的多重返回值时, 将对其进行更加深入的介绍。总而言之,多重赋值功能让Go语言与C/C++语言相比可以非常明显 地减少代码行数。
我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个 值,却因为该函数返回多个值而不得不定义一堆没用的变量。在Go中这种情况可以通过结合使 用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。
假 设GetName()函数的定义如下,它返回3个值,分别为firstName、lastName和 nickName:
func GetName() (firstName, lastName, nickName string) {
return "May", "Chan", "Chibi Maruko"
}
若只想获得nickName,则函数调用语句可以用如下方式编写:
_, _, nickName := GetName()
这种用法可以让代码非常清晰,基本上屏蔽掉了可能混淆代码阅读者视线的内容,从而大幅 降低沟通的复杂度和代码维护的难度。
在Go语言中,常量是指编译期间就已知且不可改变的值。常量可以是数值类型(包括整型、 浮点型和复数类型)、布尔类型、字符串类型等。
所谓字面常量(literal),是指程序中硬编码的常量,如:
-12
3.14159265358979323846 // 浮点类型的常量
3.2+12i // 复数类型的常量
true // 布尔类型的常量
"foo" // 字符串常量
在其他语言中,常量通常有特定的类型,比如12在C语言中会认为是一个int类型的常量。 如果要指定一个值为12的long类型常量,需要写成12l,这有点违反人们的直观感觉。Go语言 的字面常量更接近我们自然语言中的常量概念,它是无类型的。只要这个常量在相应类型的值域 范围内,就可以作为该类型的常量,比如上面的常量12,它可以赋值给int、uint、int32、 int64、float32、float64、complex64、complex128等类型的变量。
通过const关键字,你可以给字面常量指定一个友好的名字:
const Pi float64 = 3.14159265358979323846
const zero = 0.0 // 无类型浮点常量
const (
size int64 = 1024
eof = -1 // 无类型整型常量
)
const u, v float32 = 0, 3 // u = 0.0, v = 3.0,常量的多重赋值
const a, b, c = 3, 4, "foo"
// a = 3, b = 4, c = "foo", 无类型整型和字符串常量
Go的常量定义可以限定常量类型,但不是必需的。如果定义常量时没有指定类型,那么它 与字面常量一样,是无类型常量。
常量定义的右值也可以是一个在编译期运算的常量表达式,比如
const mask = 1 << 3
由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达 式,比如试图以如下方式定义常量就会导致编译错误:
const Home = os.GetEnv("HOME")
原因很简单,os.GetEnv()只有在运行期才能知道返回结果,在编译期并不能确定,所以 无法作为常量定义的右值。
Go语言预定义了这些常量:true、false和iota。
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被 重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1。
从以下的例子可以基本理解iota的用法:
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
const (
a = 1 << iota // a == 1 (iota在每个const开头被重设为0)
b = 1 << iota // b == 2
c = 1 << iota // c == 4
)
const (
u = iota * 42 // u == 0
v float64 = iota * 42 // v == 42.0
w = iota * 42 // w == 84
)
const x = iota // x == 0 (因为iota又被重设为0了)
const y = iota // y == 0 (同上)
如果两个const的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。因此,上 面的前两个const语句可简写为:
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 // c1 == 1
c2 // c2 == 2
)
const (
a = 1 <
枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们 看到可以用在const后跟一对圆括号的方式定义一组常量,这种定义法在Go语言中通常用于定义 枚举值。Go语言并不支持众多其他语言明确支持的enum关键字。
下面是一个常规的枚举表示法,其中定义了一系列整型常量:
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays // 这个常量没有导出
)
同Go语言的其他符号(symbol)一样,以大写字母开头的常量在包外可见。
以上例子中numberOfDays为包内私有,其他符号则可被其他包访问。
Go语言内置以下这些基础类型:
关于错误类型,我们会在“错误处理”一节中介绍;关于通道,我们会在4.5节中进一步介 绍;关于结构体和接口,我们则在第3章中进行详细的阐述。
在这些基础类型之上Go还封装了下面这几种类型:int、uint和uintptr等。这些类型的 特点在于使用方便,但使用者不能对这些类型的长度做任何假设。对于常规的开发来说,用int 和uint就可以了,没必要用int8之类明确指定长度的类型,以免导致移植困难。
Go语言中的布尔类型与其他语言基本一致,关键字也为bool,可赋值为预定义的true和 false示例代码如下:
var v1 bool
v1 = true
v2 := (1 == 2) // v2也会被推导为bool类型
布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。以下的示例是一些错误 的用法,会导致编译错误:
var b bool
b = 1 // 编译错误
b = bool(1) // 编译错误
以下的用法才是正确的:
var b bool
b = (1!=0) // 编译正确
fmt.Println("Result:", b) // 打印结果为Result: true
整型是所有编程语言里最基础的数据类型。Go语言支持表2-1所示的这些整型类型。
类型 | 长度(字节) | 值范围 |
int8 | 1 | -128 ~ 127 |
uint8(即byte) | 1 | 0 ~ 255 |
int16 | 2 | 32 768 ~ 32 767 |
uint16 | 2 | 0 ~ 65 535 |
int32 | 3 | -2 147 483 648 ~ 2 147 483 647 |
uint32 | 3 | 0 ~ 4 294 967 295 |
int64 | 8 | 9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807 |
uint64 | 8 | 0~ 18 446 744 073 709 551 615 |
int | 平台相关 | 平台相关 |
uint | 平台相关 | 平台相关 |
uintptr | 同指针 | 在32位平台下为4字节,64位平台下为8字节 |
需要注意的是,int和int32在Go语言里被认为是两种不同的类型,编译器也不会帮你自动 做类型转换,比如以下的例子会有编译错误:
var value2 int32
value1 := 64 // value1将会被自动推导为int类型
value2 = value1 // 编译错误
编译错误类似于:
cannot use value1 (type int) as type int32 in assignment。
使用强制类型转换可以解决这个编译错误:
value2 = int32(value1) // 编译通过
当然,开发者在做强制类型转换时,需要注意数据长度被截短而发生的数据精度损失(比如 将浮点数强制转为整数)和值溢出(值超过转换的目标类型的值范围时)问题。
Go语言支持下面的常规整数运算:+、、*、/和%。加减乘除就不详细解释了,需要说下的 是,% 和在C语言中一样是求余运算,比如:
5 % 3 // 结果为:2
Go语言支持以下的几种比较运算符:>、=、<=和!=。这一点与大多数其他语言相 同,与C语言完全一致。
下面为条件判断语句的例子:
i, j := 1, 2
if i == j {
fmt.Println("i and j are equal.")
}
两个不同类型的整型数不能直接比较,比如int8类型的数和int类型的数不能直接比较,但 各种类型的整型变量都可以直接与字面常量(literal)进行比较,比如:
var i int32
var j int64
i, j = 1, 2
if i == j { // 编译错误
fmt.Println("i and j are equal.")
}
if i == 1 || j == 2 { // 编译通过
fmt.Println("i and j are equal.")
}
Go语言支持表2-2所示的位运算符。
运算 | 含义 | 样例 |
x << y | 左移 | 124 << 2 // 结果为496 |
x >> y | 右移 | 124 >> 2 // 结果为31 |
x ^ y | 异或 | 124 ^ 2 // 结果为126 |
x & y | 与 | 124 & 2 // 结果为0 |
x | y | 或 | 124 | 2 // 结果为126 |
^x | 取反 | ^2 // 结果为-3 |
Go语言的大多数位运算符与C语言都比较类似,除了取反在C语言中是~x,而在Go语言中 是^x。
浮点型用于表示包含小数点的数据,比如1.234就是一个浮点型数据。Go语言中的浮点类型 采用IEEE-754标准的表达方式。
Go语言定义了两个类型float32和float64,其中float32等价于C语言的float类型, float64等价于C语言的double类型。
在Go语言里,定义一个浮点数变量的代码如下:
var fvalue1 float32
fvalue1 = 12
fvalue2 := 12.0 // 如果不加小数点,fvalue2会被推导为整型而不是浮点型
对于以上例子中类型被自动推导的fvalue2,需要注意的是其类型将被自动设为float64, 而不管赋给它的数字是否是用32位长度表示的。因此,对于以上的例子,下面的赋值将导致编译 错误:
fvalue1 = fvalue2
而必须使用这样的强制类型转换:
fvalue1 = float32(fvalue2)
因为浮点数不是一种精确的表达方式,所以像整型那样直接用==来判断两个浮点数是否相等 是不可行的,这可能会导致不稳定的结果。
下面是一种推荐的替代方案:
import "math"
// p为用户自定义的比较精度,比如0.00001
func IsEqual(f1, f2, p float64) bool {
return math.Fdim(f1, f2) < p
}
复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部(real),一个表示 虚部(imag)。如果了解了数学上的复数是怎么回事,那么Go语言的复数就非常容易理解了。
复数表示的示例如下:
var value1 complex64 // 由2个float32构成的复数类型
value1 = 3.2 + 12i
value2 := 3.2 + 12i // value2是complex128类型
value3 := complex(3.2, 12) // value3结果同 value2
对于一个复数z = complex(x, y),就可以通过Go语言内置函数real(z)获得该复数的实 部,也就是x,通过imag(z)获得该复数的虚部,也就是y。
更多关于复数的函数,请查阅math/cmplx标准库的文档。
在Go语言中,字符串也是一种基本类型。相比之下, C/C++语言中并不存在原生的字符串 类型,通常使用字符数组来表示,并以字符指针来传递。
Go语言中字符串的声明和初始化非常简单,举例如下:
var str string // 声明一个字符串变量
str = "Hello world" // 字符串赋值
ch := str[0] // 取字符串的第一个字符
fmt.Printf("The length of \"%s\" is %d \n", str, len(str))
fmt.Printf("The first character of \"%s\" is %c.\n", str, ch)
输出结果为:
The length of "Hello world" is 11
The first character of "Hello world" is H.
字符串的内容可以用类似于数组下标的方式获取,但与数组不同,字符串的内容不能在初始 化后被修改,比如以下的例子:
str := "Hello world" // 字符串也支持声明时进行初始化的做法
str[0] = 'X' // 编译错误
编译器会报类似如下的错误:
cannot assign to str[0]
在这个例子中我们使用了一个Go语言内置的函数len()来取字符串的长度。这个函数非常有 用,我们在实际开发过程中处理字符串、数组和切片时将会经常用到。
本节中我们还顺便示范了Printf()函数的用法。有C语言基础的读者会发现,Printf()函 数的用法与C语言运行库中的printf()函数如出一辙。读者在以后学习更多的Go语言特性时, 可以配合使用Println()和Printf()来打印各种自己感兴趣的信息,从而让学习过程更加直 观、有趣。
Go编译器支持UTF-8的源代码文件格式。这意味着源代码中的字符串可以包含非ANSI的字 符,比如“Hello world. 你好,世界!”可以出现在Go代码中。但需要注意的是,如果你的Go代 码需要包含非ANSI字符,保存源文件时请注意编码格式必须选择UTF-8。特别是在Windows下一 般编辑器都默认存为本地编码,比如中国地区可能是GBK编码而不是UTF-8,如果没注意这点在 编译和运行时就会出现一些意料之外的情况。
字符串的编码转换是处理文本文档(比如TXT、XML、HTML等)非常常见的需求,不过可 惜的是Go语言仅支持UTF-8和Unicode编码。对于其他编码,Go语言标准库并没有内置的编码转 换支持。不过,所幸的是我们可以很容易基于iconv库用Cgo包装一个。这里有一个开源项目: https://github.com/xushiwei/go-iconv。
平时常用的字符串操作如表2-3所示。
运算 | 含义 | 样例 |
x + y | 字符串连接 | "Hello" + "123" // 结果为Hello123 |
len(s) | 字符串长度 | len("Hello") // 结果为5 |
s[i] | 取字符 | "Hello" [1] // 结果为'e' |
更多的字符串操作,请参考标准库strings包。
Go语言支持两种方式遍历字符串。一种是以字节数组的方式遍历:
str := "Hello,世界"
n := len(str)
for i := 0; i < n; i++ {
ch := str[i] // 依据下标取字符串中的字符,类型为byte
fmt.Println(i, ch)
}
这个例子的输出结果为:
0 72
1 101
2 108
3 108
4 111
5 44
6 32
7 228
8 184
9 150
10 231
11 149
12 140
可以看出,这个字符串长度为13。尽管从直观上来说,这个字符串应该只有9个字符。这是 因为每个中文字符在UTF-8中占3个字节,而不是1个字节。
另一种是以Unicode字符遍历:
str := "Hello,世界"
for i, ch := range str {
fmt.Println(i, ch)//ch的类型为rune
}
输出结果为:
0 72
1 101
2 108
3 108
4 111
5 44
6 32
7 19990
10 30028
以Unicode字符方式遍历时,每个字符的类型是rune(早期的Go语言用int类型表示Unicode 字符),而不是byte。
在Go语言中支持两个字符类型,一个是byte(实际上是uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune,代表单个Unicode字符。
关于rune相关的操作,可查阅Go标准库的unicode包。另外unicode/utf8包也提供了 UTF8和Unicode之间的转换。
出于简化语言的考虑,Go语言的多数API都假设字符串为UTF-8编码。尽管Unicode字符在标 准库中有支持,但实际上较少使用。
数组是Go语言编程中最常用的数据结构之一。顾名思义,数组就是指一系列同一类型数据 的集合。数组中包含的每个数据被称为数组元素(element),一个数组包含的元素个数被称为数 组的长度。
以下为一些常规的数组声明方法:
[32]byte // 长度为32的数组,每个元素为一个字节
[2*N] struct { x, y int32 } // 复杂类型数组
[1000]*float64 // 指针数组
[3][5]int // 二维数组
[2][2][2]float64 // 等同于[2]([2]([2]float64))
从以上类型也可以看出,数组可以是多维的,比如[3][5]int就表达了一个3行5列的二维整 型数组,总共可以存放15个整型元素。
在Go语言中,数组长度在定义后就不可更改,在声明时长度可以为一个常量或者一个常量 表达式(常量表达式是指在编译期即可计算结果的表达式)。数组的长度是该数组类型的一个内 置常量,可以用Go语言的内置函数len()来获取。下面是一个获取数组arr元素个数的写法:
arrLength := len(arr)
可以使用数组下标来访问数组中的元素。与C语言相同,数组下标从0开始,len(array)-1 则表示最后一个元素的下标。下面的示例遍历整型数组并逐个打印元素内容:
for i := 0; i < len(array); i++ {
fmt.Println("Element", i, "of array is", array[i])
}
Go语言还提供了一个关键字range,用于便捷地遍历容器中的元素。当然,数组也是range 的支持范围。上面的遍历过程可以简化为如下的写法:
for i, v := range array {
fmt.Println("Array element[", i, "]=", v)
}
在上面的例子里可以看到,range具有两个返回值,第一个返回值是元素的数组下标,第二 个返回值是元素的值。
需要特别注意的是,在Go语言中数组是一个值类型(value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该 参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所 传入数组的一个副本。
下面用例子来说明这一特点:
package main
import "fmt"
func modify(array [10]int) {
array[0] = 10 // 试图修改数组的第一个元素
fmt.Println("In modify(), array values:", array)
}
func main() {
array := [5]int{1,2,3,4,5} // 定义并初始化一个数组
modify(array) // 传递给一个函数,并试图在函数体内修改这个数组内容
fmt.Println("In main(), array values:", array)
}
该程序的执行结果为:
In modify(), array values: [10 2 3 4 5]
In main(), array values: [1 2 3 4 5]
从执行结果可以看出,函数modify()内操作的那个数组跟main()中传入的数组是两个不同的实 例。那么,如何才能在函数内操作外部的数据结构呢?我们将在2.3.6节中详细介绍如何用数组切 片功能来达成这个目标。
在前一节里我们已经提过数组的特点:数组的长度在定义之后无法再次修改;数组是值类型, 每次传递都将产生一份副本。显然这种数据结构无法完全满足开发者的真实需求。
不用失望,Go语言提供了数组切片(slice)这个非常酷的功能来弥补数组的不足。
初看起来,数组切片就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是 个指针。数组切片的数据结构可以抽象为以下3个变量:
从底层实现的角度来看,数组切片实际上仍然使用数组来管理元素,因此它们之间的关系让 C++程序员们很容易联想起STL中std::vector和数组的关系。基于数组,数组切片添加了一系 列管理功能,可以随时动态扩充存放空间,并且可以被随意传递而不会导致所管理的元素被重复 复制。
创建数组切片的方法主要有两种——基于数组和直接创建,下面我们来简要介绍一下这两种 方法。
数组切片可以基于一个已存在的数组创建。数组切片可以只使用数组的一部分元素或者整个 数组来创建,甚至可以创建一个比所基于的数组还要大的数组切片。代码清单2-1演示了如何基 于一个数组的前5个元素创建一个数组切片。
package main
import "fmt"
func main() {
// 先定义一个数组
var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 基于数组创建一个数组切片
var mySlice []int = myArray[:5]
fmt.Println("Elements of myArray: ")
for _, v := range myArray {
fmt.Print(v, " ")
}
fmt.Println("\nElements of mySlice: ")
for _, v := range mySlice {
fmt.Print(v, " ")
}
fmt.Println()
}
运行结果为:
Elements of myArray:
1 2 3 4 5 6 7 8 9 10
Elements of mySlice:
1 2 3 4 5
读者应该已经注意到,Go语言支持用myArray[first:last]这样的方式来基于数组生成一 个数组切片,而且这个用法还很灵活,比如下面几种都是合法的。
基于myArray的所有元素创建数组切片:
mySlice = myArray[:]
基于myArray的前5个元素创建数组切片:
mySlice = myArray[:5]
基于从第5个元素开始的所有元素创建数组切片:
mySlice = myArray[5:]
并非一定要事先准备一个数组才能创建数组切片。Go语言提供的内置函数make()可以用于 灵活地创建数组切片。下面的例子示范了直接创建数组切片的各种方法。
创建一个初始元素个数为5的数组切片,元素初始值为0:
mySlice1 := make([]int, 5)
创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:
mySlice2 := make([]int, 5, 10)
直接创建并初始化包含5个元素的数组切片:
mySlice3 := []int{1, 2, 3, 4, 5}
当然,事实上还会有一个匿名数组被创建出来,只是不需要我们来操心而已。
操作数组元素的所有方法都适用于数组切片,比如数组切片也可以按下标读写元素,用len() 函数获取元素个数,并支持使用range关键字来快速遍历所有元素。
传统的元素遍历方法如下:
for i := 0; i
使用range关键字可以让遍历代码显得更整洁。range表达式有两个返回值,第一个是索引, 第二个是元素的值:
for i, v := range mySlice {
fmt.Println("mySlice[", i, "] =", v)
}
对比上面的两个方法,我们可以很容易地看出使用range的代码更简单易懂。
可动态增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能 力(capacity)的概念,即元素个数和分配的空间可以是两个不同的值。合理地设置存储能力的 值,可以大幅降低数组切片内部重新分配内存和搬送内存块的频率,从而大大提高程序性能。
假如你明确知道当前创建的数组切片最多可能需要存储的元素个数为50,那么如果你设置的 存储能力小于50,比如20,那么在元素超过20时,底层将会发生至少一次这样的动作——重新分 配一块“够大”的内存,并且需要把内容从原来的内存块复制到新分配的内存块,这会产生比较 明显的开销。给“够大”这两个字加上引号的原因是系统并不知道多大才是够大,所以只是一个 简单的猜测。比如,将原有的内存空间扩大两倍,但两倍并不一定够,所以之前提到的内存重新 分配和内容复制的过程很有可能发生多次,从而明显降低系统的整体性能。但如果你知道最大是 50并且一开始就设置存储能力为50,那么之后就不会发生这样非常耗费CPU的动作,从而达到空间换时间的效果。
数组切片支持Go语言内置的cap()函数和len()函数,代码清单2-2简单示范了这两个内置 函数的用法。可以看出,cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是 数组切片中当前所存储的元素个数。
package main
import "fmt"
func main() {
mySlice := make([]int, 5, 10)
fmt.Println("len(mySlice):", len(mySlice))
fmt.Println("cap(mySlice):", cap(mySlice))
}
该程序的输出结果为:
len(mySlice): 5
cap(mySlice): 10
如果需要往上例中mySlice已包含的5个元素后面继续新增元素,可以使用append()函数。 下面的代码可以从尾端给mySlice加上3个元素,从而生成一个新的数组切片:
mySlice = append(mySlice, 1, 2, 3)
函数append()的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素, 甚至直接将一个数组切片追加到另一个数组切片的末尾:
mySlice2 := []int{8, 9, 10}
// 给mySlice后面添加另一个数组切片
mySlice = append(mySlice, mySlice2...)
需要注意的是,我们在第二个参数mySlice2后面加了三个点,即一个省略号,如果没有这个省 略号的话,会有编译错误,因为按append()的语义,从第二个参数起的所有参数都是待附加的 元素。因为mySlice中的元素类型为int,所以直接传递mySlice2是行不通的。加上省略号相 当于把mySlice2包含的所有元素打散后传入。
上述调用等同于:
mySlice = append(mySlice, 8, 9, 10)
数组切片会自动处理存储空间不足的问题。如果追加的内容长度超过当前已分配的存储空间 (即cap()调用返回的信息),数组切片会自动分配一块足够大的内存。
类似于数组切片可以基于一个数组创建,数组切片也可以基于另一个数组切片创建。下面的 例子基于一个已有数组切片创建新数组切片:
oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 基于oldSlice的前3个元素构建新数组切片
有意思的是,选择的oldSlicef元素范围甚至可以超过所包含的元素个数,比如newSlice 可以基于oldSlice的前6个元素创建,虽然oldSlice只包含5个元素。只要这个选择的范围不超 过oldSlice存储能力(即cap()返回的值),那么这个创建程序就是合法的。newSlice中超出 oldSlice元素的部分都会填上0。
数组切片支持Go语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个 数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行 复制。下面的示例展示了copy()函数的行为:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
在C++/Java中,map一般都以库的方式提供,比如在C++中是STL的std::map<>,在C#中是 Dictionary<>,在Java中是Hashmap<>,在这些语言中,如果要使用map,事先要引用相应的 库。而在Go中,使用map不需要引入任何库,并且用起来也更加方便。
map是一堆键值对的未排序集合。比如以身份证号作为唯一键来标识一个人的信息,则这个 map可以定义为代码清单 2-3所示的方式。
package main
import "fmt"
// PersonInfo是一个包含个人详细信息的类型
type PersonInfo struct {
ID string
Name string
Address string
}
func main() {
var personDB map[string] PersonInfo
personDB = make(map[string] PersonInfo)
// 往这个map里插入几条数据
personDB["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."}
personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."}
// 从这个map查找键为"1234"的信息
person, ok := personDB["1234"]
// ok是一个返回的bool型,返回true表示找到了对应的数据
if ok {
fmt.Println("Found person", person.Name, "with ID 1234.")
} else {
fmt.Println("Did not find person with ID 1234.")
}
}
上面这个简单的例子基本上已经覆盖了map的主要用法,下面对其中的关键点进行细述。
map的声明基本上没有多余的元素,比如:
var myMap map[string] PersonInfo
其中,myMap是声明的map变量名,string是键的类型,PersonInfo则是其中所存放的值类型。
我们可以使用Go语言内置的函数make()来创建一个新map。下面的这个例子创建了一个键 类型为string、值类型为PersonInfo的map:
myMap = make(map[string] PersonInfo)
也可以选择是否在创建时指定该map的初始存储能力,下面的例子创建了一个初始存储能力 为100的map:
myMap = make(map[string] PersonInfo, 100)
关于存储能力的说明,可以参见2.3.6节中的内容。
创建并初始化map的代码如下:
myMap = map[string] PersonInfo{
"1234": PersonInfo{"1", "Jack", "Room 101,..."},
}
赋值过程非常简单明了,就是将键和值用下面的方式对应起来即可:
myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."}
Go语言提供了一个内置函数delete(),用于删除容器内的元素。下面我们简单介绍一下如 何用delete()函数删除map内的元素:
delete(myMap, "1234")
上面的代码将从myMap中删除键为“1234”的键值对。如果“1234”这个键不存在,那么这个调 用将什么都不发生,也不会有什么副作用。但是如果传入的map变量的值是nil,该调用将导致 程序抛出异常(panic)。
在Go语言中,map的查找功能设计得比较精巧。而在其他语言中,我们要判断能否获取到一个值不是件容易的事情。判断能否从map中获取一个值的常规做法是:
(1) 声明并初始化一个变量为空;
(2) 试图从map中获取相应键的值到该变量中;
(3) 判断该变量是否依旧为空,如果为空则表示map中没有包含该变量。
这种用法比较啰唆,而且判断变量是否为空这条语句并不能真正表意(是否成功取到对应的 值),从而影响代码的可读性和可维护性。有些库甚至会设计为因为一个键不存在而抛出异常, 让开发者用起来胆战心惊,不得不一层层嵌套try-catch语句,这更是不人性化的设计。在Go 语言中,要从map中查找一个特定的键,可以通过下面的代码来实现:
value, ok := myMap["1234"]
if ok { // 找到了
// 处理找到的value
}
判断是否成功找到特定的键,不需要检查取到的值是否为nil,只需查看第二个返回值ok, 这让表意清晰很多。配合:=操作符,让你的代码没有多余成分,看起来非常清晰易懂。
程序设计语言的流程控制语句,用于设定计算执行的次序,建立程序的逻辑结构。可以说, 流程控制语句是整个程序的骨架。
从根本上讲,流程控制只是为了控制程序语句的执行顺序,一般需要与各种条件配合,因此, 在各种流程中,会加入条件判断语句。流程控制语句一般起以下3个作用:
在具体的应用场景中,为了满足更丰富的控制需求,Go语言还添加了如下关键字:break、 continue和fallthrough。在实际的使用中,需要根据具体的逻辑目标、程序执行的时间和空 间限制、代码的可读性、编译器的代码优化设定等多种因素,灵活组合。
接下来简要介绍一下各种流程控制功能的用法以及需要注意的要点.
关于条件语句的样例代码如下:
if a < 5 {
return 0
} else {
return 1
}
关于条件语句,需要注意以下几点:
否则会编译失败:
function ends without a return statement
失败的原因在于,Go编译器无法找到终止该函数的return语句。编译失败的案例如下:
func example(x int) int {
if x == 0 {
return 5
} else {
return x
}
}
根据传入条件的不同,选择语句会执行不同的语句。下面的例子根据传入的整型变量i的不 同而打印不同的内容:
switch i {
case 0:
fmt.Printf("0")
case 1:
fmt.Printf("1")
case 2:
fallthrough
case 3:
fmt.Printf("3")
case 4, 5, 6:
fmt.Printf("4, 5, 6")
default:
fmt.Printf("Default")
}
运行上面的案例,将会得到如下结果:
比较有意思的是,switch后面的表达式甚至不是必需的,比如下面的例子:
switch {
case 0 <= Num && Num <= 3:
fmt.Printf("0-3")
case 4 <= Num && Num <= 6:
fmt.Printf("4-6")
case 7 <= Num && Num <= 9:
fmt.Printf("7-9")
}
在使用switch结构时,我们需要注意以下几点:
与多数语言不同的是,Go语言中的循环语句只支持for关键字,而不支持while和do-while 结构。关键字for的基本使用方法与C和C++中非常接近:
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
可以看到比较大的一个不同在于for后面的条件表达式不需要用圆括号()包含起来。Go语言 还进一步考虑到无限循环的场景,让开发者不用写无聊的for (;;) {} 和 do {} while(1);, 而直接简化为如下的写法:
sum := 0
for {
sum++
if sum > 100 {
break
}
}
在条件表达式中也支持多重赋值,如下所示:
a := []int{1, 2, 3, 4, 5, 6}
for i, j := 0, len(a) – 1; i < j; i, j = i + 1, j – 1 {
a[i], a[j] = a[j], a[i]
}
使用循环语句时,需要注意的有以下几点。
break,可以选择中断哪一个循环,如下例:
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 5 {
break JLoop
}
fmt.Println(i)
}
}
JLoop:
// ...
本例中,break语句终止的是JLoop标签处的外层循环。
goto语句被多数语言学者所反对,谆谆告诫不要使用。但对于Go语言这样一个惜关键字如 金的语言来说,居然仍然支持goto关键字,无疑让某些人跌破眼镜。但就个人一年多来的Go语 言编程经验来说,goto还是会在一些场合下被证明是最合适的。
goto语句的语义非常简单,就是跳转到本函数内的某个标签,如:
func myfunc() {
i := 0
HERE:
fmt.Println(i)
i++
if i < 10 {
goto HERE
}
}
函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
前面我们已经大概介绍过函数,这里我们用一个最简单的加法函数来进行详细说明:
package mymath
import "errors"
func Add(a int, b int) (ret int, err error) {
if a < 0 || b < 0 { // 假设这个函数只支持两个非负数字的加法
err= errors.New("Should be non-negative numbers!")
return
}
return a + b, nil // 支持多重返回值
}
如果参数列表中若干个相邻的参数类型的相同,比如上面例子中的a和b,则可以在参数列表 中省略前面变量的类型声明,如下所示:
func Add(a, b int)(ret int, err error) {
// ...
}
如果返回值列表中多个返回值的类型相同,也可以用同样的方式合并。 如果函数只有一个返回值,也可以这么写:
func Add(a, b int) int {
// ...
}
从其他语言转过来的同学,可能更习惯这种写法。
函数调用非常方便,只要事先导入了该函数所在的包,就可以直接按照如下所示的方式调用 函数:
import "mymath"// 假设Add被放在一个叫mymath的包中
// ...
c := mymath.Add(1, 2)
在Go语言中,函数支持多重返回值,这在之后的内容中会介绍。利用函数的多重返回值和 错误处理机制,我们可以很容易地写出优雅美观的Go代码。
Go语言中函数名字的大小写不仅仅是风格,更直接体现了该函数的可见性,这一点尤其需 要注意。对于很多注意美感的程序员(尤其是工作在Linux平台上的C程序员)而言,这里的函数 名的首字母大写可能会让他们感觉不太适应,在自己练习的时候可能会顺手改成全小写,比如写 成add_xxx这样的Linux风格。很不幸的是,如果这样做了,你可能会遇到莫名其妙的编译错误, 比如你明明导入了对应的包,Go编译器还是会告诉你无法找到add_xxx函数。
因此需要先牢记这样的规则:小写字母开头的函数只在本包内可见,大写字母开头的函数才 能被其他包使用。
这个规则也适用于类型和变量的可见性。
在C语言时代大家一般都用过printf()函数,从那个时候开始其实已经在感受不定参数的 魅力和价值。如同C语言中的printf()函数,Go语言标准库中的fmt.Println()等函数的实现 也严重依赖于语言的不定参数功能。
本节我们将介绍不定参数的用法。合适地使用不定参数,可以让代码简单易用,尤其是输入 输出类函数,比如日志函数等。
不定参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受 不定参数类型:
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
这段代码的意思是,函数myfunc()接受不定数量的参数,这些参数的类型全部是int,所 以它可以用如下方式调用:
myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)
形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。它是一 个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用。通 常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的机会.
从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type,这也是为 什么上面的参数args可以用for循环来获得每个传入的参数。
假如没有...type这样的语法糖,开发者将不得不这么写:
func myfunc2(args []int) {
for _, arg := range args {
fmt.Println(arg)
}
}
从函数的实现角度来看,这没有任何影响,该怎么写就怎么写。但从调用方来说,情形则完 全不同:
myfunc2([]int{1, 3, 7, 13})
你会发现,我们不得不加上[]int{}来构造一个数组切片实例。但是有了...type这个语法糖,我们就不用自己来处理了。
假设有另一个变参函数叫做myfunc3(args ...int),下面的例子演示了如何向其传递变参:
func myfunc(args ...int) {
// 按原样传递
myfunc3(args...)
// 传递片段,实际上任意的int slice都可以传进去
myfunc3(args[1:]...)
}
之前的例子中将不定参数类型约束为int,如果你希望传任意类型,可以指定类型为 interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:
func Printf(format string, args ...interface{}) {
// ...
}
用interface{}传递任意类型数据是Go语言的惯例用法。使用interface{}仍然是类型安 全的,这和 C/C++ 不太一样。关于它的用法,可参阅3.5节的内容。代码清单2-4示范了如何分派 传入interface{}类型的数据。
package main
import "fmt"
func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
}
func main() {
var v1 int = 1
var v2 int64 = 234
var v3 string = "hello"
var v4 float32 = 1.234
MyPrintf(v1, v2, v3, v4)
}
该程序的输出结果为:
1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.
与C、C++和Java等开发语言的一个极大不同在于,Go语言的函数或者成员的方法可以有多 个返回值,这个特性能够使我们写出比其他语言更优雅、更简洁的代码,比如File.Read()函 数就可以同时返回读取的字节数和错误信息。如果读取文件成功,则返回值中的n为读取的字节 数,err为nil,否则err为具体的出错信息:
func (file *File) Read(b []byte) (n int, err Error)
同样,从上面的方法原型可以看到,我们还可以给返回值命名,就像函数的输入参数一样。 返回值被命名之后,它们的值在函数开始的时候被自动初始化为空。在函数中执行不带任何参数 的return语句时,会返回对应的返回值变量的值。
Go语言并不需要强制命名返回值,但是命名后的返回值可以让代码更清晰,可读性更强, 同时也可以用于文档。
如果调用方调用了一个具有多返回值的方法,但是却不想关心其中的某个返回值,可以简单 地用一个下划线“_”来跳过这个返回值,比如下面的代码表示调用者在读文件的时候不想关心 Read()函数返回的错误码:
n, _ := f.Read(buf)
匿名函数是指不需要定义函数名的一种函数实现方式,它并不是一个新概念,最早可以回溯 到1958年的Lisp语言。但是由于各种原因,C和C++一直都没有对匿名函数给以支持,其他的各 种语言,比如JavaScript、C#和Objective-C等语言都提供了匿名函数特性,当然也包含Go语言。
在Go里面,函数可以像普通变量一样被传递或使用,这与C语言的回调函数比较类似。不同 的是,Go语言支持随时在代码里定义匿名函数.
匿名函数由一个不带函数名的函数声明和函数体组成,如下所示:
func(a, b int, z float64) bool {
return a*b
匿名函数可以直接赋值给一个变量或者直接执行:
f := func(x, y int) int {
return x + y
}
func(ch chan int) {
ch <- ACK
} (reply_chan) // 花括号后直接跟参数列表表示函数调用
Go的匿名函数是一个闭包,下面我们先来了解一下闭包的概念、价值和应用场景。
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者 任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含 在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环 境(作用域)。
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示 数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到 变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么 被闭包引用的变量会一直存在.
package main
import (
"fmt"
)
func main() {
var j int = 5
a := func()(func()) {
var i int = 10
return func() {
fmt.Printf("i, j: %d, %d\n", i, j)
}
}()
a()
j *= 2
a()
}
上述例子的执行结果是:
i, j: 10, 5
i, j: 10, 10
在上面的例子中,变量a指向的闭包函数引用了局部变量i和j,i的值被隔离,在闭包外不 能被修改,改变j的值以后,再次调用a,发现结果是修改过的值。
在变量a指向的闭包函数中,只有内部的匿名函数才能访问变量i,而无法通过其他途径访问 到,因此保证了i的安全性
错误处理是学习任何编程语言都需要考虑的一个重要话题。在早期的语言中,错误处理不是 语言规范的一部分,通常只作为一种编程范式存在,比如C语言中的errno。但自C++语言以来, 语言层面上会增加错误处理的支持,比如异常(exception)的概念和try-catch关键字的引入。 Go语言在此功能上考虑得更为深远。漂亮的错误处理规范是Go语言最大的亮点之一
Go语言引入了一个关于错误处理的标准模式,即error接口,该接口的定义如下:
type error interface {
Error() string
}
对于大多数函数,如果要返回错误,大致上都可以定义为如下模式,将error作为多种返回 值中的最后一个,但这并非是强制要求:
func Foo(param int)(n int, err error) {
// ...
}
调用时的代码建议按如下方式处理错误情况:
n, err := Foo(0)
if err != nil {
// 错误处理
} else {
// 使用返回值n
}
下面我用Go库中的实际代码来示范如何使用自定义的error类型。
首先,定义一个用于承载错误信息的类型。因为Go语言中接口的灵活性,你根本不需要从 error接口继承或者像Java一样需要使用implements来明确指定类型和接口之间的关系,具体 代码如下:
type PathError struct {
Op string
Path string
Err error
}
如果这样的话,编译器又怎能知道PathError可以当一个error来传递呢?关键在于下面的 代码实现了Error()方法:
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
关于接口的更多细节,可以参见3.5节。之后就可以直接返回PathError变量了,比如在下 面的代码中,当syscall.Stat()失败返回err时,将该err包装到一个PathError对象中返回:
func Stat(name string) (fi FileInfo, err error) {
var stat syscall.Stat_t
err = syscall.Stat(name, &stat)
if err != nil {
return nil, &PathError{"stat", name, err}
}
return fileInfoFromStat(&stat, name), nil
}
如果在处理错误时获取详细信息,而不仅仅满足于打印一句错误信息,那就需要用到类型转 换知识了:
fi, err := os.Stat("a.txt")
if err != nil {
if e, ok := err.(*os.PathError); ok && e.Err != nil {
// 获取PathError类型变量e中的其他信息并处理
}
}
这就是Go中error类型的使用方法。与其他语言中的异常相比,Go的处理相对比较直观、 简单。
关于类型转换的更多知识,在第3章中也会有更进一步的阐述。
关键字defer是Go语言引入的一个非常有意思的特性,相信很多C++程序员都写过类似下面 这样的代码:
class file_closer {
FILE _f;
public:
file_closer(FILE f) : _f(f) {}
~file_closer() { if (f) fclose(f); }
};
然后在需要使用的地方这么写:
void f() {
FILE f = open_file("file.txt"); // 打开一个文件句柄
file_closer _closer(f);
// 对f句柄进行操作
}
为什么需要file_closer这么个包装类呢?因为如果没有这个类,代码中所有退出函数的 环节,比如每一个可能抛出异常的地方,每一个return的位置,都需要关掉之前打开的文件句 柄。即使你头脑清晰,想明白了每一个分支和可能出错的条件,在该关闭的地方都关闭了,怎么 保证你的后继者也能做到同样水平?大量莫名其妙的问题就出现了。
在C/C++中还有另一种解决方案。开发者可以将需要释放的资源变量都声明在函数的开头部 分,并在函数的末尾部分统一释放资源。函数需要退出时,就必须使用goto语句跳转到指定位 置先完成资源清理工作,而不能调用return语句直接返回。
这种方案是可行的,也仍然在被使用着,但存在非常大的维护性问题。而Go语言使用defer 关键字简简单单地解决了这个问题,比如以下的例子:
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
defer srcFile.Close()
dstFile, err := os.Create(dstName)
if err != nil {
return
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}
即使其中的Copy()函数抛出异常,Go仍然会保证dstFile和srcFile会被正常关闭。
如果觉得一句话干不完清理的工作,也可以使用在defer后加一个匿名函数的做法:
defer func() {
// 做你复杂的清理工作
} ()
另外,一个函数中可以存在多个defer语句,因此需要注意的是,defer语句的调用是遵照 先进后出的原则,即最后一个defer语句将最先被执行。只不过,当你需要为defer语句到底哪 个先执行这种细节而烦恼的时候,说明你的代码架构可能需要调整一下了。
Go语言引入了两个内置函数panic()和recover()以报告和处理运行时错误和程序中的错误场景:
func panic(interface{})
func recover() interface{}
当在一个函数执行过程中调用panic()函数时,正常的函数执行流程将立即终止,但函数中 之前使用defer关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致 逐层向上执行panic流程,直至所属的goroutine中所有正在执行的函数被终止。错误信息将被报 告,包括在调用panic()函数时传入的参数,这个过程称为错误处理流程。
从panic()的参数类型interface{}我们可以得知,该函数接收任意类型的数据,比如整 型、字符串、对象等。调用方法很简单,下面为几个例子:
panic(404)
panic("network broken")
panic(Error("file not exists"))
recover()函数用于终止错误处理流程。一般情况下,recover()应该在一个使用defer 关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复 过程(使用recover关键字),会导致该goroutine所属的进程打印异常信息后直接退出。
以下为一个常见的场景。
我们对于foo()函数的执行要么心里没底感觉可能会触发错误处理,或者自己在其中明确加 入了按特定条件触发错误处理的语句,那么可以用如下方式在调用代码中截取recover():
defer func() {
if r := recover(); r != nil {
log.Printf("Runtime error caught: %v", r)
}
}()
foo()
无论foo()中是否触发了错误处理流程,该匿名defer函数都将在函数退出时得到执行。假 如foo()中触发了错误处理流程,recover()函数执行将使得该错误处理过程终止。如果错误处 理流程被触发时,程序传给panic函数的参数不为nil,则该函数还会打印详细的错误信息。
现在我们用从本章学到的知识来实现一个完整的程序。我们准备开发一个排序算法的比较程序, 从命令行指定输入的数据文件和输出的数据文件,并指定对应的排序算法。该程序的用法如下所示:
USAGE: sorter –i –o –a
一个具体的执行过程如下:
$ ./sorter –I in.dat –o out.dat –a qsort
The sorting process costs 10us to complete.
当然,如果输入不合法,应该给出对应的提示,接下来我们一步步实现这个程序。
我们将该函数分为两类:主程序和排序算法函数。每个排序算法都包装成一个静态库,虽然 现在看起来似乎有些多此一举,但这只是为了顺便演示包之间的依赖方法。
假设我们的程序根目录为~/goyard/sorter,因此需要在环境变量GOPATH中添加这个路径。根 目录的结构如下:
├─
├─
├─sorter.go
├─
├─
├─qsort.go
├─qsort_test.go
├─
├─bubblesort.go
├─bubblesort_test.go
├─
├─
其中sorter.go是主程序,qsort.go用于实现快速排序,bubblesort.go用于实现冒泡排序。
下面我们先定义一下排序算法函数的函数原型:
func QuickSort(in []int)[]int
func BubbleSort(in []int)[]int
我们的主程序需要做的工作包含以下几点:
接下来我们一步步地编写程序。
Go语言标准库提供了用于快迅解析命令行参数的flag包。对于本示例的参数需求,我们可 以利用flag包进行实现,
package main
import "flag"
import "fmt"
var infile *string = flag.String("i", "infile", "File contains values for sorting")
var outfile *string = flag.String("o", "outfile", "File to receive sorted values")
var algorithm *string = flag.String("a", "qsort", "Sort algorithm")
func main() {
flag.Parse()
if infile != nil {
fmt.Println("infile =", *infile, "outfile =", *outfile, "algorithm =",
*algorithm)
}
}
因为这个程序需要输入参数,所以我们不能直接用go run来跑,而是需要先编译出二进制 程序。可以用go build来完成这个过程:
$ go build sorter.go
$ ./sorter -i unsorted.dat -o sorted.dat -a bubblesort
infile = unsorted.dat outfile = sorted.dat algorithm = bubblesort
可以看到,传入的各个命令行参数已经被正确读取到各个变量中。flag包使用起来非常方 便,大大简化了C语言时代解析命令行参数的过程。
我们需要先从一个文件中把包含的内容读取到数组中,将该数组排好序后再写回到另一个文 件中,因此还需要学习如何在Go语言中操作文件。
我们先设计输入文件的格式。输入文件是一个纯文本文件,每一行是一个需要被排序的数字。 下面是一个示例的unsorted.dat文件内容:
123
3064
3
64
490
然后需要逐行从这个文件中读取内容,并解析为int类型的数据,再添加到一个int类型的 数组切片中。接下来我们实现这部分功能.
package main
import "bufio"
import "flag"
import "fmt"
import "io"
import "os"
import "strconv"
var infile *string = flag.String("i", "unsorted.dat", "File contains values for sorting")
var outfile *string = flag.String("o", "sorted.dat", "File to receive sorted values")
var algorithm *string = flag.String("a", "qsort", "Sort algorithm")
func readValues(infile string)(values []int, err error) {
file, err := os.Open(infile)
if err != nil {
fmt.Println("Failed to open the input file ", infile)
return
}
defer file.Close()
br := bufio.NewReader(file)
values = make([]int, 0)
for {
line, isPrefix, err1 := br.ReadLine()
if err1 != nil {
if err1 != io.EOF {
err = err1
}
break
}
if isPrefix {
fmt.Println("A too long line, seems unexpected.")
return
}
str := string(line) // 转换字符数组为字符串
value, err1 := strconv.Atoi(str)
if err1 != nil {
err = err1
return
}
values = append(values, value)
}
return
}
func main() {
flag.Parse()
if infile != nil {
fmt.Println("infile =", *infile, "outfile =", *outfile, "algorithm =", *algorithm)
}
values, err := readValues(*infile)
if err == nil {
fmt.Println("Read values:", values)
} else {
fmt.Println(err)
}
}
在实现readValues()函数的过程中,我们用到了os、io、bufio和strconv等Go语言标 准库中的包,用于文件读写和字符串处理。熟练掌握这些包的基本用法,将会大幅度提高使用 Go语言的工作效率。
我们还示范了数组切片的使用,并使用defer关键字以确保关闭文件句柄。
在数据处理结束后,我们需要将排序结果输出到另一个文本文件。这个过程比较简单,因此 这里我们只列出writeValues()函数的实现,读者可以自行对照Go语言标准库以熟悉相关包的 用法。
func writeValues(values []int, outfile string) error {
file, err := os.Create(outfile)
if err != nil {
fmt.Println("Failed to create the output file ", outfile)
return err
}
defer file.Close()
for _, value := range values {
str := strconv.Itoa(value)
file.WriteString(str + "\n")
}
return nil
}
接下来我们就实现排序算法。因为算法本身并不在本书讨论的范畴,所以就不再解释冒泡排 序和快速排序的算法原理。
冒泡排序算法位于bubblesort.go这个源文件中,快速排序算法则位于qsort.go文件中。对于这 种纯算法的模块,我们应该自然而然地为其编写单元测试模块。我们在第7章中将专门介绍单元 测试的相关内容。
在冒泡排序中,包含一个具体的算法实现源文件和一个单元测试文件,
// bubblesort.go
package bubblesort
func BubbleSort(values []int) {
flag := true
for i := 0; i values[j + 1] {
values[j], values[j + 1] = values[j + 1], values[j]
flag = false
} // end if
} // end for j = ...
if flag == true {
break
}
} // end for i = ...
}
// bubble_test.go
package bubblesort
import "testing"
func TestBubbleSort1(t *testing.T) {
values := []int{5, 4, 3, 2, 1}
BubbleSort(values)
if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 4 ||
values[4] !=5 {
t.Error("BubbleSort() failed. Got", values, "Expected 1 2 3 4 5")
}
}
func TestBubbleSort2(t *testing.T) {
values := []int{5, 5, 3, 2, 1}
BubbleSort(values)
if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 5 ||
values[4] !=5 {
t.Error("BubbleSort() failed. Got", values, "Expected 1 2 3 5 5")
}
}
func TestBubbleSort3(t *testing.T) {
values := []int{5}
BubbleSort(values)
if values[0] != 5 {
t.Error("BubbleSort() failed. Got", values, "Expected 5")
}
}
与冒泡排序相同,快速排序也包含一个具体的算法实现源文件和一个单元测试文件,
// qsort.go
package qsort
func quickSort(values []int, left, right int) {
temp := values[left]
p := left
i, j := left, right
for i <= j {
for j >= p && values[j] >= temp {
j--
}
if j >= p {
values[p] = values[j]
p = j
}
if values[i] <= temp && i <= p {
i++
}
if i <= p {
values[p] = values[i]
p = i
}
}
values[p] = temp
if p - left > 1 {
quickSort(values, left, p - 1)
}
if right - p > 1 {
quickSort(values, p + 1, right)
}
}
func QuickSort(values []int) {
quickSort(values, 0, len(values) - 1)
}
// qsort_test.go
package qsort
import "testing"
func TestQuickSort1(t *testing.T) {
values := []int{5, 4, 3, 2, 1}
QuickSort(values)
if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 4 ||
values[4] !=5 {
t.Error("QuickSort() failed. Got", values, "Expected 1 2 3 4 5")
}
}
func TestQuickSort2(t *testing.T) {
values := []int{5, 5, 3, 2, 1}
QuickSort(values)
if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 5 ||
values[4] !=5 {
t.Error("QuickSort() failed. Got", values, "Expected 1 2 3 5 5")
}
}
func TestQuickSort3(t *testing.T) {
values := []int{5}
QuickSort(values)
if values[0] != 5 {
t.Error("QuickSort() failed. Got", values, "Expected 5")
}
}
现在我们可以在主程序加入对算法的调用以及函数的运行计时,最终版本的sorter.go
package main
import "bufio"
import "flag"
import "fmt"
import "io"
import "os"
import "strconv"
import "time"
import "algorithm/bubblesort"
import "algorithm/qsort"
var infile *string = flag.String("i", "unsorted.dat", "File contains values for sorting")
var outfile *string = flag.String("o", "sorted.dat", "File to receive sorted values")
var algorithm *string = flag.String("a", "qsort", "Sort algorithm")
func readValues(infile string)(values []int, err error) {
file, err := os.Open(infile)
if err != nil {
fmt.Println("Failed to open the input file ", infile)
return
}
defer file.Close()
br := bufio.NewReader(file)
values = make([]int, 0)
for {
line, isPrefix, err1 := br.ReadLine()
if err1 != nil {
if err1 != io.EOF {
err = err1
}
break
}
if isPrefix {
fmt.Println("A too long line, seems unexpected.")
return
}
str := string(line) // 转换字符数组为字符串
value, err1 := strconv.Atoi(str)
if err1 != nil {
err = err1
return
}
values = append(values, value)
}
return
}
func writeValues(values []int, outfile string) error {
file, err := os.Create(outfile)
if err != nil {
fmt.Println("Failed to create the output file ", outfile)
return err
}
defer file.Close()
for _, value := range values {
str := strconv.Itoa(value)
file.WriteString(str + "\n")
}
return nil
}
func main() {
flag.Parse()
if infile != nil {
fmt.Println("infile =", *infile, "outfile =", *outfile, "algorithm =",
*algorithm)
}
values, err := readValues(*infile)
if err == nil {
t1 := time.Now()
switch *algorithm {
case "qsort":
qsort.QuickSort(values)
case "bubblesort":
bubblesort.BubbleSort(values)
default:
fmt.Println("Sorting algorithm", *algorithm, "is either unknown or
unsupported.")
}
t2 := time.Now()
fmt.Println("The sorting process costs", t2.Sub(t1), "to complete.")
writeValues(values, *outfile)
} else {
fmt.Println(err)
}
}
至此,本章的示例已经全部完成。在确认已经设置好GOPATH后,我们可以直接运行以下命 令来构建和测试程序:
$ echo $GOPATH
~/goyard/sorter
$ go build algorithm/qsort
$ go build algorithm/bubblesort
$ go test algorithm/qsort
ok algorithm/qsort0.007s
$ go test algorithm/bubblesort
ok algorithm/bubblesort0.013s
$ go install algorithm/qsort
$ go install algorithm/bubblesort
$ go build sorter
$ go install sorter
如果没有出现任何问题,那么通过执行这些命令,我们应该能够在src的同一级目录下看到两 个目录——bin和pkg,其中pkg目录下放置的是bubblesort.a和qsort.a, bin目录下放置的是sorter的二 进制可执行文件。
因为sorter接受的是一个文件格式的输入,所以需要准备这样的一个文件。我们可以在sorter 所在的bin目录内创建一个unsorted.dat文本文件,按一行一个整数的方式填入一些数据后保存。 sorted.dat会由程序自动创建,因此不需要事先创建。
接下来我们演示如何运行这个程序,并查看执行的结果:
$ cd bin
$ ls
sorterunsorted.dat
$ cat unsorted.dat
123
3064
3
64
490
1
23
5331
2
7
4
2
132
$ ./sorter -i unsorted.dat -o sorted.dat -a qsort
infile = unsorted.dat outfile = sorted.dat algorithm = qsort
The sorting process costs 3us to complete.
$ ./sorter -i unsorted.dat -o sorted.dat -a bubblesort
infile = unsorted.dat outfile = sorted.dat algorithm = bubblesort
The sorting process costs 2us to complete.
$ cat sorted.dat
1
2
2
3
4
7
23
64
123
132
490
3064
5331
可以看到,结果已经被正确排序并写入到sorted.dat文件中,至此我们的程序也算是完整 了。这个程序不仅仅演示了本章学到的大部分内容,还顺便示范了Go语言标准库中多个常用 包的用法。
相信读者基于这个程序框架可以快速使用Go语言来解决自己在工作和学习中遇到的实际 问题。
本章我们详细讲解了Go语言顺序编程的相关语法,从这些语法特征可以很容易看出C语言的 影子(毕竟肯·汤普森也是C语言的设计者),但Go又利用一系列新增特性很好地让Go程序员避 免重复之前C程序员面临的众多问题。看完这一章,你应该也可以理解为什么很多人评价Go语言 为“更好的C语言”。