阅读目录
- 理论
- Go语言的主要特征
- UTF-8 和 Unicode 有何区别?
- Go语言命名
- Go语言内置函数
- 内置接口 error
- init 函数
- init 函数和 main 函数的异同
- Go 语言内置的运算符
- 算数运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 基本类型
- byte 和 rune 类型
- 数组 Array
- 数组初始化
- 一维数组
- 多维数组
- 切片 Slice
- 创建切片的各种方式
- 切片初始化
- 通过 make 来创建切片
- append 内置函数操作切片(切片追加)
- slice 遍历
- 条件语句 select
- 1.1.1. select 语句
- 1.1.2. Golang select 的使用及典型用法
- 1.1.3. 典型用法
- 循环控制 Goto、Break、Continue
- Go 的闭包
- Go 语言递归函数
- 结构体
- 1.1. 类型别名和自定义类型
- 1.1.1. 自定义类型
- 1.1.2. 类型别名
- 1.1.3. 类型定义和类型别名的区别
- 1.2. 结构体 struct
- 1.2.1. 结构体的定义
- 1.2.2. 结构体实例化
- 1.2.3. 基本实例化
- 1.3. 匿名结构体
- 1.3.1. 创建指针类型结构体
- 1.3.2. 取结构体的地址实例化
- 1.3.3. 结构体初始化
- 1.3.4. 使用键值对初始化
- 1.3.5. 使用值的列表初始化
- 1.3.6. 结构体内存布局
- 1.3.7. 构造函数
- 1.3.8. 方法和接收者
- 1.3.9. 指针类型的接收者
- 1.3.10. 值类型的接收者
- 1.3.11. 什么时候应该使用指针类型接收者
- 1.3.12. 任意类型添加方法
- 1.3.13. 结构体的匿名字段
- 1.3.14.嵌套结构体
- 1.3.15. 嵌套匿名结构体
- 1.3.16. 嵌套结构体的字段名冲突
- 1.3.17. 结构体的“继承”
- 1.3.18. 结构体字段的可见性
- 1.3.19. 结构体与JSON序列化
- 1.3.20. 结构体标签(Tag)
- 1.3.21. 删除map类型的结构体
- 1.3.22. 实现map有序输出(面试经常问到)
- 延迟调用(defer)
- 1.1.1. Golang 延迟调用
- 1.1.2. defer陷阱
- 接口
- 1.1. 接口类型
- 1.2. 为什么要使用接口
- 1.3. 接口的定义
- 空接口
- 空接口的应用
- 空接口作为map的值
- 类型断言
- 并发编程
- 1. Goroutine
- 使用 goroutine
- 1. runtime 包
- 1.1. runtime.Gosched()
- 1.2. runtime.Goexit()
- 1.3. runtime.GOMAXPROCS
- Channel
- channel 类型
- 创建 channel
- channel 操作
- 无缓冲的通道
- 有缓冲的通道
- close()
- 如何优雅的从通道循环取值
- 单向通道
- 通道总结
- select 多路复用
- 如果多个 channel 同时 ready,则随机选择一个执行。
- 可以用于判断管道是否存满。
- 并发安全和锁
- 1. 互斥锁
- 2. 读写互斥锁
- Sync
- 1. sync.WaitGroup 实现并发任务
- 2. sync.Once 执行一次
- 3. sync.Map
.
理论
Go语言的主要特征
1.自动立即回收。
2.更丰富的内置类型。
3.函数多返回值。
4.错误处理。
5.匿名函数和闭包。
6.类型和接口。
7.并发编程。
8.反射。
9.语言交互性。
UTF-8 和 Unicode 有何区别?
- Unicode 是「字符集」
- UTF-8 是「编码规则」
字符集:
为每一个「字符」分配一个唯一的 ID(学名为码位 / 码点 / Code Point);
编码规则:
将「码位」转换为字节序列的规则(编码/解码 可以理解为 加密/解密 的过程)
Go语言命名
1.Go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则:
1)首字符可以是任意的Unicode字符或者下划线
2)剩余字符可以是Unicode字符、下划线、数字
3)字符长度不限
Go语言内置函数
- append – 用来追加元素到数组、slice中,返回修改后的数组、slice。
- close – 主要用来关闭 channel。
- delete – 从map中删除 key 对应的 value。
- panic – 停止常规的 goroutine (panic 和 recover:用来做错误处理)
- recover – 允许程序定义 goroutine 的 panic 动作。
- real – 返回 complex 的实部 (complex、real imag:用于创建和操作复数)
- imag – 返回 complex 的虚部。
- make – 用来分配内存,返回 Type 本身(只能应用于 slice, map, channel)
- new – 用来分配内存,主要用来分配值类型,比如 int、struct。返回指向Type的指针。
- cap – capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
- copy – 用于复制和连接 slice,返回复制的数目。
- len – 来求长度,比如string、array、slice、map、channel ,返回长度
- print、println – 底层打印函数,在部署环境中建议使用 fmt 包
内置接口 error
// 只要实现了Error()函数,返回值为String的都实现了err接口
type error interface {
Error() String
}
init 函数
1 init 函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等。
2 每个包可以拥有多个 init 函数。
3 包的每个源文件也可以拥有多个 init 函数。
4 同一个包中多个 init 函数的执行顺序 go 语言没有明确的定义(说明)
5 不同包的 init 函数按照包导入的依赖关系决定该初始化函数的执行顺序。
6 init 函数不能被其他函数调用,而是在main函数执行之前,自动被调用。
init 函数和 main 函数的异同
相同点:
两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
不同点:
init 可以应用于任意包中,且可以重复定义多个。
main 函数只能用于main包中,且只能定义一个。
两个函数的执行顺序:对同一个go文件的init()调用顺序是从上到下的。
Go 语言内置的运算符
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
算数运算符
关系运算符
逻辑运算符
位运算符
赋值运算符
基本类型
类型 | 长度(字节) | 默认值 | 说明 |
bool | 1 | false | |
byte | 1 | 0 | uint8 |
rune | 4 | 0 | Unicode Code Point, int32 |
int, uint | 4或8 | 0 | 32 或 64 位 |
int8, uint8 | 1 | 0 | -128 ~ 127, 0 ~ 255,byte是uint8 的别名 |
int16, uint16 | 2 | 0 | -32768 ~ 32767, 0 ~ 65535 |
int32, uint32 | 4 | 0 | -21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名 |
int64, uint64 | 8 | 0 | |
float32 | 4 | 0.0 | |
float64 | 8 | 0.0 | |
complex64 | 8 | ||
complex128 | 16 | ||
uintptr | 4或8 | 以存储指针的 uint32 或 uint64 整数 | |
array | 值类型 | ||
struct | 值类型 | ||
string | "" | UTF-8 字符串 | |
slice | nil | 引用类型 | |
map | nil | 引用类型 | |
channel | nil | 引用类型 | |
interface | nil | 接口 | |
function | nil | 函数 |
byte 和 rune 类型
Go 语言的字符有以下两种:
- uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
- rune类型,代表一个 UTF-8字符。
package main
import (
"fmt"
)
func main() {
traversalString()
}
// 遍历字符串
func traversalString() {
// byte
s := "wgchen博客"
for i := 0; i < len(s); i++ {
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
//rune
for _, r := range s {
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}
E:\go_test>go run main.go
119(w) 103(g) 99(c) 104(h) 101(e) 110(n) 229(å) 141() 154(š) 229(å) 174(®) 162(¢)
119(w) 103(g) 99(c) 104(h) 101(e) 110(n) 21338(博) 23458(客)
E:\go_test>
数组 Array
1、数组:是同一种数据类型的固定长度的序列。
2、数组定义:var a [len]int
,比如:var a [5]int
,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
3、长度是数组类型的一部分,因此,var a[5] int
和 var a[10]int
是不同的类型。
4、数组可以通过下标进行访问,下标是从 0
开始,最后一个元素下标是:len-1
for i := 0; i < len(a); i++ {
}
for index, v := range a {
}
5、访问越界,如果下标在数组合法范围之外,则触发访问越界,会 panic。
6、数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
7、支持 "=="、"!="
操作符,因为内存总是被初始化过的。
8、指针数组 [n]*T
,数组指针 *[n]T
。
数组初始化
一维数组
全局:
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
局部:
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
package main
import (
"fmt"
)
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
func main() {
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
fmt.Println(arr0, arr1, arr2, str)
fmt.Println(a, b, c, d)
}
[1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [ hello world tom]
[1 2 0] [1 2 3 4] [0 0 100 0 200] [{user1 10} {user2 20}]
多维数组
全局
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
局部:
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}}
// 第 2 纬度不能用 "..."。
package main
import (
"fmt"
)
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
func main() {
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}}
// 第 2 纬度不能用 "..."。
fmt.Println(arr0, arr1)
fmt.Println(a, b)
}
输出结果:
[[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[1 2 3] [7 8 9]]
[[1 2 3] [4 5 6]] [[1 1] [2 2] [3 3]]
值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。
package main
import (
"fmt"
)
func test(x [2]int) {
fmt.Printf("x: %p\n", &x)
x[1] = 1000
}
func main() {
a := [2]int{}
fmt.Printf("a: %p\n", &a)
test(a)
fmt.Println(a)
}
输出结果:
a: 0xc42007c010
x: 0xc42007c030
[0 0]
内置函数 len 和 cap 都返回数组长度 (元素数量)。
package main
func main() {
a := [2]int{}
println(len(a), cap(a)) // 2 2
}
切片 Slice
需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。
1、切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
2、切片的长度可以改变,因此,切片是一个可变的数组。
3、切片遍历方式和数组一样,可以用 len() 求长度。表示可用元素数量,读写操作不能超过该限制。
4、cap 可以求出 slice 最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array)
,其中 array 是 slice 引用的数组。
5、切片的定义:var 变量名 []
类型,比如 var str []string var arr []int
。
6、如果 slice == nil
,那么 len、cap 结果都等于 0。
创建切片的各种方式
package main
import "fmt"
func main() {
//1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("是空")
} else {
fmt.Println("不是空")
}
// 2.:=
s2 := []int{}
// 3.make()
var s3 []int = make([]int, 0)
fmt.Println(s1, s2, s3)
// [] [] []
// 4.初始化赋值
var s4 []int = make([]int, 0, 0)
fmt.Println(s4)
// []
s5 := []int{1, 2, 3}
fmt.Println(s5)
// [1 2 3]
// 5.从数组切片
arr := [5]int{1, 2, 3, 4, 5}
var s6 []int // 前包后不包
s6 = arr[1:4]
fmt.Println(s6)
// [2 3 4]
}
切片初始化
全局:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end]
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1]
//去掉切片的最后一个元素
局部:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[start:end]
slice6 := arr[:end]
slice7 := arr[start:]
slice8 := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
package main
import (
"fmt"
)
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
// 可以简写为 var slice []int = arr[:end]
var slice1 []int = arr[0:6]
// 可以简写为 var slice[]int = arr[start:]
var slice2 []int = arr[5:10]
// var slice []int = arr[:]
var slice3 []int = arr[0:len(arr)]
// 去掉切片的最后一个元素
var slice4 = arr[:len(arr)-1]
func main() {
fmt.Printf("全局变量:arr %v\n", arr)
fmt.Printf("全局变量:slice0 %v\n", slice0)
fmt.Printf("全局变量:slice1 %v\n", slice1)
fmt.Printf("全局变量:slice2 %v\n", slice2)
fmt.Printf("全局变量:slice3 %v\n", slice3)
fmt.Printf("全局变量:slice4 %v\n", slice4)
fmt.Printf("-----------------------------------\n")
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[2:8]
slice6 := arr[0:6] //可以简写为 slice := arr[:end]
slice7 := arr[5:10] //可以简写为 slice := arr[start:]
slice8 := arr[0:len(arr)] //slice := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
fmt.Printf("局部变量: arr2 %v\n", arr2)
fmt.Printf("局部变量: slice5 %v\n", slice5)
fmt.Printf("局部变量: slice6 %v\n", slice6)
fmt.Printf("局部变量: slice7 %v\n", slice7)
fmt.Printf("局部变量: slice8 %v\n", slice8)
fmt.Printf("局部变量: slice9 %v\n", slice9)
}
E:\go_test>go run main.go
全局变量:arr [0 1 2 3 4 5 6 7 8 9]
全局变量:slice0 [2 3 4 5 6 7]
全局变量:slice1 [0 1 2 3 4 5]
全局变量:slice2 [5 6 7 8 9]
全局变量:slice3 [0 1 2 3 4 5 6 7 8 9]
全局变量:slice4 [0 1 2 3 4 5 6 7 8]
-----------------------------------
局部变量: arr2 [9 8 7 6 5 4 3 2 1 0]
局部变量: slice5 [2 3 4 5 6 7]
局部变量: slice6 [0 1 2 3 4 5]
局部变量: slice7 [5 6 7 8 9]
局部变量: slice8 [0 1 2 3 4 5 6 7 8 9]
局部变量: slice9 [0 1 2 3 4 5 6 7 8]
E:\go_test>
通过 make 来创建切片
var slice []type = make([]type, len)
slice := make([]type, len)
slice := make([]type, len, cap)
读写操作实际目标是底层数组,只需注意索引号的差别。
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 5}
s := data[2:4]
s[0] += 100
s[1] += 200
fmt.Println(s)
fmt.Println(data)
}
[102 203]
[0 1 102 203 4 5]
可直接创建 slice 对象,自动分配底层数组。
package main
import "fmt"
func main() {
// 通过初始化表达式构造,可使用索引号。
s1 := []int{0, 1, 2, 3, 8: 100}
fmt.Println(s1, len(s1), cap(s1))
// 使用 make 创建,指定 len 和 cap 值。
s2 := make([]int, 6, 8)
fmt.Println(s2, len(s2), cap(s2))
// 省略 cap,相当于 cap = len。
s3 := make([]int, 6)
fmt.Println(s3, len(s3), cap(s3))
}
输出结果:
[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6
使用 make 动态创建 slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3}
p := &s[2] // *int, 获取底层数组元素指针。
*p += 100
fmt.Println(s)
}
输出结果:
[0 1 102 3]
至于 [][]T
,是指元素类型为 []T
。
package main
import (
"fmt"
)
func main() {
data := [][]int{
[]int{1, 2, 3},
[]int{100, 200},
[]int{11, 22, 33, 44},
}
fmt.Println(data)
}
输出结果:
[[1 2 3] [100 200] [11 22 33 44]]
可直接修改 struct array/slice 成员。
package main
import (
"fmt"
)
func main() {
d := [5]struct {
x int
}{}
s := d[:]
d[1].x = 10
s[2].x = 20
fmt.Println(d)
fmt.Printf("%p, %p\n", &d, &d[0])
}
[{0} {10} {20} {0} {0}]
0xc4200160f0, 0xc4200160f0
append 内置函数操作切片(切片追加)
package main
import (
"fmt"
)
func main() {
var a = []int{1, 2, 3}
fmt.Printf("slice a : %v\n", a)
// slice a : [1 2 3]
var b = []int{4, 5, 6}
fmt.Printf("slice b : %v\n", b)
// slice b : [4 5 6]
c := append(a, b...)
fmt.Printf("slice c : %v\n", c)
// slice c : [1 2 3 4 5 6]
d := append(c, 7)
fmt.Printf("slice d : %v\n", d)
// slice d : [1 2 3 4 5 6 7]
e := append(d, 8, 9, 10)
fmt.Printf("slice e : %v\n", e)
// slice e : [1 2 3 4 5 6 7 8 9 10]
}
slice 遍历
package main
import (
"fmt"
)
func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := data[:]
for index, value := range slice {
fmt.Printf("inde : %v , value : %v\n", index, value)
}
}
条件语句 select
1.1.1. select 语句
select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
语法
Go 编程语言中 select 语句的语法如下:
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
以下描述了 select 语句的语法:
1、每个 case 都必须是一个通信。
2、所有 channel 表达式都会被求值。
3、 所有被发送的表达式都会被求值。
4、如果任意某个通信可以进行,它就执行;其他被忽略。
5、 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:
如果有default子句,则执行该语句。
如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
package main
import "fmt"
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
以上代码执行结果为:
no communication
select 可以监听 channel 的数据流动。
select 的用法与 switch 语法非常类似,由 select 开始的一个新的选择块,每个选择条件由 case 语句来描述。
与 switch 语句可以选择任何使用相等比较的条件相比,select 由比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作。
// 不停的在这里检测
select {
// 检测有没有数据可以读
case <-chanl :
// 如果 chanl 成功读取到数据,则进行该 case 处理语句。
// 检测有没有可以写
case chan2 <- 1 :
// 如果成功向 chan2 写入数据,则进行该case处理语句。
// 假如没有default,那么在以上两个条件都不成立的情况下,
// 就会在此阻塞一般 default 会不写在里面,
// select 中的 default 子句总是可运行的,
// 因为会很消耗CPU资源
default:
//如果以上都没有符合条件,那么则进行default处理流程
}
在一个 select 语句中,Go会按顺序从头到尾评估每一个发送和接收的语句。
如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
①如果给出了default语句,那么就会执行default的流程,同时程序的执行会从select语句后的语句中恢复。
②如果没有default语句,那么select语句将被阻塞,直到至少有一个case可以进行下去。
1.1.2. Golang select 的使用及典型用法
基本使用
select 是 Go 中的一个控制结构,类似于 switch 语句,用于处理异步IO操作。select 会监听 case 语句中 channel 的读写操作,当 case 中 channel 读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。
select 中的 case 语句必须是一个 channel 操作 select 中的 default 子句总是可运行的。
如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
如果没有可运行的 case 语句,且有 default 语句,那么就会执行default 的动作。
如果没有可运行的 case 语句,且没有 default 语句,select 将阻塞,直到某个 case 通信可以运行。
例如:
package main
import "fmt"
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
// same as: i3, ok := <-c3
case i3, ok := (<-c3):
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
//输出:no communication
1.1.3. 典型用法
1、超时判断。
// 比如在下面的场景中,
// 使用全局 resChan 来接受response,
// 如果时间超过 3S,resChan中还没有数据返回,
// 则第二条 case 将执行
var resChan = make(chan int)
// do request
func test() {
select {
case data := <-resChan:
doData(data)
case <-time.After(time.Second * 3):
fmt.Println("request time out")
}
}
func doData(data int) {
//...
}
2、退出
//主线程(协程)中如下:
var shouldQuit=make(chan struct{})
fun main(){
{
//loop
}
//...out of the loop
select {
case <-c.shouldQuit:
cleanUp()
return
default:
}
//...
}
// 再另外一个协程中,如果运行遇到非法操作或不可处理的错误,
// 就向shouldQuit发送数据通知程序停止运行
close(shouldQuit)
3.判断 channel 是否阻塞
// 在某些情况下是存在不希望channel缓存满了的需求的,
// 可以用如下方法判断
ch := make (chan int, 5)
//...
data:=0
select {
case ch <- data:
default:
//做相应操作,比如丢弃data。视需求而定
}
循环控制 Goto、Break、Continue
1、三个语句都可以配合标签(label)使用
2、标签名区分大小写,定以后若不使用会造成编译错误
3、continue、break配合标签(label)可用于多层循环跳出
4、goto 是调整执行位置,与 continue、break 配合标签(label)的结果并不相同。
Go 的闭包
Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。
package main
import (
"fmt"
)
func a() func() int {
i := 0
b := func() int {
i++
fmt.Println(i)
return i
}
return b
}
func main() {
c := a()
c()
c()
c()
a() //不会输出i
}
输出结果:
PS E:\go_test> go run .\main.go
1
2
3
PS E:\go_test>
闭包复制的是原对象指针,这就很容易解释延迟引用现象。
package main
import "fmt"
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
func main() {
f := test()
f()
}
输出:
x (0xc42007c008) = 100
x (0xc42007c008) = 100
在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调匿名函数时,只需以某个寄存器传递该对象即可。
FuncVal { func_address, closure_var_pointer ... }
外部引用函数参数局部变量。
package main
import "fmt"
// 外部引用函数参数局部变量
func add(base int) func(int) int {
return func(i int) int {
base += i
return base
}
}
func main() {
tmp1 := add(10)
fmt.Println(tmp1(1), tmp1(2))
// 11 13
// 此时tmp1和tmp2不是一个实体了
tmp2 := add(100)
fmt.Println(tmp2(1), tmp2(2))
// 101 103
}
返回 2 个闭包。
package main
import "fmt"
// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
// 定义2个函数,并返回
// 相加
add := func(i int) int {
base += i
return base
}
// 相减
sub := func(i int) int {
base -= i
return base
}
// 返回
return add, sub
}
func main() {
f1, f2 := test01(10)
// base一直是没有消
fmt.Println(f1(1), f2(2))
// 11 9
// 此时base是9
fmt.Println(f1(3), f2(4))
// 12 8
}
Go 语言递归函数
递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。
构成递归需具备的条件:
1、子问题须与原始问题为同样的事,且更为简单。
2、不能无限制地调用本身,须有个出口,化简为非递归状况处理。
数字阶乘
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且 0
的阶乘为 1
。自然数 n
的阶乘写作 n!
。1808年,基斯顿·卡曼引进这个表示法。
package main
import "fmt"
func factorial(i int) int {
if i <= 1 {
return 1
}
return i * factorial(i-1)
}
func main() {
var i int = 7
fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}
PS E:\go_test> go run .\main.go
Factorial of 7 is 5040
PS E:\go_test>
结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。
Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
1.1. 类型别名和自定义类型
1.1.1. 自定义类型
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。
我们可以基于内置的基本类型定义,也可以通过struct定义。
例如:
// 将MyInt定义为int类型
type MyInt int
通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
1.1.2. 类型别名
类型别名是Go1.9版本添加的新功能。
类型别名规定:
TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
我们之前见过的 rune 和 byte 就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
1.1.3. 类型定义和类型别名的区别
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a)
//type of a:main.NewInt
fmt.Printf("type of b:%T\n", b)
//type of b:int
}
结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型。b 的类型是 int。MyInt 类型只会在代码中存在,编译完成时并不会有 MyInt 类型。
1.2. 结构体 struct
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称 struct。 也就是我们可以通过struct来定义自己的类型了。
Go语言中通过struct来实现面向对象。
1.2.1. 结构体的定义
使用 type 和 struct 关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
其中:
1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
3.字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个Person(人)结构体,代码如下:
type person struct {
name string
city string
age int8
}
同样类型的字段也可以写在一行,
type person1 struct {
name, city string
age int8
}
这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。
1.2.2. 结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用 var 关键字声明结构体类型。
var 结构体实例 结构体类型
1.2.3. 基本实例化
package main
import "fmt"
type person struct {
name string
city string
age int8
}
func main() {
var p1 person
p1.name = "wgchen"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1)
//p1={wgchen 北京 18}
fmt.Printf("p1=%#v\n", p1)
//p1=main.person{name:"wgchen", city:"北京", age:18}
}
我们通过.来访问结构体的字段(成员变量),例如 p1.name 和 p1.age等。
1.3. 匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
package main
import (
"fmt"
)
func main() {
var user struct {
Name string
Age int
}
user.Name = "wgchen"
user.Age = 18
fmt.Printf("%#v\n", user)
// struct { Name string; Age int }{Name:"wgchen", Age:18}
}
1.3.1. 创建指针类型结构体
我们还可以通过使用 new 关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var p2 = new(person)
fmt.Printf("%T\n", p2)
//*main.person
fmt.Printf("p2=%#v\n", p2)
//p2=&main.person{name:"", city:"", age:0}
从打印的结果中我们可以看出p2是一个结构体指针。
需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2)
//p2=&main.person{name:"测试", city:"北京", age:18}
1.3.2. 取结构体的地址实例化
使用 &
对结构体进行取地址操作相当于对该结构体类型进行了一次 new 实例化操作。
p3 := &person{}
fmt.Printf("%T\n", p3)
//*main.person
fmt.Printf("p3=%#v\n", p3)
//p3=&main.person{name:"", city:"", age:0}
p3.name = "博客"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3)
//p3=&main.person{name:"博客", city:"成都", age:30}
p3.name = "博客"其实在底层是(*p3).name = "博客"
,这是Go语言帮我们实现的语法糖。
1.3.3. 结构体初始化
type person struct {
name string
city string
age int8
}
func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4)
//p4=main.person{name:"", city:"", age:0}
}
1.3.4. 使用键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
p5 := person{
name: "ppro",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5)
//p5=main.person{name:"ppro", city:"北京", age:18}
也可以对结构体指针进行键值对初始化,例如:
p6 := &person{
name: "pprof",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6)
//p6=&main.person{name:"pprof", city:"北京", age:18}
当某些字段没有初始值的时候,该字段可以不写。
此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7)
//p7=&main.person{name:"", city:"北京", age:0}
1.3.5. 使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{
"pprof",
"北京",
18,
}
fmt.Printf("p8=%#v\n", p8)
//p8=&main.person{name:"pprof", city:"北京", age:18}
使用这种格式初始化时,需要注意:
1.必须初始化结构体的所有字段。
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用。
1.3.6. 结构体内存布局
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
1.3.7. 构造函数
Go语言的结构体没有构造函数,我们可以自己实现。
例如,下方的代码就实现了一个 person 的构造函数。
因为 struct 是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
调用构造函数
p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9)
1.3.8. 方法和接收者
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。
接收者的概念就类似于其他语言中的 this 或者 self。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
其中,
1.接收者变量:
接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。
例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
2.接收者类型:
接收者类型和参数类似,可以是指针类型和非指针类型。
3.方法名、参数列表、返回参数:具体格式与函数定义相同。
举个例子:
package main
import "fmt"
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言%d!\n", p.name, p.age)
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
PS E:\go_test> go run .\main.go
测试的梦想是学好Go语言25!
PS E:\go_test>
1.3.9. 指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。
这种方式就十分接近于其他语言中面向对象中的 this 或者 self。 例如我们为 Person 添加一个 SetAge 方法,来修改实例变量的年龄。
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
调用该方法:
func main() {
p1 := NewPerson("测试", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}
1.3.10. 值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。
在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}
1.3.11. 什么时候应该使用指针类型接收者
1.需要修改接收者中的值。
2.接收者是拷贝代价比较大的大对象。
3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
1.3.12. 任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
举个例子,我们基于内置的 int 类型使用 type 关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
package main
import "fmt"
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
1.3.13. 结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
//Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person{
"pprof",
18,
}
fmt.Printf("%#v\n", p1)
//main.Person{string:"pprof", int:18}
fmt.Println(p1.string, p1.int)
// pprof 18
}
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
1.3.14.嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
package main
import "fmt"
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "黑龙江",
City: "哈尔滨",
},
}
fmt.Printf("user1=%#v\n", user1)
//user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
1.3.15. 嵌套匿名结构体
package main
import "fmt"
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}
func main() {
var user2 User
user2.Name = "pprof"
user2.Gender = "女"
//通过匿名结构体.字段名访问
user2.Address.Province = "黑龙江"
//直接访问匿名结构体的字段名
user2.City = "哈尔滨"
fmt.Printf("user2=%#v\n", user2)
//user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
1.3.16. 嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。
这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
package main
import "fmt"
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user3 User
user3.Name = "pprof"
user3.Gender = "女"
// user3.CreateTime = "2019"
//ambiguous selector user3.CreateTime
//指定Address结构体中的CreateTime
user3.Address.CreateTime = "2000"
//指定Email结构体中的CreateTime
user3.Email.CreateTime = "2000"
fmt.Println(user3)
}
PS E:\go_test> go run .\main.go
{pprof 女 { 2000} { 2000}}
PS E:\go_test>
1.3.17. 结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
package main
import "fmt"
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
1.3.18. 结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
1.3.19. 结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。
易于人阅读和编写。
同时也易于机器解析和生成。
JSON键值对是用来保存JS对象的一种方式,键/
值对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值;
多个键值之间使用英文,
分隔。
package main
import (
"encoding/json"
"fmt"
)
// Student 学生
type Student struct {
ID int
Gender string
Name string
}
// Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
1.3.20. 结构体标签(Tag)
Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag 在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体标签由一个或多个键值对组成。
键与值使用冒号分隔,值用双引号括起来。
键值对之间使用一个空格分隔。
注意事项:
为结构体编写 Tag 时,必须严格遵守键值对的规则。
结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。
例如不要在 key 和 value 之间添加空格。
例如我们为 Student 结构体的每个字段定义 json 序列化时使用的Tag:
package main
import (
"encoding/json"
"fmt"
)
// Student 学生
type Student struct {
//通过指定tag实现json序列化该字段时的key
ID int `json:"id"`
//json序列化是默认使用字段名作为key
Gender string
//私有不能被json包访问
name string
}
func main() {
s1 := Student{
ID: 1,
Gender: "女",
name: "pprof",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data)
//json str:{"id":1,"Gender":"女"}
}
PS E:\go_test>
PS E:\go_test> go run .\main.go
json str:{"id":1,"Gender":"女"}
PS E:\go_test>
1.3.21. 删除map类型的结构体
package main
import "fmt"
type student struct {
id int
name string
age int
}
func main() {
ce := make(map[int]student)
ce[1] = student{1, "xiaolizi", 22}
ce[2] = student{2, "wang", 23}
fmt.Println(ce)
delete(ce, 2)
fmt.Println(ce)
}
PS E:\go_test> go run .\main.go
map[1:{1 xiaolizi 22} 2:{2 wang 23}]
map[1:{1 xiaolizi 22}]
PS E:\go_test>
1.3.22. 实现map有序输出(面试经常问到)
package main
import (
"fmt"
"sort"
)
func main() {
map1 := make(map[int]string, 5)
map1[1] = "www.map.com"
map1[2] = "rpc.map.com"
map1[5] = "ceshi"
map1[3] = "xiaohong"
map1[4] = "xiaohuang"
sli := []int{}
for k, _ := range map1 {
sli = append(sli, k)
}
sort.Ints(sli)
for i := 0; i < len(map1); i++ {
fmt.Println(map1[sli[i]])
}
}
PS E:\go_test> go run .\main.go
www.map.com
rpc.map.com
xiaohong
xiaohuang
ceshi
PS E:\go_test>
延迟调用(defer)
1.1.1. Golang 延迟调用
defer 特性
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
defer 用途
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
go 语言 defer
go 语言的 defer 功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。
defer 是先进后出
这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。
package main
import "fmt"
func main() {
var whatever [5]struct{}
for i := range whatever {
defer fmt.Println(i)
}
}
输出结果:
PS E:\go_test> go run .\main.go
4
3
2
1
0
PS E:\go_test>
defer 碰上闭包
package main
import "fmt"
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Println(i) }()
}
}
输出结果:
PS E:\go_test> go run .\main.go
4
4
4
4
4
PS E:\go_test>
defer f.Close
这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子:
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer t.Close()
}
}
PS E:\go_test> go run .\main.go
c closed
c closed
c closed
PS E:\go_test>
这个输出并不会像我们预计的输出 c b a
,而是输出 c c c
。
可是按照前面的 go spec 中的说明,应该输出 c b a才对啊。
那我们换一种方式来调用一下.
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func Close(t Test) {
t.Close()
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer Close(t)
}
}
输出结果:
PS E:\go_test> go run .\main.go
c closed
b closed
a closed
PS E:\go_test>
这个时候输出的就是 c b a
。
当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出 c b a
。
看似多此一举的声明。
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
t2 := t
defer t2.Close()
}
}
输出结果:
PS E:\go_test> go run .\main.go
c closed
b closed
a closed
PS E:\go_test>
通过以上例子,结合这句话。可以得出下面的结论:
defer 后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说 struct 这里的 this 指针如何处理,通过这个例子可以看出 go 语言并没有把这个明确写出来的 this 指针当作参数来看待。
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。
哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
package main
func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x)
// div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer println("c")
}
func main() {
test(0)
}
c
b
a
panic: runtime error: integer divide by zero
*
延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。
package main
func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y)
// y 闭包引用
}(x)
// x 被复制
x += 20
y += 100
println("x =", x, "y =", y)
}
func main() {
test()
}
PS E:\go_test> go run .\main.go
x = 30 y = 120
defer: 10 120
PS E:\go_test>
*
滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。
package main
import (
"fmt"
"sync"
"time"
)
var lock sync.Mutex
func test() {
lock.Lock()
lock.Unlock()
}
func testdefer() {
lock.Lock()
defer lock.Unlock()
}
func main() {
func() {
t1 := time.Now()
for i := 0; i < 10000; i++ {
test()
}
elapsed := time.Since(t1)
fmt.Println("test elapsed: ", elapsed)
}()
func() {
t1 := time.Now()
for i := 0; i < 10000; i++ {
testdefer()
}
elapsed := time.Since(t1)
fmt.Println("testdefer elapsed: ", elapsed)
}()
}
PS E:\go_test> go run .\main.go
test elapsed: 566.4µs
testdefer elapsed: 0s
PS E:\go_test>
1.1.2. defer陷阱
defer 与 closure
package main
import (
"errors"
"fmt"
)
func foo(a, b int) (i int, err error) {
defer fmt.Printf("first defer err %v\n", err)
defer func(err error) {
fmt.Printf("second defer err %v\n", err)
}(err)
defer func() {
fmt.Printf("third defer err %v\n", err)
}()
if b == 0 {
err = errors.New("divided by zero!")
return
}
i = a / b
return
}
func main() {
foo(2, 0)
}
输出结果:
PS E:\go_test> go run .\main.go
third defer err divided by zero!
second defer err <nil>
first defer err <nil>
PS E:\go_test>
解释:如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。
defer 与 return
package main
import "fmt"
func foo() (i int) {
i = 0
defer func() {
fmt.Println(i)
}()
return 2
}
func main() {
foo()
}
输出结果:
PS E:\go_test> go run .\main.go
2
PS E:\go_test>
解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。
所以 defer closure 输出结果为 2 而不是 1。
defer nil 函数
package main
import (
"fmt"
)
func test() {
var run func() = nil
defer run()
fmt.Println("runs")
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
test()
}
输出结果:
runs
runtime error: invalid memory address or nil pointer dereference
解释:
名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。
然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。
在错误的位置使用 defer
当 http.Get 失败时会抛出异常。
package main
import "net/http"
func do() error {
res, err := http.Get("http://www.google.com")
defer res.Body.Close()
if err != nil {
return err
}
// ..code...
return nil
}
func main() {
do()
}
输出结果:
panic: runtime error: invalid memory address or nil pointer dereference
因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常。
解决方案
总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer。
package main
import "net/http"
func do() error {
res, err := http.Get("http://xxxxxxxxxx")
if res != nil {
defer res.Body.Close()
}
if err != nil {
return err
}
// ..code...
return nil
}
func main() {
do()
}
在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。
解释:
在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。
通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。
上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。
不检查错误
在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉。
package main
import "os"
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer f.Close()
}
// ..code...
return nil
}
func main() {
do()
}
改进一下,
package main
import "os"
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
}
// ..code...
return nil
}
func main() {
do()
}
再改进一下,通过命名的返回变量来返回 defer 内的错误。
package main
import "os"
func do() (err error) {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if ferr := f.Close(); ferr != nil {
err = ferr
}
}()
}
// ..code...
return nil
}
func main() {
do()
}
释放相同的资源
如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。
package main
import (
"fmt"
"os"
)
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if err := f.Close(); err != nil {
fmt.Printf("defer close book.txt err %v\n", err)
}
}()
}
// ..code...
f, err = os.Open("another-book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if err := f.Close(); err != nil {
fmt.Printf("defer close another-book.txt err %v\n", err)
}
}()
}
return nil
}
func main() {
do()
}
输出结果: defer close book.txt err close ./another-book.txt: file already closed。
当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭
解决方案:
package main
import (
"fmt"
"io"
"os"
)
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close book.txt err %v\n", err)
}
}(f)
}
// ..code...
f, err = os.Open("another-book.txt")
if err != nil {
return err
}
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close another-book.txt err %v\n", err)
}
}(f)
}
return nil
}
func main() {
do()
}
接口
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
1.1. 接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface 是一组 method 的集合,是 duck-type programming 的一种体现。
不关心属性(数据),只关心行为(方法)。
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
1.2. 为什么要使用接口
package main
import "fmt"
type Cat struct{}
func (c Cat) Say() string {
return "喵喵喵"
}
type Dog struct{}
func (d Dog) Say() string {
return "汪汪汪"
}
func main() {
c := Cat{}
fmt.Println("猫:", c.Say())
d := Dog{}
fmt.Println("狗:", d.Say())
}
PS E:\go_test> go run .\main.go
猫: 喵喵喵
狗: 汪汪汪
PS E:\go_test>
上面的代码中定义了猫和狗,然后它们都会叫,你会发现 main 函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?
像类似的例子在我们编程过程中会经常遇到:
比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?
Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。
1.3. 接口的定义
Go语言提倡面向接口编程。
- 接口是一个或多个方法签名的集合。
- 任何类型的方法集中只要拥有该接口’对应的全部方法’签名。就表示它 “实现” 了该接口,无须在该类型上显式声明实现了哪个接口。这称为Structural Typing。
- 所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。当然,该类型还可以有其他方法。
- 接口只有方法声明,没有实现,没有数据字段。
- 接口可以匿名嵌入其他接口,或嵌入到结构中。
- 对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
- 只有当接口存储的类型和对象都为 nil 时,接口才等于 nil。
- 接口调用不会做 receiver 的自动转换。
- 接口同样支持匿名字段方法。
- 接口也可实现类似OOP中的多态。
- 空接口可以作为任何类型数据的容器。
- 一个类型可实现多个接口。
- 接口命名习惯以 er 结尾。
空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
package main
import "fmt"
func main() {
// 定义一个空接口x
var x interface{}
s := "pprof"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
PS E:\go_test> go run .\main.go
type:string value:pprof
type:int value:100
type:bool value:true
PS E:\go_test>
// 定义一个空接口x
// var x interface{}
s := "pprof"
// x = s
fmt.Printf("type:%T value:%v\n", s, s)
i := 100
// x = i
fmt.Printf("type:%T value:%v\n", i, i)
b := true
// x = b
fmt.Printf("type:%T value:%v\n", b, b)
PS E:\go_test> go run .\main.go
type:string value:pprof
type:int value:100
type:bool value:true
PS E:\go_test>
空接口的应用
空接口作为函数的参数。
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
类型断言
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
接口值
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
我们来看一个具体的例子:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式: x.(T)
其中:
x:表示类型为 interface{} 的变量
T:表示断言 x 可能是的类型。
该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量,第二个值是一个布尔值,若为 true 则表示断言成功,为 false 则表示断言失败。
举个例子:
package main
import "fmt"
func main() {
var x interface{}
x = "pprof"
// v, ok := x.(string) // pprof
v, ok := x.(int) // 类型断言失败
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用 switch 语句来实现:
package main
import "fmt"
func main() {
var x interface{}
x = "pprof"
justifyType(x)
}
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
PS E:\go_test> go run .\main.go
x is a string,value is pprof
PS E:\go_test>
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。
不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
并发编程
进程和线程
A、进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
B、线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C、一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
并发和并行
A、多线程程序在一个核的cpu上运行,就是并发。
B、多线程程序在多个核的cpu上运行,就是并行。
并发
并行
协程和线程
协程:
独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:
一个线程上可以跑多个协程,协程是轻量级的线程。
goroutine 只是由官方实现的超级"线程池"。
每个实力 4~5KB 的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。
并发不是并行:
并发主要由切换时间片来实现"同时"运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。
goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
1. Goroutine
在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?
Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。
使用 goroutine
Go语言中使用 goroutine 非常简单,只需要在调用函数的时候在前面加上 go
关键字,就可以为一个函数创建一个 goroutine。
一个 goroutine 必定对应一个函数,可以创建多个 goroutine 去执行相同的函数。
启动单个 goroutine;
启动 goroutine 的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。
举个例子如下:
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
hello()
fmt.Println("main goroutine done!")
}
这个示例中 hello 函数和下面的语句是串行的,执行的结果是打印完 Hello Goroutine !后打印 main goroutine done!。
接下来我们在调用 hello 函数前面加上关键字go,也就是启动一个 goroutine 去执行 hello 这个函数。
func main() {
// 启动另外一个goroutine去执行hello函数
go hello()
fmt.Println("main goroutine done!")
}
这一次的执行结果只打印了main goroutine done!,并没有打印 Hello Goroutine!。为什么呢?
在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。
当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。
所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了。
func main() {
// 启动另外一个goroutine去执行hello函数
go hello()
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
执行上面的代码你会发现,这一次先打印 main goroutine done!,然后紧接着打印Hello Goroutine!。
首先为什么会先打印 main goroutine done! 是因为我们在创建新的goroutine 的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。
启动多个 goroutine
在Go语言中实现并发就是这样简单,我们还可以启动多个 goroutine。让我们再来一个例子:
(这里使用了 sync.WaitGroup 来实现 goroutine 的同步)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。
这是因为 10 个 goroutine 是并发执行的,而 goroutine 的调度是随机的。
注意
如果主协程退出了,其他任务还执行吗。
(运行下面的代码测试一下吧)
package main
import (
"fmt"
"time"
)
func main() {
// 合起来写
go func() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
if i == 2 {
break
}
}
}
PS E:\go_test> go run .\main.go
main goroutine: i = 1
new goroutine: i = 1
new goroutine: i = 2
main goroutine: i = 2
PS E:\go_test>
1. runtime 包
1.1. runtime.Gosched()
让出CPU时间片,重新等待安排任务(大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤)
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println("go " + s)
}
}("world")
// 主协程
for i := 0; i < 2; i++ {
// 切一下,再次分配任务
runtime.Gosched()
fmt.Println("for hello")
}
}
1.2. runtime.Goexit()
退出当前协程
(一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了)
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("up A.defer")
func() {
defer fmt.Println("B.defer")
// 结束协程
runtime.Goexit()
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("down A")
}()
for {
}
}
PS E:\go_test> go run .\main.go
B.defer
up A.defer
exit status 0xc000013a
PS E:\go_test>
1.3. runtime.GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
fmt.Println("a")
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
fmt.Println("b")
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Minute)
}
PS E:\go_test> go run .\main.go
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
b
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
a
两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
fmt.Println("a")
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
fmt.Println("b")
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
Go语言中的操作系统线程和 goroutine 的关系:
- 1、一个操作系统线程对应用户态多个goroutine。
- 2、go程序可以同时使用多个操作系统线程。
- 3、goroutine 和 OS 线程是多对多的关系,即 m:n。
Channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine 中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel 类型
channel 是一种类型,一种引用类型。
声明通道类型的格式如下:
var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
创建 channel
通道是引用类型,通道类型的空值是 nil
。
var ch chan int
fmt.Println(ch) // <nil>
声明的通道后需要使用make函数初始化之后才能使用。
创建channel的格式如下:
make(chan 元素类型, [缓冲大小])
channel 的缓冲大小是可选的。
举几个例子:
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel 操作
- 通道有发送(send)、
- 接收(receive)
- 关闭(close)三种操作。
发送和接收都使用 <-
符号。
现在我们先使用以下语句定义一个通道:
ch := make(chan int)
发送
将一个值发送到通道中。
ch <- 10 // 把10发送到ch中
接收
从一个通道中接收值。
x := <- ch // 从 ch 中接收值并赋值给变量 x
<-ch // 从 ch 中接收值,忽略结果
关闭
我们通过调用内置的 close 函数来关闭通道。
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭通道。
通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
1.对一个关闭的通道再发送值就会导致 panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致 panic。
无缓冲的通道
无缓冲的通道又称为阻塞的通道。
我们来看一下下面的代码:
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
PS E:\go_test> go run .\main.go
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
E:/go_test/main.go:7 +0x31
exit status 2
PS E:\go_test>
为什么会出现 deadlock 错误呢?
因为我们使用 ch := make(chan int)
创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。
就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在 ch <- 10
这一行代码形成死锁,那如何解决这个问题呢?
一种方法是启用一个 goroutine 去接收值,例如:
package main
import "fmt"
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
// 启用 goroutine 从通道接收值
go recv(ch)
ch <- 10
fmt.Println("发送成功")
}
PS E:\go_test> go run .\main.go
接收成功 10
发送成功
PS E:\go_test>
无缓冲通道上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时值才能发送成功,两个 goroutine 将继续执行。
相反,如果接收操作先执行,接收方的 goroutine 将阻塞,直到另一个 goroutine 在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道。
有缓冲的通道
解决上面问题的方法还有一种就是使用有缓冲区的通道。
我们可以在使用 make 函数初始化通道的时候为其指定通道的容量,例如:
package main
import "fmt"
func main() {
// 创建一个容量为1的有缓冲区通道
ch := make(chan int, 1)
ch <- 10
fmt.Println("发送成功")
}
PS E:\go_test> go run .\main.go
发送成功
PS E:\go_test>
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。
就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的l en
函数获取通道内元素的数量,使用 cap
函数获取通道的容量,虽然我们很少会这么做。
close()
可以通过内置的 close() 函数关闭 channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main结束")
}
PS E:\go_test> go run .\main.go
0
1
2
3
4
main结束
PS E:\go_test>
如何优雅的从通道循环取值
当通过通道发送有限的数据时,我们可以通过 close 函数关闭通道来告知从该通道接收值的 goroutine 停止等待。
当通道被关闭时,往该通道发送值会引发 panic
,从该通道里接收的值一直都是类型零值。
那如何判断一个通道是否被关闭了呢?
我们来看下面这个例子:
package main
import "fmt"
// channel 练习
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}
从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是 for range
的方式。
单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。
例如,我们把上面的例子改造如下:
package main
import "fmt"
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
PS E:\go_test> go run .\main.go
0
1
4
9
16
...
其中,
1 chan<- int
是一个只能发送的通道,可以发送但是不能接收;
2 <-chan int
是一个只能接收的通道,可以接收但是不能发送。
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
通道总结
channel常见的异常总结,如下图:
注意:关闭已经关闭的 channel 也会引发 panic。
select 多路复用
在某些场景下我们需要同时从多个通道接收数据。
通道在接收数据时,如果没有数据可以接收将会发生阻塞。
你也许会写出如下代码使用遍历的方式来实现:
for{
// 尝试从 ch1 接收值
data, ok := <-ch1
// 尝试从 ch2 接收值
data, ok := <-ch2
…
}
这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。
为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。
每个case会对应一个通道的通信(接收或发送)过程。
select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
select可以同时监听一个或多个channel,直到其中一个channel ready。
package main
import (
"fmt"
"time"
)
func test1(ch chan string) {
time.Sleep(time.Second * 5)
ch <- "test1"
}
func test2(ch chan string) {
time.Sleep(time.Second * 2)
ch <- "test2"
}
func main() {
// 2个管道
output1 := make(chan string)
output2 := make(chan string)
// 跑2个子协程,写数据
go test1(output1)
go test2(output2)
// 用select监控
select {
case s1 := <-output1:
fmt.Println("s1=", s1)
case s2 := <-output2:
fmt.Println("s2=", s2)
}
}
PS E:\go_test> go run .\main.go
s2= test2
PS E:\go_test>
如果多个 channel 同时 ready,则随机选择一个执行。
package main
import (
"fmt"
)
func main() {
// 创建2个管道
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
go func() {
//time.Sleep(2 * time.Second)
int_chan <- 1
}()
go func() {
string_chan <- "hello"
}()
select {
case value := <-int_chan:
fmt.Println("int:", value)
case value := <-string_chan:
fmt.Println("string:", value)
}
fmt.Println("main结束")
}
PS E:\go_test> go run .\main.go
int: 1
main结束
PS E:\go_test>
可以用于判断管道是否存满。
package main
import (
"fmt"
"time"
)
// 判断管道有没有存满
func main() {
// 创建管道
output1 := make(chan string, 10)
// 子协程写数据
go write(output1)
// 取数据
for s := range output1 {
fmt.Println("res:", s)
time.Sleep(time.Second)
}
}
func write(ch chan string) {
for {
select {
// 写数据
case ch <- "hello":
fmt.Println("write hello")
default:
fmt.Println("channel full")
}
time.Sleep(time.Millisecond * 500)
}
}
并发安全和锁
有时候在Go代码中可能会存在多个 goroutine 同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。
举个例子:
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
PS E:\go_test> go run .\main.go
10000
PS E:\go_test>
上面的代码中我们开启了两个 goroutine 去累加变量 x
的值,这两个 goroutine 在访问和修改 x
变量的时候就会存在数据竞争,导致最后的结果与期待的不符。
1. 互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个 goroutine 可以访问共享资源。
Go语言中使用 sync
包的 Mutex
类型来实现互斥锁。
使用互斥锁来修复上面代码的问题:
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
PS E:\go_test> go run .\main.go
10000
PS E:\go_test>
使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;
当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的。
2. 读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
读写锁在Go语言中使用 sync
包中的 RWMutex
类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
读写锁示例:
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
// lock.Lock() // 加互斥锁
rwlock.Lock() // 加写锁
x = x + 1
// 假设读操作耗时10毫秒
time.Sleep(10 * time.Millisecond)
rwlock.Unlock() // 解写锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func read() {
// lock.Lock() // 加互斥锁
rwlock.RLock() // 加读锁
// 假设读操作耗时1毫秒
time.Sleep(time.Millisecond)
rwlock.RUnlock() // 解读锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
PS E:\go_test> go run .\main.go
168.5655ms
PS E:\go_test>
需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。
Sync
1. sync.WaitGroup 实现并发任务
在代码中生硬的使用 time.Sleep
肯定是不合适的,Go语言中可以使用 sync.WaitGroup
来实现并发任务的同步。
sync.WaitGroup
有以下几个方法:
方法名 | 功能 |
(wg * WaitGroup) Add(delta int) | 计数器+delta |
(wg *WaitGroup) Done() | 计数器-1 |
(wg *WaitGroup) Wait() | 阻塞直到计数器变为0 |
sync.WaitGroup
内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N
个并发任务时,就将计数器值增加 N
。
每个任务完成时通过调用 Done()
方法将计数器减 1
。
通过调用 Wait()
来等待并发任务执行完,当计数器值为 0
时,表示所有并发任务已经完成。
我们利用 sync.WaitGroup
将上面的代码优化一下:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(1)
// 启动另外一个goroutine去执行hello函数
go hello()
fmt.Println("main goroutine done!")
wg.Wait()
}
PS E:\go_test> go run .\main.go
main goroutine done!
Hello Goroutine!
PS E:\go_test>
需要注意 sync.WaitGroup
是一个结构体,传递的时候要传递指针。
2. sync.Once 执行一次
说在前面的话:这是一个进阶知识点。
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的 sync 包中提供了一个针对只执行一次场景的解决方案–sync.Once。
sync.Once 只有一个 Do 方法,其签名如下:
func (o *Once) Do(f func()) {}
注意:如果要执行的函数 f
需要传递参数就需要搭配闭包来使用。
加载配置文件示例
延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。
因为预先初始化一个变量(比如在 init
函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。
我们来看一个例子:
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
多个 goroutine 并发调用 Icon
函数时不是并发安全的,现代的编译器和CPU可能会在保证每个 goroutine 都满足串行一致的基础上自由地重排访问内存的顺序。
loadIcons
函数可能会被重排为以下结果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在这种情况下就会出现即使判断了 icons
不是 nil
也不意味着变量初始化完成了。
考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化 icons
的时候不会被其他的 goroutine 操作,但是这样做又会引发性能问题。
使用 sync.Once
改造的示例代码如下:
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
sync.Once 其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。
这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
3. sync.Map
Go语言中内置的 map 不是并发安全的。请看下面的示例:
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
上面的代码开启少量几个 goroutine 的时候可能没什么问题,当并发多了之后执行上面的代码就会报 fatal error: concurrent map writes
错误。
像这种场景下就需要为 map 加锁来保证并发的安全性了,Go语言的 sync 包中提供了一个开箱即用的并发安全版 map–sync.Map。开箱即用表示不用像内置的 map 一样使用 make 函数初始化就能直接使用。
同时 sync.Map 内置了诸如 Store、Load、LoadOrStore、Delete、Range 等操作方法。
package main
import (
"fmt"
"strconv"
"sync"
)
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
PS E:\go_test> go run .\main.go
k=:0,v:=0
k=:19,v:=19
k=:2,v:=2
k=:3,v:=3
k=:4,v:=4
k=:5,v:=5
k=:6,v:=6
k=:7,v:=7
k=:8,v:=8
k=:9,v:=9
k=:10,v:=10
k=:11,v:=11
k=:12,v:=12
k=:18,v:=18
k=:14,v:=14
k=:15,v:=15
k=:16,v:=16
k=:17,v:=17
k=:1,v:=1
k=:13,v:=13
PS E:\go_test>