Function
函数声明
函数声明包含:
函数名
形参列表(变量名+类型)
无默认参数值
函数最外层局部变量
调用函数必须按照声明顺序为所有形参提供实参
实参通过值方式传递,形参是实参的值拷贝。若实参是引用类型(pointer/slice/map/function/channel),即便形参是值拷贝,也可以修改实参
返回值列表(可省略)
函数最外层局部变量
多个返回值在
()
内,只返回一个无名变量()
可省略。每个返回值被声明为一个局部变量,并被初始化为对应类型零值若有返回值列表,函数则必须以
return
语句结尾Go支持函数多返回值,若函数返回值列表都显示命名变量,则
return
语句可省略操作数(bare return
)函数体
1 | func hypot(x, y float64) float64 { // hypot 函数名;x, y float64 形参列表;float64 返回值列表的无名变量类型 |
判断两个函数是否为相同函数类型:
- 函数形参列表的变量类型(变量名不影响)一一对应
- 函数返回值列表的变量类型(变量名不影响)一一对应
递归
Go允许函数递归,递归实现Fibonacci数列
1 | func Fibonacci(n int) int { |
函数值
Go中函数为第一类值(first-class values),即函数和其他类型一样,拥有类型(函数类型),可被赋值给变量,可传递给函数做实参,可做函数的返回值
1 | func square(n int) int { return n * n } |
函数类型的零值为nil
。调用值为nil
的函数值会panic
1 | var f func(int) int // f为函数类型变量,值为nil // 函数类型变量声明 |
函数值之间不能进行比较,故函数值无法作为map的key。函数值可与nil
比较
关于函数类型的例子
1 | type Calculate func(int, int) // 声明函数类型 |
关于函数值做形参列子
1 | type CalculateOps func(int, int) int // 声明函数类型 |
匿名函数
普通命名函数只能在包级别进行声明,匿名函数(anonymous function)可在函数内进行,并且可访问函数的完整词法域
1 | func (形参列表) [返回值列表] { 函数体 } |
匿名函数实现闭包(closures)
1 | func squares() func() int { // 函数squares返回一个匿名函数 |
迭代变量陷阱
1 | func main() { |
for…range
引入新的词法域,变量d
在该词法域中被声明,匿名函数中记录的是变量d
的内存地址而非某次循环的值
闭包函数记录的是闭包变量的内存地址,闭包变量的值仅在闭包函数执行时确定for...range
引入新的词法域,变量d
在该词法域中被声明,并只会被初始化一次(多次迭代d只会有一个内存地址),而并非每次迭代都分配新地址,产生一个新的变量d
。所以最后printDirs
中的d
都指向同一个内存地址,最后打印出来的就都是d
最后迭代值。
解决办法d := d
,是重新声明一个变量d
,重新分配内存给新变量d
,接下来的词法域中新变量d
覆盖迭代d
,printDirs
中5个匿名函数里的d
都是指向5个不同的内存地址
1 | var echo []func() |
可变参数
在声明可变参数函数时,在形参列表最后一个参数类型前加上…
,表示该参数可接收任意数量该类型的实参。
对于传入的多个实参,Go先创建一个array,并将多个实参复制到array中,在把array的一个slice作为实参传入,所以函数接收到的是[]T
类型的slice。若以slice作为多个实参传入,则传入参数写成slice…
1 | func sum(vals ...int) int { |
Deferred函数
Go使用defer
语句来延迟执行函数,延迟发生在defer所在函数执行完毕时,不管是return
正常结束还是panic
异常结束。(defer在return/panic前执行)
defer
后接的必须是函数调用(即必须要有()
)
1 | // WRONG |
使用defer
需要注意的点:
defer执行顺序为先进后出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func defer_call() {
defer func() { fmt.Println("defer1") }()
defer func() { fmt.Println("defer2") }()
defer func() { fmt.Println("defer3") }()
panic("panic")
}
func main() {
defer_call()
/*
defer3
defer2
defer1
panic: panic
*/
}defer函数的参数在被defer声明时就已经确定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1
b := 2
defer calc("1", a, calc("10", a, b))
a = 0
defer calc("2", a, calc("20", a, b))
b = 1
}
/*
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4
*/执行分析:
- 程序执行到
defer calc("1", a, calc("10", a, b))
时,由于defer函数的参数在声明时就要确定,所以需要先计算出外层calc()
的b
值,于是执行calc(“10”, a, b)
,calc(“10”, 1, 2)
输出10 1 2 3
,返回3
- 第一个defer函数参数确定为
defer calc(“1”, 1, 3)
- 程序执行到
defer calc("2", a, calc("20", a, b))
时,此时变量a
被改成0
(a=0
),再次由于defer函数的参数在声明时需要确定,所以需要先计算外层calc()
的b
值,于是执行calc(“20”, a, b)
,calc(“20”, 0, 2)
输出20 0 2 2
,返回2
- 第二个defer函数参数确定为
defer calc(“2”, 0, 2)
b=1
,由于第二个defer函数参数已经确定,确定后既是修改变量b
的值也不影响第二个defer函数的参数- 按照defer先进后出规则,先执行第二个defer
defer calc(“2”, 0, 2)
输出2 0 2 2
- 执行第一个defer
defer calc(“1”, 1, 3)
输出1 1 3 4
- 程序执行到
defer可读改有名返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func doubleScore(source float32) (score float32) {
defer func() {
if score < 1 || score >= 100 {
//将影响返回值
score = source
}
}()
score = source * 2
return
//return source * 2
}
func main() {
fmt.Println(doubleScore(0)) //0
fmt.Println(doubleScore(20.0)) //40
fmt.Println(doubleScore(50.0)) //50
}1
2
3
4
5
6
7
8func c() (i int) {
defer func() { i++ }()
return 1
}
func main() {
fmt.Println(c()) // 2
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
func DeferFunc4(i int) int {
t := i
defer func(t int) {
t += 3
}(t)
return t
}
func main() {
println(DeferFunc1(1)) // 4
println(DeferFunc2(1)) // 1
println(DeferFunc3(1)) // 3
fmt.Println(DeferFunc4(1)) // 1
}
panic & recover
1 | func badCall() { |
Method
对象简单为一个值或变量,此对象包含方法,方法是和特殊类型关联的函数。
面向对象程序是通过方法来表达其属性和对应的操作,如此使用对象时,不需要直接操作对象而是借助方法完成操作
方法声明
在函数声明时,在函数名前加上一个变量,即方法的声明。此变量称之为receiver(接收器)
1 | type Point struct { |
方法和struct成名变量都在同一个命名空间中,所以方法名和成员名不能相同,若在声明一个X
方法,则会和成员变量X
冲突
Go中除了底层类型为pointer和interface的命名类型外,其余的所有命名类型都可定义方法(数值/字符串/slice/map……)
指针对象的方法
1 | func (p *Point) ScaleBy(factor float64) { |
指针对象的方法,即方法的receiver(接收器)为指针对象。上面方法的方法名为(*Point).ScaleBy
只有类型(T)和指向类型的指针(*T)才能作为receiver(接收器),若类型名本身是指针无法作为receiver(接收器)。正如上面所讲,底层类型为指针的命名类型不可定义方法
1 | type P *int |
如何调用指针对象的方法
1 | type Point struct { |
不管方法的receiver是T
还是*T
类型,都可通过T
和*T
类型对象调用,编译器会自动识别。但个人最好明白是编译器帮忙做了转换
1 | type Student struct{} |
嵌套扩展方法
1 | type Point struct { |
Point类的方法也被引入了ColoredPoint,但一个ColoredPoint并不是一个Point,而是ColoredPoint has Point
方法值和方法表达式
方法值和函数值类似,一般使用方法常规为object.method()
,object.method
称为选择器(没有()
),选择器返回一个方法值,即将方法绑定到特定接收器变量的函数,直接通过该函数接收参数调用方法
1 | var cp ColoredPoint |
1 | type Rocket struct { /* ... */ } |
方法表达式
1 | p := Point{1, 2} |
Interface
接口声明
接口是方法的集合,只要实现了接口内所有的方法就实现了接口,即当一个具体类型实现了接口内要求的所有方法,则该类型就是该接口的实例
接口支持内嵌
1 | type Reader interface { // 接口声明 |
实现接口的条件
指针对象方法接口实现
对于指针对象的方法,需要特别注意一点:指针类型的receiver,只有指针类型对象实现该method
values | method receiver |
---|---|
T | (t T) |
*T | (t T) and (t *T) |
method receiver | values |
---|---|
(t T) | T and *T |
(t *T) | *T |
在指针对象的方法中,无论是T
还是*T
类型都可调用T
和*T
方法,即T
类型也能调用*T
方法。但接口实现中,*T
方法只有*T
类型实现了,T
类型并无实现*T
方法
1 | // 无论是`T`还是`*T`类型都可调用`T`和`*T`方法 |
1 | type People interface { |
接口断言
由于接口的实现采用隐式实现,即只要实现了接口内所有方法,就实现了接口。当要判断一个类型是否实现接口时,可通过如下方法:
1 | var _ io.Writer = (*bytes.Buffer)(nil) // nil转换为*bytes.Buffer类型,并尝试赋值操作(空标识符,类型为io.Writer)。无编译报错,则*bytes.Buffer实现了io.Writer接口 |
接口值
接口本质上是由2部分组成:
- 指向值类型的指针
- 指向值内容的指针
1 | type InterfaceStruct struct { |
接口值是可进行比较的,但当接口的pointType
类型是不可比较类型(slice
/map
)则无法进行比较操作。这里尤其注意接口和nil
的比较
判断接口是否等于nil
,需要接口的pointType
和 pointValue
都为nil
时才等于nil
1 | type People interface { |
类型断言
类型断言语法:x.(T)
(x
必须为接口类型且不为nil
)
类型断言主要分2种情况:
T
为具体类型,x.(T)
检查x
的类型是否和T
类型相同判断成功后会返回
x
的pointValue1
2
3
4w := 1
if i, ok := interface{}(w).(int); ok { //由于x.(T)中x必须为接口类型,所以需将i进行类型转换interface{}(i)
fmt.Println("ok", i) // ok 1
}T
为接口类型,x.(T)
检查x
的类型是否实现T
接口判断成功后,
x
的pointType和pointValue都不变,但x
的类型被转换为T
类型
type switch
1 | i := 1 |
Goroutines & Channels
Goroutines
Go语言中,每一个并发的执行单元叫作一个goroutine。程序启动时,main()
函数即在一个goroutine中运行,此gouroutine称之为main goroutine
创建一个gouroutine
1 | go function() |
1 | func main() { |
上面代码在main()
函数中创建了一个goroutine来执行一个匿名函数,但输出的结果仅有main start
和main end
并没有新建goroutine的输出。因为新建完goroutine后,main()
函数继续往下执行,执行完后main goroutine直接就结束了,由于main goroutine结束其所创建的goroutine也会被杀掉,导致新建的goroutine还没来得及输出就已经被结束了。改进方案就是需要用到channel
、sync
等来解决
channel方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14func main() {
c := make(chan bool)
go func() {
fmt.Println("GO GO GO!!!")
c <- true
close(c) // 必须close,否则for...range会出现死锁
}()
//使用for...range对channel进行不断读取,直到close channel
for v := range c {
fmt.Println(v)
}
}sync方式
sync.WaitGroup()
方式适用于知道具体数目goroutine的情况1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22func add(wg *sync.WaitGroup, index int) {
a := 0
for i := 0; i < 100; i++ {
a += i
}
fmt.Println(index, a)
wg.Done() // 通知wg.Wait(),goroutine已执行完
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // 根据CPU核心数并发执行
wg := sync.WaitGroup{}
wg.Add(10) // 10个goroutine
//&wg 需要传递指针,需要在add()中进行Done()操作,不能值拷贝而需要地址拷贝
for i := 0; i < 10; i++ {
go add(&wg, i)
}
wg.Wait()
}
Channels
channel声明
channel是goroutine间的通讯机制,允许让一个goroutine向另一个goroutine发送消息,每个channel都必须规定其类型,表示该channel只允许传输该类型的消息,一般表示为chan T
。使用内置的make()
函数创建channel
1 | ch := make(chan int) // 创建一个int类型的channel |
channel为引用类型,零值为nil
。两个相同类型的channel可比较==
/!=
channel主要操作有2个:
- 发送:
ch <- xxx
- 接收:
xxx = <- ch
、<- ch
关闭channel使用close()
函数,close(ch)
后,对channel进行发送操作会引发panic,接收操作仍可接收到之前已成功发送的数据。不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收
channel缓存
channel以是否能缓存分为2种
无缓存channel
无缓存channel将会阻塞goroutine对channel的操作。当一个goroutine对一个无缓存channel进行发送操作后,该goroutine会立马阻塞,直到有goroutine对该channel进行接收操作,接收操作完成后发送&接收的goroutine才能继续执行后面语句。接收操作先发送也是如此。
由于无缓存channel会导致goroutine的阻塞,发送和接收者相当于做了一次同步操作,所以无缓存channel也称为同步channel
1
make(chan T) // 创建无缓存channel
1
2
3
4
5
6
7
8
9
10
11
12func main() {
c := make(chan string) // 创建无缓存channel
fmt.Println("main start")
go func() { // 新建goroutine执行匿名函数
time.Sleep(3 * time.Second)
fmt.Println("groutine")
c <- "done" // 发送数据到无缓存channel
}()
<-c // 从无缓存channel接收数据。一般情况下main goroutine执行较快,到此程序阻塞,等待其他goroutine执行发送操作
fmt.Println("main end")
}有缓存channel
有缓存channel内部持有一个缓存队列,向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素
如果缓存队列已满,发送操作将会阻塞;如果缓存队列为空,接收操作将会阻塞
1
make(chan T, N) // 创建有缓存channel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func main() {
//c := make(chan string)
c := make(chan string, 1) // 有缓存channel
fmt.Println("main start")
go func() {
time.Sleep(3 * time.Second)
fmt.Println("groutine")
<-c
}()
c <- "done" // 向有缓存channel发送数据,由于有1个缓存空间,所以一次发送操作不会阻塞,main goroutine继续执行结束,另一个goroutine还没来得及执行就已结束,无输出
fmt.Println("main end")
}
/*
main start
main end
*/
pipeline
channels可用于将多个goroutine连接在一起,一个channel的输出作为下一个channel的输入,这类串联的channel称之为pipeline
go Counter -> chan naturals -> go Squarer -> chan squares -> go Printer
1 | func main() { |
单方向channel
默认情况下channel都是双向的,但可创建只能发送或只能接收的单方向channel
1 | ch := make(chan int) |
1 | func squarer(out chan<- int, in <-chan int) { |
select多路复用
Go语言中select...case
用于同时监听多个channel,case
语句必须是一个面向channel的操作
1 | select { |
select…case
执行流程:
- select中只要有一个case能执行成功,则立刻执行
- 如果同一时间有多个case均能执行成功则伪随机方式抽取任意一个case执行
- 如果所有case都不能执行成功,则执行
default
。如果没有default
,则一直阻塞,直到有一个case
能执行
1 | func main() { |
基于共享变量的并发
sync.Mutex互斥锁
sync.Mutex
包中提供了Lock()
和Unlock()
方法,当需要访问共享变量时,通过调用Lock()
/Unlock()
方法获取互斥锁。如果互斥锁被其他goroutine调用Lock()
先获取,那么访问该共享变量的goroutine便会阻塞,阻塞一直到其他goroutine调用Unlock()
释放互斥锁。
Lock()
和Unlock()
之间的代码段,称之为临界区
1 | var ( |
sync.RWMutex读写锁
sync.RWMutex
包提供多读单写锁,读锁:sync.RWMutex.Rlock()
/sync.RWMutex.RUnlock()
;写锁:sync.RWMutex.Lock()
/sync.RWMutex.Unlock()
Rlock()
只能在临界区共享变量没有任何写入操作时才可用。
1 | var mu sync.RWMutex |
sync.Once初始化
sync.Once
中的Do()
方法能在并发环境下,确保函数只被执行一次,sync.Once.Do(f)
。常用于做初始化操作
1 | var a string |
竞争条件检测
Go提供竞争条件检测分析工具,只要在go build
、go run
或者go test
命令后面加上-race
即可
1 | go test -race mypkg // test the package |
Goroutines和线程
os线程使用固定大小的栈(一般2MB);goroutine的栈大小不是固定的,可动态伸缩最大为1GB
os线程被操作系统内核调度,进行上下文切换;goroutine由Go调度器调度,Go调度器调度不需要进行上下文切换,调度代价小
Go的调度器使用了一个叫做GOMAXPROCS
的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数
进程有进程id(pid
),线程有线程id(tid
),goroutine没有id