Golang学习笔记
本文是从YouTube观看视频资料Golang初学者教程时顺手做的笔记,仅供辅助学习和回顾使用。由于水平有限,可能会存在一定的翻译错误和内容错误。
YouTube资料来源:
文章目录
- Golang学习笔记
- 1 数字类型
- 1.1 print中的ln和f
- 1.2 int类型转换为float32类型
- 1.3 float32类型转换为int类型
- 1.4 int转换为string
- 1.5 复数complex64和complex128
- 1.5.1 复数的使用
- 1.5.2 使用real和imag获得实部和虚部
- 2 boolean类型
- 3 符号运算
- 4 字符与字符串
- 4.1 字符串
- 4.2 打印字符
- 4.3 字节切片和rune
- 5 常量Const
- 5.1 定义常量
- 5.2 无类型常量(Untype)
- 5.3 枚举常量
- 5.3.1 定义iota
- 5.3.2 常量块的使用
- 5.3.3 常量块中_
- 5.3.4 固定偏移量
- 5.3.5 iota在设置role中的应用
- 6 数组
- 6.1 定义与初始化
- 6.2 字符串数组
- 6.3 存储矩阵
- 6.4 数组的指针
- 6.5 数组切片
- 6.5.1 基本示例
- 6.5.2 使用make制作切片
- 6.5.3 capacity
- 6.5.4 append
- 6.5.4.1 添加一些元素
- 6.5.4.2 删除某些元素
- 7 Map
- 7.1 定义Map
- 7.2 使用make制作map
- 7.3 map存储的特性
- 7.4 删除内容
- 7.5 检查式的查询语法
- 7.6 map是引用类型(指针)
- 8 struct
- 8.1 定义struct
- 8.2 匿名struct
- 8.3 struct是独立的值类型
- 8.4 嵌入
- 8.5 标签(Tags)
- 9 If语句
- 9.1 基本语法
- 9.2 通常用法
- 9.3 比较
- 9.4 判断浮点数
- 10 Switch语句
- 10.1 基本语法
- 10.2 比较
- 10.3 break
- 10.4 fallthrough
- 10.5 特殊语法(Type switches)
- 11 循环
- 11.1 简单循环
- 11.1.1 定义多个参数
- 11.1.2 for循环中的;
- 11.2 提前退出
- 11.2.1 无限循环(break)
- 11.2.2 continue
- 11.2.3 Labels标签
- 11.3 循环遍历集合
- 11.3.1 range
- 11.3.2 在range中使用_
- 12 控制
- 12.1 延迟(Defer)
- 12.2 Panic
- 12.2.1 出现Panic
- 12.2.2 panic和defer的关系
- 12.3 Recover
- 13 指针(Pointers)
- 13.1 定义指针
- 13.2 数组与指针
- 13.3 struct与指针
- 13.4 nil
- 13.5 切片与指针
- 13.6 map与指针
- 14 函数
- 14.1 命名规则
- 14.2 奇怪的规定
- 14.3 传入参数
- 14.4 使用指针
- 14.5 可变参数
- 14.6 返回函数
- 14.6 抛出错误
- 14.7 匿名函数
- 14.8 方法
- 15 接口
- 15.1 基本概念
- 15.2 接口组合
- 15.3 类型转换
- 15.3.1 空接口
- 15.3.2 类型switch
- 15.4 值类型(values)与指针类型(pointers)
- 16 Goroutine
- 16.1 创建Goroutine
- 16.2 同步机制(Synchronization)
- 16.2.1 权重组(WaitGroups)
- 16.2.2 互斥锁(Mutexes)
- 16.3 并发(Parallelism)
- 17 Channel
- 17.1 基本概念
- 17.2 限制数据流
- 17.3 缓冲(Buffered)
- 17.4 循环
- 17.4 关闭
- 17.6 Select
1 数字类型
1.1 print中的ln和f
使用和打印一个int类型
var i int = 42
//ln和f的区别
fmt.Println("%v, %T", i, i)
fmt.Printf("%v, %T\n", i, i)
得到的结果:
%v, %T 42 42
42, int
f可以进行格式化的输出,ln自带换行。
1.2 int类型转换为float32类型
var j float32 = 45.2
var i int = 42
fmt.Printf("%v, %T\n", j, j)
j = float32(i)
fmt.Printf("%v, %T\n", j, j)
得到的结果:
45.2, float32
42, float32
1.3 float32类型转换为int类型
var j float32 = 45.2
var i = int(j)
fmt.Printf("%v, %T\n", i, i)
得到的结果:
42, int
对小数点后的内容进行了截断。
1.4 int转换为string
//直接转换是ASCII码
s = string(i)
fmt.Printf("%v, %T\n", s, s)
//使用strconv包转换
s = strconv.Itoa(i)
fmt.Printf("%v, %T\n", s, s)
得到的结果:
*, string
42, string
int直接转换为string是对应的ASCII,需使用strconv转换。
1.5 复数complex64和complex128
1.5.1 复数的使用
var c1 complex64 = 2i
fmt.Printf("%v, %T\n", c1, c1)
c1 = 1 + 3i
fmt.Printf("%v, %T\n", c1, c1)
得到的结果:
(0+2i), complex64 //0会自动显示出来
(1+3i), complex64
1.5.2 使用real和imag获得实部和虚部
//用real和imag拉出实部和虚部,如果是complex64,则出来的分别是两个float32
fmt.Printf("%v, %T\n", real(c1), real(c1))
fmt.Printf("%v, %T\n", imag(c1), imag(c1))
var c2 complex128 = 1 + 2i
//用real和imag拉出实部和虚部,如果是complex128,则出来的分别是两个float64
fmt.Printf("%v, %T\n", real(c2), real(c2))
fmt.Printf("%v, %T\n", imag(c2), imag(c2))
//另外一种构造方式,直接使用函数
var c3 complex128 = complex(5, 10)
fmt.Printf("%v, %T", c3, c3)
complex64和complex128其实是拆分成两个float的组件。
complex类型解释:
// complex64 is the set of all complex numbers with float32 real and
// imaginary parts.
type complex64 complex64
// complex128 is the set of all complex numbers with float64 real and
// imaginary parts.
type complex128 complex128
2 boolean类型
n := 1 == 1
m := 1 == 2
fmt.Printf("%v, %T\n", n, n)
fmt.Printf("%v, %T\n", m, m)
得到的结果:
true, bool
false, bool
注意所有数据如果没有赋初始值,都会是0或false等。
3 符号运算
var a int = 10 //1010 > 10
var b int = 3 //0011 > 3
fmt.Println(a & b) //0010 > 2
fmt.Println(a | b) //1011 > 11
fmt.Println(a ^ b) //1001 > 9
fmt.Println(a &^ b) //1000 > 8
得到的结果:
2
11
9
8
注意符号运算中的二进制。注意在if语句的判断条件中,与是&&,或是||。
4 字符与字符串
4.1 字符串
var s string = "this is a string"
fmt.Printf("%v, %T\n", s, s)
得到的结果:
this is a string, string
4.2 打印字符
//直接打印字符串中的一个字符是uint8类型
fmt.Printf("%v, %T\n", s[2], s[2])
//转换过后显示字符串中的字符
fmt.Printf("%v, %T\n", string(s[2]), string(s[2]))
直接打印字符串中的一个字符是uint8类型,需要转换之后才能显示字符:
105, uint8
i, string
4.3 字节切片和rune
//使用数组a来表示s(字节切片)
var a = []byte(s)
fmt.Printf("%v, %T\n", a, a)
//使用rune来表示一个符文,得到的结果是ASCII和int32类型
var r rune = 'a'
fmt.Printf("%v, %T\n", r, r)
得到的结果:
[116 104 105 115 32 105 115 32 97 32 115 116 114 105 110 103], []uint8
97, int32
string类型解释:
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
字符串表示为一个utf-8的字符集,并且字符串可以为空,但不能为0值。同时注意,Values of string type are immutable.即字符串类型的值是不可变的。
5 常量Const
5.1 定义常量
一般使用C定义常量:
#define MYCONST 10
但是在GoLand中,如果首字母大写则会export,所以不适用首字母大写
//常量定义和其他语言有所不同,一般包内使用驼峰命名
const myConst int = 42
fmt.Printf("%v, %T\n", myConst, myConst)
//如果要暴露给其他文件,则在驼峰的基础上将第一个字母大写
const MyConst int = 43
fmt.Printf("%v, %T\n", MyConst, MyConst)
得到的结果:
42, int
43, int
const的值不允许被更改。且必须在运行时被确定,即不能使用函数定义。
例如:
import "math"
func main(){
const myConst float64 = math.Sin(1.57)
fmt.Printf("%v, %T\n", myConst, myConst)
}
这将产生一个编译错误。因为函数在运行时并没有被确定。
但是,内部常量可以更改外部常量的值和类型,例如:
const a int16 = 27
func main(){
const a int = 14
fmt.Printf("%v, %T\n", a, a)
}
得到的结果是int而非int16,值为14而非27:
14, int
注意常量的重用。
5.2 无类型常量(Untype)
如果显式定义一个常量,并将它与一个不匹配的类型进行操作,编译器会报错,例如:
const a int = 42 //指定为int类型
var b int16 = 27
fmt.Printf("%v, %T\n", a+b, a+b)
编译器报错:
invalid operation: a + b (mismatched types int and int16)
将常量a更改为无类型常量:
const a = 42 //删去int的类型指定
var b int16 = 27
fmt.Printf("%v, %T\n", a+b, a+b)
得到正确的结果:
69, int16
此时,编译器做的操作其实是("%v, %T\n", 42 + b, 42 + b),即找到a所在的位置,并用这个常量的值替换它,而不管这个常量的类型是什么。
5.3 枚举常量
一般在包级别使用枚举常量,函数级别不常用。
const ai = iota
func main(){
fmt.Printf("%v, %T\n", ai, ai)
}
ai会被自动判定为int类型,并且初值为0,上述代码结果如下:
0, int
5.3.1 定义iota
iota是一个计数器。并且使用常量块定义枚举常量与不使用常量块定义有区别。
例1:不使用常量块
const ai = iota
const bi = iota
const ci = iota
func main(){
//枚举常量
fmt.Printf("%v, %T\n", ai, ai)
fmt.Printf("%v, %T\n", bi, bi)
fmt.Printf("%v, %T\n", ci, ci)
}
得到的结果:
0, int
0, int
0, int
例2:使用常量块
const (
ai1 = iota
bi1 = iota
ci1 = iota
)
func main(){
//枚举常量(使用常量块定义)
fmt.Printf("%v, %T\n", ai1, ai1)
fmt.Printf("%v, %T\n", bi1, bi1)
fmt.Printf("%v, %T\n", ci1, ci1)
}
得到的结果:
0, int
1, int
2, int
iota正在改变常量的值,在同一个常量块里的常量被重新评估。
5.3.2 常量块的使用
上一个例子可以看到,abc都被定义为iota。一般来说,如果只定义a为iota,而不定义b和c,期望发生一个没有对常量进行定义的编译错误,但是如果在常量块中,则会自动定义。如下所示:
const (
ai2 = iota
bi2 //不进行定义
ci2 //不进行定义
)
func main(){
//枚举常量(使用常量块定义,并且b和c不进行定义)
fmt.Printf("%v, %T\n", ai2, ai2)
fmt.Printf("%v, %T\n", bi2, bi2)
fmt.Printf("%v, %T\n", ci2, ci2)
}
得到的结果为:
0, int
1, int
2, int
和普通的常量块定义没有区别。
5.3.3 常量块中_
const (
_ = iota
abc
def
)
此时表示_是一个零值,但表示我们并不在乎(不使用),编译器可以将它抛弃。
5.3.4 固定偏移量
const (
a = iota + 5
)
得到的结果:
5
只要定义时不是函数表达式,就可以应用加、减、乘、除、取余数等。
5.3.5 iota在设置role中的应用
定义一些角色,使用iota来快速获得对应的角色和访问权限。
//role
const (
isAdmin = 1 << iota //0000001
isHeadquarters //0000010
canSeeFinFinancials //0000100
canSeeAfrica //0001000
canSeeAsia //0010000
canSeeNorthAmerica //0100000
canSeeSouthAmerica //1000000
)
func main(){
//role
var roles byte = isAdmin | canSeeFinFinancials | canSeeNorthAmerica
("%b\n", roles)
}
得到的结果:
100101
也可以进行检查,查看是否有对应的权限:
//角色检查
fmt.Printf("Is Admin? %v\n", isAdmin&roles == isAdmin)
fmt.Printf("Is HQ? %v\n", isHeadquarters&roles == isHeadquarters)
得到的结果:
Is Admin? true
Is HQ? false
6 数组
6.1 定义与初始化
var grades [3]int
fmt.Printf("Grades: %v", grades)
得到的结果:
Grades: [0 0 0]
初始化数组:
var grades1 = [3]int{97, 85, 66}
fmt.Printf("Grades1: %v\n", grades1)
得到的结果:
Grades1: [97 85 66]
可以使用方括号中"."来暗示声明:
var grades2 = [...]int{100, 76, 90}
fmt.Printf("Grades1: %v\n", grades2)
注意.的数量是固定的3个,否则编译不通过,这种方式可以让程序变得更加健壮,因为不用时刻记住这个数组的大小,它将会随着程序自动更新。
得到的结果:
Grades2: [100 76 90]
6.2 字符串数组
定义一个大小为5的字符串数组,并给其中0~2数组元素赋值,打印输出数组的大小。
var students [5]string
fmt.Printf("Students: %v\n", students)
students[0] = "LiMing"
students[1] = "LiHua"
students[2] = "ZhangSan"
fmt.Printf("Students:%v\n", students)
fmt.Printf("Students #1: %v\n", students[0])
fmt.Printf("Number of Students: %v\n", len(students))
得到的结果:
Students: [ ]
Students:[LiMing LiHua ZhangSan ]
Students #1: LiMing
Number of Students: 5
注意数组大小和数组内存的元素无关,与一开始定义的大小有关。
6.3 存储矩阵
//矩阵
var identityMatrix [3][3]int
identityMatrix[0] = [3]int{1, 0, 0}
identityMatrix[0] = [3]int{0, 1, 0}
identityMatrix[0] = [3]int{0, 0, 1}
fmt.Println(identityMatrix)
得到的结果:
[[0 0 1] [0 0 0] [0 0 0]]
6.4 数组的指针
定义一个三元组a,将a的值赋给b。将b[1]的元素更改为5。
a := [...]int{1, 2, 3}
b := a //整个数组赋值
b[1] = 5
fmt.Println(a)
fmt.Println(b)
得到的结果为:
[1 2 3]
[1 5 3]
可以看到b复制了整个a数组的值,并且a和b没有关联。如果使用指针赋值,结果将会变得不一样:
//数组指针
ap := [...]int{1, 2, 3}
bp := &ap //将ap数组的指针赋值给bp
bp[1] = 5
fmt.Println(ap)
fmt.Println(bp)
得到的结果为:
[1 5 3]
&[1 5 3]
更改b的时候,实际上是在更改相同的基础数据。
6.5 数组切片
6.5.1 基本示例
as1 := [3]int{1, 2, 3} //指定了数组的大小
bs1 := as1
fmt.Println(as1)
fmt.Println(bs1)
fmt.Printf("Length AS1: %v\n", len(as1))
fmt.Printf("Capacity AS1: %v\n", cap(as1))
as2 := []int{1, 2, 3} //没有指定数组的大小(等于切片)
bs2 := as2
fmt.Println(as2)
fmt.Println(bs2)
fmt.Printf("Length AS2: %v\n", len(as2))
fmt.Printf("Capacity AS2: %v\n", cap(as2))
得到的结果:
[1 2 3] //a数组的值不随着b数组的值改变
[1 5 3]
Length AS1: 3
Capacity AS1: 3
[1 5 3]
[1 5 3] //a数组的值随着b数组的值改变
Length AS2: 3
Capacity AS2: 3
可以看到如果使用数组切片,a数组的值将会随着b数组的值改变。其实b就是指向a中的一个指针。
示例:
aa := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
bb := aa[:]
cc := aa[3:] //从第3个元素开始(不包括这个元素),即从4th element到结束
dd := aa[:6] //从第1个元素到第6个元素结束(包括这个元素),总共6个元素
ee := aa[3:6] //4th, 5th, 6th
fmt.Println(aa)
fmt.Println(bb)
fmt.Println(cc)
fmt.Println(dd)
fmt.Println(ee)
得到的结果:
[1 2 3 4 5 6 7 8 9 10]
[1 2 3 4 5 6 7 8 9 10]
[4 5 6 7 8 9 10]
[1 2 3 4 5 6]
[4 5 6]
可以使用冒号对数组进行切片。并且所有操作都指向同一块基础数据,即如果对其中一个数据进行更改,所有数据都将被更改。
6.5.2 使用make制作切片
make函数与其对应的参数。第1个是数组类型,第2个是数组的长度,第3个是数组的容量。注意数组的容量和数组的长度是不相同的。
//make函数
am := make([]int, 3, 100)
fmt.Println(am)
fmt.Printf("Length AM: %v\n", len(am))
fmt.Printf("Capacity AM: %v\n", cap(am))
得到的结果:
[0 0 0]
Length AM: 3
Capacity AM: 100
6.5.3 capacity
如果不指定capacity的值,capacity可以随着数组的扩大不断动态地增加。
//capacity
aca := []int{}
fmt.Println(aca)
fmt.Printf("Length ACA: %v\n", len(aca))
fmt.Printf("Capacity ACA: %v\n", cap(aca))
//使用append插入一个元素
aca = append(aca, 1)
fmt.Println(aca)
fmt.Printf("Length ACA: %v\n", len(aca))
fmt.Printf("Capacity ACA: %v\n", cap(aca))
得到的结果:
[]
Length ACA: 0
Capacity ACA: 0
[1]
Length ACA: 1
Capacity ACA: 1
再对数组进行添加:
aca = append(aca, 2, 3, 4, 5)
fmt.Println(aca)
fmt.Printf("Length ACA: %v\n", len(aca))
fmt.Printf("Capacity ACA: %v\n", cap(aca))
得到的结果:
[1 2 3 4 5]
Length ACA: 5
Capacity ACA: 6
可以看到capacity和length的值是不同的。
6.5.4 append
6.5.4.1 添加一些元素
如果要使用append添加一整个数组,写法必须注意,以下的写法是错误的:
aca = append(aca, []int{6, 7, 8}) //这里数组的写法是错误的
fmt.Println(aca)
fmt.Printf("Length ACA: %v\n", len(aca))
fmt.Printf("Capacity ACA: %v\n", cap(aca))
编译器报错:
cannot use []int{...} (type []int) as type int in append
要使用正确的写法:
aca = append(aca, []int{6, 7, 8}...) //...表示对这个数组进行单独的切片再存入
fmt.Println(aca)
fmt.Printf("Length ACA: %v\n", len(aca))
fmt.Printf("Capacity ACA: %v\n", cap(aca))
得到的结果:
[1 2 3 4 5 6 7 8]
Length ACA: 8
Capacity ACA: 12
可以看到,此时append(aca, []int{6, 7, 8}…)写法的作用与append(aca, 6, 7, 8)是完全等价的。
6.5.4.2 删除某些元素
可以使用切片对数组的某些元素进行删除:
//append删除元素
apd := []int{1, 2, 3, 4, 5}
fmt.Println(apd)
bpd := append(apd[:2], apd[3:]...)
fmt.Println(bpd)
fmt.Println(apd)
得到的结果:
[1 2 3 4 5] //未进行操作前apd中的元素
[1 2 4 5] //bpd中的元素
[1 2 4 5 5] //进行操作以后apd中的元素
可以看到bpd删除了第3个元素,实际上是将前2个元素和第3个元素以后的元素进行了切片并重新拼接。由于切片是对底层相同的数据元素进行操作,所以使用这种方法是非常敏感的,可能会产生一些意想不到的错误。
7 Map
7.1 定义Map
map是一个键值对的集合:map[“key”] = “value”
statePpipulation := map[string]int{
"BeiJing": 1234567,
"ShangHai": 39459,
}
fmt.Println(statePpipulation)
得到的结果:
map[BeiJing:1234567 ShangHai:39459]
map的key类型不可以是切片:
m := map[[]int]string{} //不可以是[]int
fmt.Println(m)
如果这样定义一个map,编译器将会报错:
invalid map key type []int
更改为数组类型,可以正常运行:
m := map[[3]int]string{} //更改为[3]int
fmt.Println(m)
得到的结果:
map[]
7.2 使用make制作map
//使用make制作map
statePopulationMake := make(map[string]int)
statePopulationMake = map[string]int{
"HangZhou": 1234567,
}
fmt.Println(statePopulationMake)
得到的结果:
map[HangZhou:1234567]
7.3 map存储的特性
map无法保证以顺序的方式返回结果。
//增加内容将会改变map的整体顺序
state := make(map[string]int)
state = map[string]int{
"China": 1,
"American": 2,
"Japan": 3,
}
fmt.Println(state)
state["Africa"] = 4 //增加一条内容
fmt.Println(state)
得到的结果:
map[American:2 China:1 Japan:3]
map[Africa:4 American:2 China:1 Japan:3]
可以发现map中的顺序并不是按照123来排列的。并且加入第4条内容以后,顺序也会发生改变。
7.4 删除内容
使用delete的内置函数进行删除操作:
//删除操作
stateD := make(map[string]int)
stateD = map[string]int{
"China": 1,
"American": 2,
"Japan": 3,
}
fmt.Println(stateD["Japan"]) //打印输出Japan的内容
delete(stateD, "Japan") //删除Japan的内容
fmt.Println(stateD["Japan"]) //打印被删除的Japan内容
fmt.Println(stateD["HangZhou"]) //打印一个本不存在map中的内容:HangZhou
如果对已经删除的元素进行输出,将会返回0值:
3
0
0
综上,如果要打印一个不在map中的内容,并不会报错,而是返回0值。
7.5 检查式的查询语法
可以使用“value,ok”语法进行查询:
//查询操作
stateC := make(map[string]int)
stateC = map[string]int{
"China": 1,
"American": 2,
"Japan": 3,
}
pop := stateC["C"] //进行拼写错误的查询
fmt.Println(pop)
pop1, ok1 := stateC["C"] //拼写错误的查询,加检查语法
fmt.Println(pop1, ok1)
pop2, ok2 := stateC["China"] //拼写正确的查询,加检查语法
fmt.Println(pop2, ok2)
得到的结果:
0 //拼写错误,返回0值
0 false //拼写错误,检查返回false
1 true //拼写正确,检查返回true
检查语法可以用来判断是否存在。如果只需要检查是否存在,可以使用“_”:
_, ok3 := state["China"] //如果只需要检查是否存在
fmt.Println(ok3)
得到的结果:
true
7.6 map是引用类型(指针)
和数组切片一样,如果对同一个map有多个分配,则它们都指向同一片基础数据,即map实际上是一种引用类型,这意味着在一个位置操作map中的数据,任何其他指向这个map的变量都能看到这种变化。可以与8.3的struct类型进行比较。
//指针操作
stateP := make(map[string]int)
stateP = map[string]int{
"China": 1,
"American": 2,
"Japan": 3,
}
fmt.Println(stateP) //打印输出原map
sp := stateP
delete(sp, "Japan") //对指针进行删除操作
fmt.Println(stateP) //再次打印原map内容
fmt.Println(sp) //打印sp的内容
得到的结果:
map[American:2 China:1 Japan:3] //打印输出原map
map[American:2 China:1] //删除Japan后,再次打印原map内容
map[American:2 China:1] //打印sp的内容
可以看到原来的map和修改后的map指向的是同一片数据。
8 struct
8.1 定义struct
定义一个struct结构体,有四个字段,对其中三个字段进行赋值。在sturct中的命名规则和其他类型的规则相同,如果要export,则使用Pascal命名,如果是import,则使用驼峰命名。
在下例中,Docter暴露给外部,同时将结构体中的Number字段也暴露给外部,则外部只可以看到Doctor和其中的Number,其他字段则是不可见的。
type Docter struct {
Number int //如果要export,使用Pascal命名
actorName string //如果是import,则使用驼峰命名
companions []string
episodes []string
}
func main() {
aDoctor := Docter{
Number: 3,
actorName: "Shark",
companions: []string{
"LiMing",
"ZhangSan",
},
//episodes字段可以不进行初始化,默认为0值
}
fmt.Println(aDoctor)
fmt.Println(aDoctor.Number) //使用.方法来调用具体的字段内容
fmt.Println(aDoctor.companions[1])
}
得到的结果:
{3 Shark [LiMing ZhangSan] []}
3
ZhangSan
还可以使用位置法进行struct的初始赋值,但是不推荐使用。
8.2 匿名struct
第一组花括号定义了struct中的keywords,第二组花括号作为初始化器定义了struct中的内容。匿名struct通常使用在非常短暂的数据操作中,比如JSON响应到网络服务调用等。
//匿名struct
bDoctor := struct {
name string
}{"LiMing"}
fmt.Println(bDoctor)
得到的结果:
{LiMing}
使用匿名的好处是可以不必创建一个在整个过程中都可用的正式类型,只包装一个可能只用一次的东西。
8.3 struct是独立的值类型
与map和slice不同,在应用程序中传递一个struct结构体时,指的是独立的数据集。实际上是在传送相同数据的副本。如果是在操作一个非常非常大的数据集的时候,要记住每次操作都是创建出一个对应的副本。可以与7.6的map类型进行比较。
//匿名struct
bDoctor := struct {
name string
}{"LiMing"}
fmt.Println(bDoctor)
//赋值后更改struct的值
anotherDoctor := bDoctor
anotherDoctor.name = "ZhangSan"
fmt.Println(bDoctor)
fmt.Println(anotherDoctor)
得到的结果:
{LiMing} //bDoctor的值
{ZhangSan} //anotherDoctor的值
可以看到anotherDoctor值的改变并不影响原struct值的改变。即在一个位置对struct进行操作,将不会影响应用程序中的任何其他变量。
如果要让它指向同一片基础数据,可以使用指针&:
anotherDoctorP := &bDoctor //指针
anotherDoctorP.name = "ZhangSan"
fmt.Println(bDoctor)
fmt.Println(anotherDoctorP)
得到的结果:
{ZhangSan} //bDoctor的值
&{ZhangSan} //anotherDoctorP的值
可以看到对anotherDoctorP的改变也影响了原struct值的改变。
8.4 嵌入
**go语言不支持传统的面向对象原则。**go没有继承,但是使用一种类似继承的模型,称为组合。如果按照以下方法定义Bird和Animal两个struct结构体,则它们是两个独立的结构体,之间没有关联。
type Animal struct {
Name string
Origin string
}
type Bird struct {
SpeedKPH float32
CanFly bool
}
在传统的面向对象语言中,假设我们可以这样定义一个sturuct:
type Animal struct {
Name string
Origin string
}
type Bird struct {
Animal animal //定义一个对象animal,要为它指定一个字段名
SpeedKPH float32
CanFly bool
}
如果我们要调用的Animal中的Name字段,通常这样使用:
但是如果我们使用嵌入定义,即想要在一个结构中嵌入另一个结构,只需要在这个结构中列出要嵌入的结构的类型,而不需要为它指定一个字段名:
type Animal struct {
Name string
Origin string
}
type Bird struct {
Animal //直接声明Animal,而不需要为它指定一个字段名(嵌入)
SpeedKPH float32
CanFly bool
}
go会自动处理对Name字段的请求的委托,为我们嵌入Animal类型,除了嵌入之外没有任何的联系,所以Bird和Animal不是传统的继承关系,而是一种组合关系。它们不能互换使用,如果要互换使用数据,则需要使用接口。
此时,一个Bird类型的结构体b要调用属于Animal类型中的Name,可以直接这样使用:
此时,我们可以将所有字段看成是归Bird结构所有,而不必关心嵌入结构体的内部结构。
对嵌入结构体进行操作:
//嵌入式结构体
b := Bird{} //创建一个空的结构体,再进行操作
b.Name = "LiMing"
b.Origin = "China"
b.SpeedKPH = 48
b.CanFly = false
fmt.Println(b.Name)
得到的结果:
LiMing
还有一种描述情况,需要明确地知道其中的内部结构。这一点和上面的声明式不太一样,如果只是声明对象并从外部操作它,不需要知道内部结构:
//文字声明
c := Bird{ //与创建一个空的结构体再进行操作不同,这里直接进行初始化赋值
Animal: Animal{ //此时必须明确地知道嵌入结构体的内部结构
Name: "LiMing",
Origin: "China",
},
SpeedKPH: 48,
CanFly: false,
}
fmt.Println(c)
上述样例中,c和b的结果是一样的,但是赋值时的情况发生了改变。
8.5 标签(Tags)
通过添加标签来描述一些关于某些字段的特定的信息。假设在一个Web应用中,需要用户填写一个表单,此时的Name字段确保名称是必须的并且不超过最大长度:
type AnimalTags struct {
Name string `required max:"100"` //反引号不是单引号,反引号是左上角的~`按键。
Origin string
}
标签的格式使用反引号作为分隔符,这样就有了一些用空格分隔的键值对。go中规定的用法是使用空格分隔子标签。如果确实需要键值关系,使用冒号将键和值分隔,并且值通常放在引号中。
要使用标签,需要引用go中的**“reflect”**包。
import (
"fmt"
"reflect" //引用“reflect”包
)
//标签
type AnimalTags struct {
Name string `required max:"100"`
Origin string
}
func main() {
//标签
t := reflect.TypeOf(AnimalTags{}) //创建了一个空的结构体
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag)
}
由于必须传入一个对象,所以初始化一个空的Animal类型,并且从这个类型中获取一个字段,使用FieldByName方法,通过询问字段的标签属性来获取标签,得到的结果:
required max:"100"
标签值本身并没有意义,所以需要使用某些验证库来进行解析。我们可以用它来将信息传递到验证框架中。
9 If语句
9.1 基本语法
基本写法,false将不会执行:
if true {
fmt.Println("The test is true")
}
if false {
fmt.Println("The test if false")
}
得到的结果:
The test is true
和其他语言不同,go中不允许使用一个单行块作为if语句的结果进行评估:
if true //这是错误的写法
fmt.Println("The test is true")
即不使用花括号,编译器将会报错:
syntax error: unexpected newline, expecting { after if clause
所以,即使只有一行代码需要执行,也必须使用花括号。
9.2 通常用法
if语句的第一部分是初始化程序,初始化器允许我们运行一个语句并生成一些信息来设置我们在if块中的操作。在初始化中创建的变量将只能作用于块内部,如果在块外使用初始化中定义的变量,将会弹出一个未定义(undefined)的错误。
statePopulations := map[string]int{
"HangZhou": 123456,
"ShangHai": 858765,
"BeiJing": 239475,
}
if pop, ok := statePopulations["HangZhou"]; ok { //使用;分隔两个部分
fmt.Println(pop)
}
注意if语句中使用了“;”分隔前后两个部分,得到的结果:
123456
这里是判断了“HangZhou”是否在map中,检查的结果是存在,则输出对应的值。
9.3 比较
比较没什么好说的,和其他语言一样。如下述所示,这是一个最简单的猜数字游戏:
number := 50
guess := 50
if guess < number {
fmt.Println("Too low")
}
if guess > number {
fmt.Println("Too high")
}
if guess == number {
fmt.Println("You got it!")
}
AND是&&,OR是||,NOT是!。
并且,如果||语句中判断一部分已经返回了true,则不必执行更多的代码,因为它已经知道这个测试将会被通过。这与其他语言也是类似的。
9.4 判断浮点数
处理浮点数时必须非常小心,因为浮点数是十进制的近似值,而不是精确的表示:
//判断浮点数
myNum := 0.123
if myNum == math.Pow(math.Sqrt(myNum), 2) {
fmt.Println("These are the same")
} else {
fmt.Println("These are different")
}
这段代码是对0.123进行了开根号,再取它的平方。一般来说,这样的操作得到的会是一个相同的值,但是得到的结果却是不同:
These are different
所以一般在生成一个错误的值的时候,是检查该错误值是否小于一定的门槛:
//近似值判断浮点数
if math.Abs(myNum/math.Pow(math.Sqrt(myNum), 2)-1) < 0.001 {
fmt.Println("These are the same")
} else {
fmt.Println("These are different")
}
得到的将会是一个正确的结果,但这并不是一个完美的解决办法:
These are the same
问题的关键在于我们必须保证设置的错误参数足够小以捕获这些情况。一般来说,我们不应该比较两个浮点数是否等效。
10 Switch语句
10.1 基本语法
switch和if语句一样,可以使用初始化。同时要注意,如果使用标记语法,case之间的关键字不能重叠,否则会报错:
switch i := 2 + 3; i { //使用;分隔初始化的部分
case 1, 5, 10:
fmt.Println("one, five or ten")
case 2, 4, 6: //不能重叠的意思是:这里不能再出现其他case中的1、5、10这三个数
fmt.Println("two, four or six")
default:
fmt.Println("another number")
}
得到的结果:
one, five or ten
第一部分是初始化设定项,可以为我们生成一个对应的标签。在上述示例中,生成的标签i的值为5,可以匹配第一个case的内容。和if语句一样,使用”;“分隔两个部分。
**值得注意的是,在case中,可以不必使用{}来分隔一个块。**因为switch中的分隔符可以保证它正确地进行分隔。
10.2 比较
可以使用比较运算符和逻辑运算符:
i := 10
switch {
case i <= 10: //输入的i=10满足了这个case
fmt.Println("less than or equal to 10")
case i <= 20: //输入的i=10同时也满足了这个case,同时<=20也也包含了<=10的情况(重叠)
fmt.Println("less than or equal to 20")
default:
fmt.Println("greater tan 20")
}
得到的结果:
less than or equal to 10 //只执行了第1个case
可以看到,switch在同时满足第1个case和第2个case的情况下,只执行了第1个case。同时,与标记语法不同的是,使用标签列表时,关键字的取值范围可以重叠,并且第一个取值为true的case将会被执行。
10.3 break
在其他语言中,可能会需要显式地放置break:
i := 10
switch {
case i <= 10:
fmt.Println("less than or equal to 10")
break
case i <= 20:
fmt.Println("less than or equal to 20")
break
default:
fmt.Println("greater tan 20")
break
}
但是在go中,break其实是隐含的,因为在case中都会有正确的中断。
如果想提前退出switch的运行,也可以使用break:
j = 1
switch j.(type) {
case int:
fmt.Println("i is an int")
break
fmt.Println("hello world") //这条语句将不会被执行
case float64:
fmt.Println("i is a float64")
case string:
fmt.Println("i is string")
case [2]int:
fmt.Println("i is [2]int")
default:
fmt.Println("i is another type")
}
得到的结果:
i is an int
可以看到在满足了int类型的case下,并不会打印出”hello world“。
10.4 fallthrough
在10.2的例子中,在满足两个case的条件下,我们只执行了第一个case的内容,如果确实需要执行第二个case,可以使用fallthrough:
i = 10
switch {
case i <= 10:
fmt.Println("less than or equal to 10")
fallthrough
case i <= 20: //此时i=10满足case1和case2
fmt.Println("less than or equal to 20")
default:
fmt.Println("greater tan 20")
}
得到的结果为:
less than or equal to 10
less than or equal to 20
但是如果使用了fall through,即使第二个case的条件不满足,也会执行该case。因为实际上是取消了break的中断:
i = 10
switch {
case i <= 10:
fmt.Println("less than or equal to 10")
fallthrough
case i >= 20: //此时i=10满足case1但不满足case2
fmt.Println("less than or equal to 20")
default:
fmt.Println("greater tan 20")
}
得到的结果依旧为:
less than or equal to 10
less than or equal to 20
如果不是确定需要跟进的用例,否则不要经常使用fall through,因为这意味着我们必须对控制流负责。
10.5 特殊语法(Type switches)
可以在空接口上添加.(type)来实现一个特殊语法,即获取该接口的基础类型。
var j interface{} = [3]int{}
switch j.(type) {
case int:
fmt.Println("i is an int")
case float64:
fmt.Println("i is a float64")
case string:
fmt.Println("i is string")
case [2]int:
fmt.Println("i is [2]int")
default:
fmt.Println("i is another type")
}
得到的结果为:
i is another type
如果将j改为其他值,将会返回对应的结果:
j = 1 -> i is an int
j = 1.1 -> i is a float64
j = "1" -> i is syring
j = [2]int{} -> i is [2]int
11 循环
11.1 简单循环
for i := 0; i < 5; i++ {
fmt.Println(i)
}
得到的结果:
0
1
2
3
4
11.1.1 定义多个参数
在其他语言中,也许在初始化程序中这样定义两个参数是允许的,但是在go中是非法的:
for i := 0, j := 0; i < 5; i++, j++ { //定义了i和j,循环i和j都++
fmt.Println(i)
}
编译器会报错:
syntax error: unexpected :=, expecting {
原因是”,“不能用来分隔两个语句,所以要将i和j写成同一条语句。此外,增量操作i++和j++不是表达式,而是会被认为是两条语句,而在for循环中这里只能出现一条语句,所以同时做到这两点的方法是也将它们写成同一条语句:
for i, j := 0, 0; i < 5; i, j = i+1, j+1 {
fmt.Println(i, j)
}
这样就可以正确运行了:
0 0
1 1
2 2
3 3
4 4
11.1.2 for循环中的;
除了在for中进行初始化,还可以在程序的其他地方进行初始化,但是要注意不能去掉第一个”;“,否则将会报错:
i := 0
for ; i < 5; i++ { //第一个;不能丢
fmt.Println(i)
}
除此之外,可以将计数器放到for循环中,但是也要注意不能去掉第二个”;“,否则会报错:
i := 0
for ; i < 5; { //在有第一个;的情况下,第二个;不能丢
fmt.Println(i)
i++
}
还有一种简洁的写法,即第一个和第二个;同时不写的情况下,编译可以通过,此时for循环做一个比较的操作:
i := 0
for i < 5 { //两个;要么同时保留要么同时丢弃
fmt.Println(i)
i++
}
11.2 提前退出
11.2.1 无限循环(break)
在一般情况下,如果进行无限循环将会报错:
i := 0
for {
fmt.Println(i)
i++
}
但是,在某些情况下我们确实需要进行无限循环,直到我们期待的事件发生,此处假定i的值为5时退出循环:
i := 0
for {
fmt.Println(i)
i++
if i == 5 {
break
}
}
我们第一次看到break是在switch中,但是其实break在for循环中经常被使用,尤其是这种无限循环。
11.2.2 continue
continue的用法是退出循环的这个迭代并重新进入循环:
for k := 0; k < 10; k++ {
if k%2 == 0 {
continue
}
fmt.Println(k)
}
这是一个跳过偶数,只打印奇数的示例。得到的结果:
1
3
5
7
9
continue并不经常被使用,但是它非常有用。
11.2.3 Labels标签
如果简单地使用break,它将只会跳出最近的一个循环:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
fmt.Println(i * j)
if i*j >= 3 {
break
}
}
}
得到的结果:
1
2
3
2
4
3
如果使用Labels标签,此处定义一个Loop,再break Loop,将会跳出Loop包含内层和外层的所有循环:
Loop:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
fmt.Println(i * j)
if i*j >= 3 {
break Loop
}
}
}
得到的结果:
1
2
3
11.3 循环遍历集合
11.3.1 range
对数组和切片使用range,可以不必关心它们的大小:
s := []int{1, 2, 3}
for k, v := range s {
fmt.Println(k, v)
}
得到的结果:
0 1
1 2
2 3
其中k表示索引的值(关键字key),v表示对应的值(value)。
对map使用range:
statePopulation := map[string]int{
"BeiJing": 1234567,
"ShangHai": 39459,
}
for k, v := range statePopulation {
fmt.Println(k, v)
}
得到的结果:
BeiJing 1234567
ShangHai 39459
对字符串使用range:
str := "hello Go!"
for k, v := range str {
fmt.Println(k, v, string(v))
}
得到的结果:
0 104 h
1 101 e
2 108 l
3 108 l
4 111 o
5 32
6 71 G
7 111 o
8 33 !
直接输出v将会得到字符的Unicode表示,可以使用string进行转换。
11.3.2 在range中使用_
如果我们只想获得某些值,可以使用之前使用过的_,比如在map样例中:
statePopulation := map[string]int{
"BeiJing": 1234567,
"ShangHai": 39459,
}
for _, v := range statePopulation {
fmt.Println(v)
}
得到的结果:
1234567
39459
但是要注意,如果不使用_,将会出现错误。
12 控制
Control Flow将会改变程序的执行流。
12.1 延迟(Defer)
延迟将会在函数的结尾后输出:
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
得到的结果:
1
3
2
实际上是在退出当前函数的时候,查看是否有任何要被调用的延迟函数。
如果有多个defer函数,将会遵循**“后进先出”**的原则:
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
得到的结果:
3
2
1
这种设计是合理的,我们可能会经常使用defer来关闭打开的资源,而以打开资源相反的顺序关闭是有道理的,因为一种资源可能会依赖在它之前打开的资源。
此外,defer是在当时就直接接受参数,而不会在它改变之后跟着改变:
a := "1"
defer fmt.Println(a)
a = "2"
我们可能会期待defer出来的结果是2,但事实上是:
1
defer并不会关注正在变化的值。
必须警惕的是,在循环中使用defer要格外小心,我们必须显式地直接关闭资源,否则在循环中使用defer,所有打开的资源都将保持打开状态直到程序结束以后,这可能会导致不可预知的内存问题。
12.2 Panic
12.2.1 出现Panic
在很多时候,程序会出现运行不下去的情况,比如说除数为0:
a, b := 1, 0
ans := a / b
fmt.Println(ans)
会得到一个Panic的结果:
panic: runtime error: integer divide by zero
可以手动抛出一个panic:
fmt.Println("1")
panic("something bad happened") //程序会在这里退出
fmt.Println("2") //该语句将不会被执行
得到的结果:
1
panic: something bad happened
在抛出panic之后的程序将不会被执行。
12.2.2 panic和defer的关系
panic运行在defer之后:
fmt.Println("1")
defer fmt.Println("defer") //先运行defer
panic("something bad happened") //再运行panic
fmt.Println("2")
得到的结果为:
1
defer
panic: something bad happened
这个问题的重要性在于,如果应用程序崩溃,任何延迟关闭的调用都会在panic之前执行,可以很好地保存数据。
12.3 Recover
**recover只在deferred中才有作用。**因为当应用程序出现panic时,它将不会执行任何程序,但是会执行defer函数。
recover函数要做的是,如果应用程序没有出现panic,它将返回nil。但是如果它出现了nil,则将会返回实际导致应用程序的错误panic,此时我们需要记录这个错误。
fmt.Println("1")
defer func() {
if err := recover(); err != nil {
log.Panicln("Error:", err)
}
}()
panic("something bad happened")
fmt.Println("2")
得到的结果:
1
2022/01/04 20:30:34 Error: something bad happened
panic: something bad happened [recovered]
panic: Error: something bad happened
如果在更深层的函数调用栈中使用panic、defer和recover,应用程序只会终止发生panic的那个函数,因为它已经处于无法运行的不可靠状态,但是应用程序仍然会返回上方调用recover函数的函数调用栈继续执行,因为recover函数表明应用程序处于可以继续执行的状态,结果就好像什么错误都没有发生一样。
但是,如果遇到了无法处理的panic,并且试图用recover来恢复它,我们仍然可以通过再次调用panic函数来重新抛出该panic,并进一步管理该panic,此时,panic将会向上传播,应用程序则将不会返回上层函数继续执行。
13 指针(Pointers)
13.1 定义指针
不使用指针,go将会对赋值操作创建一个副本,如果使用指针,将会使用地址操作符&来指向同一份基础数据,以下是一个基本示例:
var a int = 42
var b *int = &a
fmt.Println(a, b)
fmt.Println(&a, b)
fmt.Println(a, *b)
a = 27
fmt.Println(a, *b)
得到的结果:
42 0xc000016098
0xc000016098 0xc000016098
42 42
27 27
改变的是同一份数据。同时可以输出内存地址。
13.2 数组与指针
a := [3]int{1, 2, 3}
b := &a[0]
c := &a[1]
fmt.Printf("%v %p %p\n", a, b, c)
得到的结果:
[1 2 3] 0xc0000ae090 0xc0000ae098
可以看到b和c之间的地址相差了4,是因为int类型占4个字节。
如果是在其他语言(C或C++)中,可以对指针进行运算,但是在go中不允许这样做:
a := [3]int{1, 2, 3}
b := &a[0]
c := &a[1] - 4 //不允许进行运算
fmt.Printf("%v %p %p\n", a, b, c)
按理来说,c应该得到和b一样的值,但是编译器报错:
invalid operation: &a[1] - 4 (mismatched types *int and int)
13.3 struct与指针
不使用指针:
type myStruct struct {
foo int
}
var ms myStruct //不使用指针
ms = myStruct{foo: 42}
fmt.Println(ms)
得到的结果:
{42}
将ms改为指针类型:
type myStruct struct {
foo int
}
var ms *myStruct //改为指针类型
ms = &myStruct{foo: 42}
fmt.Println(ms)
得到的结果:
&{42}
此时ms实际上是存储了结构体的地址信息。
要将变量初始化未指向对象的指针,还可以使用内置的new函数,但是要注意,如果使用new函数,则不能进行初始化操作,只能创建一个空的对象:
//new函数
type myStruct struct {
foo int
}
var ms *myStruct
ms = new(myStruct)
fmt.Println(ms)
得到的结果是一个0值:
&{0}
注意在new函数中不能使用初始化语法来初始化对象。
13.4 nil
在13.3的示例中,使用了new函数,这个时候获取到的是一个零值,此时指针并不指向任何数据,那么指针就是零值:
type myStruct struct {
foo int
}
var ms *myStruct
fmt.Println(ms) //在定义之后立即打印这个指针
ms = new(myStruct)
fmt.Println(ms)
得到的结果:
<nil>
&{0}
解引用运算符的优先级实际上低于点运算符,所以一般来说,应该这样使用指针:
type myStruct struct {
foo int
}
var ms *myStruct
ms = new(myStruct)
(*ms).foo = 42
fmt.Println((*ms).foo)
得到的结果:
42
但是每次都使用(*ms).foo太麻烦了,我们可以直接使用来使用它,这实际上是个语法糖:
type myStruct struct {
foo int
}
var ms *myStruct
ms = new(myStruct)
= 42
()
一样可以得到相同的结果:
42
指针ms实际上并没有foo字段,而是指向一个结构体,这个结构体具有foo字段。
13.5 切片与指针
之前在数组和切片中做过一个示例:
a := [3]int{1, 2, 3}
b := a
fmt.Println(a, b)
a[1] = 42
fmt.Println(a, b)
将a赋值给b,实际上是为b创建一个a的副本,a的值改变不会影响到b:
[1 2 3] [1 2 3]
[1 42 3] [1 2 3]
如果把a改为切片,则a和b都指向同一片基础数据:
a := []int{1, 2, 3}
b := a
fmt.Println(a, b)
a[1] = 42
fmt.Println(a, b)
a的值改变会影响到b:
[1 2 3] [1 2 3]
[1 42 3] [1 42 3]
所以切片不包含数据本身,而是包含一个指针。
13.6 map与指针
7.6的示例已经说明,map类型是一种指针。
综上,在处理slice和map的时候必须非常小心,因为在传递map和slice的过程中,应用程序可能会让我们陷入数据以意想不到的的方式变化的情况。
14 函数
14.1 命名规则
func函数的命名规则和其他类型一样,使用Pascal或者驼峰大小写作为函数的名称,这取决于我们是否想要使用大小写来确定其可见性。
14.2 奇怪的规定
在其他语言中,{}的位置一般是可以随意放置的,而在go中,{必须和func放在同一行:
func main() //如果我这样写,就会报错
{
fmt.Println("hello shark")
}
编译器报错:
.\:5:6: missing function body
.\:6:1: syntax error: unexpected semicolon or newline before {
且}必须单独一行:
func main() { //如果我这样写,编译器(至少JB的GOland)在编译的时候会自动帮我把}放到下一行
fmt.Println("hello shark")}
如果这样写,编译器(至少JB的GOland)不会报错,而是会在编译的时候会自动把}放到下一行,然后通过编译。
14.3 传入参数
传入一个字符串,注意参数除了不需要使用var以外,与正常的参数类型写法一致。
func main() {
i := 1
sayMessage("hello shark", i)
}
func sayMessage(msg string, idx int) {
fmt.Println(msg, idx)
}
得到的结果:
hello shark 1
go提供了一些语法糖,比如说可以使用更简洁的传参方式来取代冗长的参数定义,当我们定义两个string类型的时候,我们可以这样定义:
func sayGreeting(greeting string, name string)
但是也可以这样定义:
func sayGreeting(greeting, name string)
14.4 使用指针
不使用指针传递参数:
func main() {
name := "shark"
useCopy(name)
fmt.Println(name)
}
func useCopy(name string) {
name = "ok"
fmt.Println(name)
}
在函数内部改变值将不会影响函数外部:
ok //userCopy打印出的内容
shark //main打印出的内容
使用指针传递参数:
func main() {
name := "shark"
usePointer(&name)
fmt.Println(name)
}
func usePointer(name *string) {
*name = "ok"
fmt.Println(*name)
}
函数内部改变值将会影响函数外部:
ok //usePointer打印出的内容
ok //main打印出的内容
注意,如果是传递slice和map,则总是在传递指针。
14.5 可变参数
…是告诉go在运行时接受所有传入的参数,并将它们包装成一个切片:
func main() {
//可变参数
sum("sum is ", 1, 2, 3, 4, 5)
}
func sum(msg string, values ...int) { //传入一个string类型和一个int类型的可变参数
fmt.Println(values) //打印切片
result := 0
for _, v := range values { //计算sum的值
result += v
}
fmt.Println(msg, result)
}
得到的结果:
[1 2 3 4 5]
sum is 15
但是要注意,可变参数只能使用一个,并且必须放在所有参数声明的最后。
14.6 返回函数
在上一个例子中,稍微更改一下程序,可以通过返回函数得到相同的结果:
func main() {
//可变参数
s := sum(1, 2, 3, 4, 5)
fmt.Println("sum is ", s)
}
func sum(msg string, values ...int) int { //传入一个string类型和一个int类型的可变参数
fmt.Println(values) //打印切片
result := 0
for _, v := range values { //计算sum的值
result += v
}
return result //返回结果
}
go提供一个可以隐式返回结果的语法糖,我们只需要告诉函数在哪个位置返回即可:
func main() {
//可变参数
s := sum(1, 2, 3, 4, 5)
fmt.Println("sum is ", s)
}
func sum(msg string, values ...int) (result int) { //这里定义返回结果result
fmt.Println(values) //打印切片
//在上例中这里定义了result := 0,在本例中已经不需要
for _, v := range values { //计算sum的值
result += v
}
return //告知这里将要返回,但不必指定返回名称
}
14.6 抛出错误
package main
import "fmt"
func main() {
d, err := divide(5.0, 0.0) //0做除数
if err != nil { //如果返回nil说明没有错误,如果没有返回nil则打印错误信息
fmt.Println(err)
return
}
fmt.Println(d)
}
func divide(a, b float64) (float64, error) {
if b == 0.0 { //如果除数为0,则提前结束程序,并返回错误信息
return 0.0, fmt.Errorf("Cannot divide by zero")
}
return a / b, nil
}
得到的结果:
Cannot divide by zero
14.7 匿名函数
以下是一个最简单的匿名函数:
package main
import "fmt"
func main() {
var fun = func() { //定义一个匿名函数
fmt.Println("hello shark")
}
fun() //执行这个函数
}
在14.6的除法函数中,我们可以在函数内部这样定义divide函数:
func main() {
var divide func(float64, float64) (float64, error)
}
14.8 方法
方法类似于函数的写法,但又有些不同:
package main
import "fmt"
type greeter struct { //定义一个结构体greeter
greeting string
name string
}
func (g greeter) greet() { //定义一个属于greeter结构体的方法
fmt.Println(g.greeting, g.name)
g.name = "abc" //在方法中更改greeter结构体中name字段的值
}
func main() {
g := greeter{
greeting: "hello",
name: "shark",
}
g.greet() //执行方法
fmt.Println("new name: ", g.name)
}
如果想要方法只访问其父类型的数据而不能改变它们的值,那么就要指定的是值类型而不使用指针,但是要知道这样做的代价是非常大的,因为每次调用这个方法都将会创建出这个结构的一个副本。可以看到,在方法内部更改结构体的值将不会改变main中结构体的值:
hello shark
new name: shark
但是如果传入的是指针类型,在结构体中的操作将会更改底层数据:
func (g *greeter) greet() { //定义一个属于greeter结构体的方法,并且传入的是指针类型
fmt.Println(g.greeting, g.name)
g.name = "abc" //在方法中更改greeter结构体中name字段的值
}
得到的结果为:
hello shark
new name: abc
在操作大型的数据结构的时候,传入指针将会变得非常有效率,因为不必复制一整个结构体。但是要注意,map和slice原本就是指针类型,所以对它们做的任何操作都会影响到底层数据。
为结构体添加一些方法是最经典的用法,但是方法不仅仅可以定义在结构体上,也可以添加到任意的类型上(整数、字符串等)。
15 接口
15.1 基本概念
先看一个struct的示例:
type Docter struct {
Number int //如果要export,使用Pascal命名
actorName string //如果是import,则使用驼峰命名
companions []string
episodes []string
}
接口的定义方式和结构体差不多,但是struct最终是一个数据容器,而接口并不描述数据,而是描述行为:
package main
import "fmt"
type Writer interface { //定义接口
Write([]byte) (int, error) //接收byte的字符切片,返回int和error
}
type ConsoleWriter struct { //定义结构体
}
func (cw ConsoleWriter) Write(data []byte) (int, error) { //定义方法
n, err := fmt.Println(string(data))
return n, err
}
func main() {
var w Writer = ConsoleWriter{}
w.Writer([]byte("Hello shark"))
}
得到的结果:
Hello shark
15.2 接口组合
type Writer interface {
Write([]byte) (int, error)
}
type Closer interface {
Closer() error //定义了一个封闭的方法,返回值为一个错误
}
type WriterCloser interface {
Writer
Closer
}
定义了一个Writer接口和一个Closer接口,Closer定义了一个封闭的方法,返回值为一个错误,以防万一我们尝试时发生了错误的使用。再定义了一个组合接口WriterCloser,这与嵌入类似,可以在接口中嵌入另一个接口。
type BufferedWriterCloser struct { //定义结构体
buffer *bytes.Buffer //缓冲区
}
func (bwc *BufferedWriterCloser) Write(data []byte) (int, error) {
n, err := bwc.buffer.Write(data) //类型的读写操作,将传入的参数存到缓冲区内
if err != nil {
return 0, err
}
v := make([]byte, 8) //制作长度为8的切片
for bwc.buffer.Len() > 8 { //如果长度大于8,则输出
_, err := bwc.buffer.Read(v) //读出8个字符
if err != nil { //判断是否发生错误
return 0, err
}
_, err = fmt.Println(string(v)) //未发生错误,则打印读出的8个字符
if err != nil {
return 0, err
}
}
return n, nil
}
func (bwc *BufferedWriterCloser) Close() error {
for bwc.buffer.Len() > 0 { //如果缓冲区内还有内容,就输出8个字符
data := bwc.buffer.Next(8)
_, err := fmt.Println(string(data))
if err != nil {
return err
}
}
return nil
}
在最底层,为了确保能够正确的初始化,定义一个构造函数用于刷新缓冲区:
func NewBufferedWriterCloser() *BufferedWriterCloser {
return &BufferedWriterCloser{
buffer: bytes.NewBuffer([]byte{})
}
}
main函数:
func main() {
var wc WriterCloser = NewBufferedWriterCloser() //初始化
wc.Write([]byte("Hello YouTube listeners, this is a test")) //写入数据
wc.Close() //关闭处理
}
得到的结果:
Hello Yo
uTube li
steners,
this is
a test
15.3 类型转换
在上一个例子的基础上,在main函数中增加两行代码:
bwc := wc.(*BufferedWriterCloser)
fmt.Println(bwc)
得到的结果:
Hello Yo
uTube li
steners,
this is
a test
&{0xc00010e3c0}
可以看到成功进行了类型转换。如果失败,将会返回一个panic。
15.3.1 空接口
var myObj interface{} = NewBufferedWriterCloser() //定义了一个空接口
if wc, ok := myObj.(WriterCloser); ok { //进行类型转换
wc.Write([]byte("Hello YouTube listeners, this is a test")) //写入数据
wc.Close() //关闭处理
}
r, ok := myObj.(io.Reader)
if ok {
fmt.Println(r)
} else {
fmt.Println("Conversion failed")
}
得到的结果:
Hello Yo
uTube li
steners,
this is
a test
Conversion failed
这个空接口对的类型转换失败了,而对WriterCloser的类型转换成功了。
15.3.2 类型switch
这个与10.5的内容极其相似,主要是用到了空接口,重新温习一下:
var j interface{} = 0 //这里用到了空接口
switch j.(type) {
case int:
fmt.Println("i is an int")
case string:
fmt.Println("i is string")
default:
fmt.Println("i is another type")
}
得到的结果:
i is an int
15.4 值类型(values)与指针类型(pointers)
这里设置一个不完整的样例,把15.2样例中的所有逻辑处理都删除,使得代码更加简洁,只需关注其中的值类型和指针类型的变化:
type Writer interface { //不必关注
Write([]byte) (int, error)
}
type Closer interface { //不必关注
Close() error
}
type WriterCloser interface { //不必关注
Writer
Closer
}
type BufferedWriterCloser struct { //不必关注
buffer *bytes.Buffer
}
//注意,这里的bwc使用了值类型
func (bwc BufferedWriterCloser) Write(data []byte) (int, error) {
return 0, nil
}
//注意,这里的bwc使用了值类型
func (bwc BufferedWriterCloser) Close() error {
return nil
}
func main() {
var wc WriterCloser = BufferedWriterCloser{} //值类型可以覆盖此时所有的方法集
fmt.Println(wc)
}
得到的结果:
{<nil>}
一切运转正常,但是将其中的代码稍微更改一下:
type Writer interface { //不必关注
Write([]byte) (int, error)
}
type Closer interface { //不必关注
Close() error
}
type WriterCloser interface { //不必关注
Writer
Closer
}
type BufferedWriterCloser struct { //不必关注
buffer *bytes.Buffer
}
//注意,这里的bwc使用了指针类型
func (bwc *BufferedWriterCloser) Write(data []byte) (int, error) {
return n, nil
}
//注意,这里的bwc使用了值类型
func (bwc BufferedWriterCloser) Close() error {
return nil
}
func main() {
var wc WriterCloser = BufferedWriterCloser{} //此时值类型不能覆盖所有的方法集
fmt.Println(wc)
}
编译器运行出错:
cannot use BufferedWriterCloser{} (type BufferedWriterCloser) as type WriterCloser in assignment:
BufferedWriterCloser does not implement WriterCloser (Write method has pointer receiver)
这是因为接口中的方法集出现了问题,即单一的值类型的转换并不能覆盖它所有的方法集(有值类型和指针类型两种),如果此时将main函数中的代码更改为:
func main() {
var wc WriterCloser = &BufferedWriterCloser{} //这里的类型做了更改&
fmt.Println(wc)
}
得到一个正确的结果:
&{<nil>}
此时再次修改一下方法中的类型,为了更好地观察,只放出做了修改的部分:
//注意,在上例中这里的bwc使用了指针类型,重新改为值类型
func (bwc BufferedWriterCloser) Write(data []byte) (int, error) {
return n, nil
}
//注意,这里的bwc使用了值类型
func (bwc BufferedWriterCloser) Close() error {
return nil
}
func main() {
var wc WriterCloser = &BufferedWriterCloser{} //注意,这里的类型并没有改回去
fmt.Println(wc)
}
一样可以得到正确的结果:
&{<nil>}
说明指针类型可以覆盖值类型的所有方法集。
此时再次修改一下方法中的类型,为了更好地观察,只放出做了修改的部分:
//注意,这里的bwc使用了指针类型
func (bwc *BufferedWriterCloser) Write(data []byte) (int, error) {
return n, nil
}
//注意,这里的bwc使用了指针类型
func (bwc *BufferedWriterCloser) Close() error {
return nil
}
func main() {
var wc WriterCloser = &BufferedWriterCloser{} //注意,这里的类型并没有改回去
fmt.Println(wc)
}
一样可以得到正确的结果:
&{<nil>}
说明指针类型可以覆盖值类型、指针类型的所有方法集。
至此,可以得出结论:
①如果方法集中仅仅使用了值类型,那么在类型转换的时候,可以使用值类型或者指针类型;
②如果方法集中仅仅使用了指针类型,那么在类型转换的时候,只能使用指针类型;
③如果方法集中不仅使用了值类型也使用了指针类型,那么在类型转换的时候,只能使用指针类型;
16 Goroutine
16.1 创建Goroutine
在函数前打上关键字“go”来创建绿色线程。在go中,并不是创建庞大的重开销线程,而是创建一个线程的抽象,称其为goroutine:
package main
import "fmt"
func sayhello() {
fmt.Println("Hello")
}
func main() {
go sayhello()
}
但是在上例中,并不能打印出“Hello”,因为main在产生一个goroutine之后,应用程序就会退出,所以函数根本没有时间打印出它的消息,需要让主进程延迟一会:
package main
import (
"fmt"
"time"
)
func sayhello() {
fmt.Println("Hello")
}
func main() {
go sayhello()
time.Sleep(100 * time.Millisecond) //延迟
}
使用匿名函数打印Hello,但是在睡眠之前,将msg的值修改为Goodbye:
func main() {
var msg = "Hello"
go func(msg string) {
fmt.Println(msg)
}(msg)
msg = "Goodbye"
time.Sleep(100 * time.Millisecond)
}
得到的结果为:
Goodbye
可以发现,goroutine并不会中断主线程,直到它遇到了一个睡眠的调用。这意味着,即使main函数启动了一个goroutine,但实际上并没有马上执行,而是继续运行main函数,并且要打印的内容在接下来被重新赋值,打印出来的时被修改后的值,这实际上创造了所谓的竞争条件。
要想避免这种情况,可以直接进行传送参数:
func main() {
var msg = "Hello"
go func(msg string) {
fmt.Println(msg)
}(msg)
msg = "Goodbye"
time.Sleep(100 * time.Millisecond)
}
这样得到的就是一个修改前的结果:
Hello
这样的做法其实是将main函数中的msg变量和go例程进行了解耦,因为要打印的这条消息实际上是在传递时创建的副本,这通常是传递数据时使用的方式。
现在,这个go例程运行地很好,但是并不是最佳的实践方式,因为使用了一个睡眠调用函数。我们实际上将应用程序性能和应用程序时钟周期绑定到实际的世界时钟,这是非常不可靠的。所以为了解决这个问题,我们实际上可以使用权重组。
16.2 同步机制(Synchronization)
16.2.1 权重组(WaitGroups)
创建一个变量,然后从同步包中提取它。权重组的目的是将多个go例程同步在一起。将go例程添加到权重组以后们就不需要sleep函数了,而是将它替换为等待权重组的完成,在go例程中,可以告诉权重组它实际上已经完成了它的执行:
var wg = sync.WaitGroup{}
func main() {
var msg = "Hello"
wg.Add(1) //增加权重组
go func(msg string) {
fmt.Println(msg)
wg.Done() //告知权重组已经执行完成
}(msg)
msg = "Goodbye"
wg.Wait() //等待线程的完成
}
得到的结果:
Hello
得到的结果和上例相同,但是不再依赖滚动时钟,而是用足够的时间去完成,然后关闭。
16.2.2 互斥锁(Mutexes)
在下面这个例子中,循环了十次,但实际上创建了二十个go例程,因为每次循环会在权重组中添加两个。
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
var counter = 0 //计数器
func sayhello() { //打印计数器
fmt.Printf("Hello #%v\n", counter)
wg.Done()
}
func increment() { //计数器递增
counter++
wg.Done()
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(2) //每次增加两个线程
go sayhello()
go increment()
}
wg.Wait()
}
一般来说,我们应该期望得到这样一个结果,即“Hello #0”、“Hello #1”……顺序递增地输出,但是得到的是一个糟糕的结果,并且每次运行的结果都会改变:
Hello #0
Hello #4
Hello #5
Hello #6
Hello #7
Hello #2
Hello #8
Hello #8
Hello #9
Hello #2
这是因为这里发生的事情实际上是go例程之间在相互竞争,而各个go例程之间没有同步导致的。每一个go例程只是为了尽可能快地完成任务。所以为了解决这个问题,需要使用互斥锁的概念。
引入一个简单的读写互斥锁
package main
import (
"fmt"
"runtime"
"sync"
)
var wg = sync.WaitGroup{}
var counter = 0 //计数器
var m = sync.RWMutex{}
func main() {
runtime.GOMAXPROCS(100)
for i := 0; i < 10; i++ {
wg.Add(2) //每次增加两个线程
m.RLock() //上锁
go sayhello()
m.Lock() //上锁
go increment()
}
wg.Wait()
}
func sayhello() { //打印计数器
fmt.Printf("Hello #%v\n", counter)
wg.Done()
m.RUnlock() //解锁
}
func increment() { //计数器递增
counter++
m.Unlock() //解锁
wg.Done()
}
得到正确的结果:
Hello #0
Hello #1
Hello #2
Hello #3
Hello #4
Hello #5
Hello #6
Hello #7
Hello #8
Hello #9
但是,这个应用程序引出了新的问题,因为我们基本上已经完全破坏了并发和这个应用程序中的并行性,因为所有的互斥体都在强制数据以单线程方式同步和运行,所以这个进程可能比没有go例程的性能更差,因为我们需要额外的开销来处理这些互斥锁。
16.3 并发(Parallelism)
这个简单的程序可以返回操作程序在机器上可用的核心数:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("Thread: %v", runtime.GOMAXPROCS(-1))
}
得到的结果:
16
17 Channel
17.1 基本概念
channel使用内置的make函数创建,使用channel关键字来表示要创建一个通道,并且指定流经它的数据流的类型。通道的类型是强类型的,这意味着如果指定了int,那么只能发送整数来通过这个通道。
定义了两个匿名函数的goroutine,第一个go例程为匿名函数,它从通道接收数据;第二个例程实际上将成为发送程序:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int) //创建一个channel,指定的数据流为int
wg.Add(2)
go func() { //接收channel中的数据到i,并且打印它的值
i := <-ch
fmt.Println(i)
wg.Done()
}()
go func() {
i := 42
ch <- i //向channel中输入数据
i = 27 //更改i的值,看看对channel中的数据是否造成影响
wg.Done()
}()
wg.Wait()
}
得到的结果为:
42
开始时设置i=42,然后在传入i后重新分配i=27,但是它并不会影响传送入channel中的值,因为就像所有其他变量一样,当我们传递数据进入通道时,实际上传递的是数据的副本,接收go例程并不关心我们是否更改了变量。
goroutine的另一个常见用例是如果有需要异步处理的数据,那么我们可能希望有不同数量的go例程将数据发送到通道中,对以上的程序做一个小的修改:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int) //创建一个channel,指定的数据流为int
for j := 0; j < 5; j++ {
wg.Add(2)
go func() { //接收channel中的数据到i,并且打印它的值
i := <-ch
fmt.Println(i)
wg.Done()
}()
go func() {
ch <- 42 //向channel中输入数据
wg.Done()
}()
}
wg.Wait()
}
实际上是在此循环内创建go例程,在此生成了10个goroutine,五个发送方和五个接收方,并且所有这些都将使用这个单一的channel来传达它们的信息:
42
42
42
42
42
在这种情况下,可以正确的运行,但是如果将发送方放到循环外面,将会出现错误:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int) //创建一个channel,指定的数据流为int
//发送方放到了循环外面
go func() { //接收channel中的数据到i,并且打印它的值
i := <-ch
fmt.Println(i)
wg.Done()
}()
for j := 0; j < 5; j++ {
wg.Add(2)
go func() {
ch <- 42 //实际上会在这一行暂停go例程的执行,直到通道中有可用的空间
wg.Done()
}()
}
wg.Wait()
}
得到的结果为:
42
fatal error: all goroutines are asleep - deadlock!
出现错误的原因是只有五个发送方,却只有一个接收方,所以我们只能接收到一个信息,然后造成了死锁,因为发送方试图将消息放入通道,但是默认使用了无缓冲的通道,于是在第二个发送go例程会阻塞,因为我们的应用程序中没有任何东西会接收该消息,然后go会杀死这个进程,因为它意识到出现了问题。
17.2 限制数据流
我们可以把go例程设计为仅在一个方向上处理通道数据,下面这个例子使用了两个go例程,分别向channel中输入数据和取出数据:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int) //创建一个channel,指定的数据流为int
wg.Add(2)
go func() {
fmt.Println(<-ch) //从channel中取出数据
ch <- 27 //向channel中输入数据
wg.Done()
}()
go func() {
ch <- 42 //向channel中输入数据
fmt.Println(<-ch) //从channel中取出数据
wg.Done()
}()
wg.Wait()
}
得到的结果:
42
27
可以看到使用channel实现了同步。即如果第二个例程还未往通道里放入数据42,则第一个例程想要取出数据的时候会被阻塞,直到42和27依次被放入通道。
在上个例子的基础上,将通道修改为只读和只写:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int) //创建一个channel,指定的数据流为int
wg.Add(2)
go func(ch <-chan int) { //接收通道
fmt.Println(<-ch) //从channel中取出数据
ch <- 27 //向channel中输入数据
wg.Done()
}(ch)
go func(ch chan<- int) { //发送通道(数据放入)
ch <- 42 //向channel中输入数据
fmt.Println(<-ch) //从channel中取出数据
wg.Done()
}(ch)
wg.Wait()
}
运行程序,编译器将会报错:
invalid operation: ch <- 27 (send to receive-only type <-chan int)
invalid operation: <-ch (receive from send-only type chan<- int)
第一个错误是我们试图在接收通道(receive-only)中放入数据,这样做是无效的行为;类似的,第二个错误是试图在发送通道中接收数据(send-only)。
17.3 缓冲(Buffered)
针对17.1中出现的有五个发送方和只有一个接收方而造成的死锁问题,可以使用缓冲来解决。先将for循环问题简化:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int) //创建一个channel,指定的数据流为int
wg.Add(2)
go func(ch <-chan int) { //接收通道
fmt.Println(<-ch) //从channel中取出数据
wg.Done()
}(ch)
go func(ch chan<- int) { //发送通道(数据放入)
ch <- 42 //向channel中放入两个数据42、27
ch <- 27 //这个数据不能够被处理,将会引发错误
wg.Done()
}(ch)
wg.Wait()
}
这里简化了五个发送方,而是向通道中放入两个数据,而只取出一次,等价于多个发送方和一个接收方,可以看到得到的输出和之前一样,也是发生了死锁:
42
fatal error: all goroutines are asleep - deadlock!
为了解决这个问题,需要在创建通道时输入第二个参数,即添加一个缓冲区:
ch := make(chan int, 50) //创建一个channel,指定的数据流为int,缓冲区大小为50
得到的结果:
42
但是注意到这里仍然有一点问题,因为有一条数据(27)丢失了。它在某种意义上解决了一个问题,但是创造了一个新的问题。但这并不是缓冲引起的。
缓冲通道的真正设计目的是如果发送方或接收方在两侧以不同的频率运行,我们可以从传感器中检索数据以及解决突发传输。接收方可以接收一定数量的数据并进行处理,同时不必在处理的时候阻塞发送方。
17.4 循环
在通道的循环中,情况变得稍微有些不一样,回顾一下在11.3.1节中,如何在数组中使用range:
s := []int{1, 2, 3}
for k, v := range s
第一个参数k是索引,第二个参数v才是值,而在通道中,直接使用一个参数表示取到的值:
go func(ch <-chan int) { //接收通道
for i := range ch {
fmt.Println(i) //从channel中取出数据
}
wg.Done()
}(ch)
在17.3两个发送方与一个接收者的缓冲问题中,我们利用缓冲解决了一个死锁问题,但是造成了数据的丢失,现在我们试图使用上面这个循环代码替换接收者来获取通道内的值:
42
27
fatal error: all goroutines are asleep - deadlock!
所有数据都正常地被取出,但是再次发生了死锁问题。发生问题的原因是在循环中陷入了僵局,因为除了取出数据外,还在继续监听其他消息,例程不知道该如何退出这个循环范围,进而造成了死锁问题。所以必须使用关闭通道来终止这个循环。
17.4 关闭
在发送方结束发送数据以后,关闭这个通道,接收方的循环将会检测到这一点:
package main
import (
"fmt"
"sync"
)
var wg = sync.WaitGroup{}
func main() {
ch := make(chan int, 50) //创建一个channel,指定的数据流为int
wg.Add(2)
go func(ch <-chan int) { //接收通道
for i := range ch {
fmt.Println(i) //从channel中取出数据
}
wg.Done()
}(ch)
go func(ch chan<- int) { //发送通道(数据放入)
ch <- 42 //向channel中放入两个数据
ch <- 27
close(ch) //关闭通道
wg.Done()
}(ch)
wg.Wait()
}
可以得到正确结果:
42
27
与此同时,还可以使用检测语句来手动地控制退出:
go func(ch <-chan int) { //接收通道
for {
if i, ok := <-ch; ok{
fmt.Println(i) //从channel中取出数据
} else {
break
}
}
wg.Done()
}(ch)
17.6 Select
在下面这个例子中,额外创建了一个通道,这个通道的类型签名被强类型化为一个没有字段的结构,没有字段的结构的特点是在go中需要零内存分配,其目的是实现信号,让接收方知道消息已被发送。当然也可以把这个通道的类型签名改为bool,但使用无字段结构将会节省一些内存分配,这是非常常用的用法。
package main
import (
"fmt"
"time"
)
const (
logInfo = "INFO"
logWarning = "WARNING"
logError = "ERROR"
)
type logEntry struct {
time time.Time
severity string
message string
}
var logCh = make(chan logEntry, 50)
var doneCh = make(chan struct{}) //这个通道的类型签名被强类型化为一个没有字段的结构
func main() {
go logger()
logCh <- logEntry{time.Now(), logInfo, "starting"}
logCh <- logEntry{time.Now(), logInfo, "down"}
time.Sleep(100 * time.Millisecond)
doneCh <- struct{}{}
}
func logger() { //监听来自通道的消息并打印
for {
select {
case entry := <-logCh:
fmt.Printf("%v - [%v]%v\n", entry.time.Format("2006-01-01T15:04:05"),
entry.severity, entry.message)
case <-doneCh:
break
}
}
}
在select没有default的情况下,如果条件得不到满足,将会一直阻塞。但如果想要一个非阻塞的select语句,则需要加上default。