go语言基础拾遗

这一辈子,我需要的不多,一碗饭一杯茶而已,但是我希望饭是你做的,茶是你泡的

Posted by yishuifengxiao on 2021-09-22

一 基础知识

1.1 new关键字的使用

尽管没有构造函数,go有一个内置的函数new,可以用来分配一个类型需要的内存。new(X)&X{}是等效的:

1
2
3
demo := new(HelloDemo)
// 等效
demo := &HelloDemo{}

用那种方式取决于你,但是你会发现,当需要去初始化结构体字段时,大多数人更喜欢使用后者,因为后者更易读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type HelloDemo struct {
name string
age int
}

func main() {
demo1 := new(HelloDemo)
demo1.name = "yishui"
demo1.age = 18

fmt.Println(demo1)

//对比

demo2 := &HelloDemo{
name: "yishui",
age: 18,
}
fmt.Println(demo2)
}

输出结果为:

1
2
&{yishui 18}
&{yishui 18}

1.2 匿名函数

1.2.1 无参无返回值(一)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
)

func main() {
i := 0

str := "mike"

//匿名函数,无参无返回值

f1 := func() {

//引用到函数外的变量

fmt.Printf("方式1:i = %d, str = %s\n", i, str)

}

//函数调用
f1()
}

输出结果为

1
方式1:i = 0, str = mike

1.3.2 无参无返回值(二)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main() {
i := 0

str := "mike"

func() { //匿名函数,无参无返回值

fmt.Printf("方式3:i = %d, str = %s\n", i, str)

}() //别忘了后面的(), ()的作用是,此处直接调用此匿名函数
}

别忘了后面的(), ()的作用是,此处直接调用此匿名函数


1.3.3 有参有返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
)

func main() {

v := func(a, b int) (result int) {

result = a + b

return

}(1, 1) //别忘了后面的(1, 1), (1, 1)的作用是,此处直接调用此匿名函数, 并传参

fmt.Println("v = ", v)
}

输出结果为

1
v =  2

1.3.4 defer和匿名函数结合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {

a, b := 10, 20

defer func(x int) { // a以值传递方式传给x

fmt.Println("defer:", x, b) // b 闭包引用

}(a)

a += 10

b += 100

fmt.Printf("a = %d, b = %d\n", a, b)

}

运行结果为

1
2
a = 20, b = 120
defer: 10 120

1.3.5 回调函数

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
37
38
package main

import "fmt"

func main() {

//函数调用,第三个参数为函数名字,此函数的参数,返回值必须和FuncType类型一致
result := Calc(1, 1, Add)
fmt.Println(result) //2

var f FuncType = Minus
fmt.Println("result = ", f(10, 2)) //result = 8

}

type FuncType func(int, int) int //声明一个函数类型, func后面没有函数名

//函数中有一个参数类型为函数类型:f FuncType

func Calc(a, b int, f FuncType) (result int) {

result = f(a, b) //通过调用f()实现任务

return

}

func Add(a, b int) int {

return a + b

}

func Minus(a, b int) int {

return a - b

}

输出结果为

1
2
2
result = 8

二 数据类型

2.1 数组的使用

2.1.1 数组的创建

1
2
3
var n int = 10
var a [n]int //err, non-constant array bound n
var b [10]int //ok

数组⻓度必须是常量,且是类型的组成部分。[2]int[3]int是不同类型。

2.1.2 初始化

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)

func main() {
a := [3]int{1, 2} // 未初始化元素值为 0
b := [...]int{1, 2, 3} // 通过初始化值确定数组长度
c := [5]int{2: 100, 4: 200} // 通过索引号初始化元素,未初始化元素值为 0
fmt.Println(a, b, c) //[1 2 0] [1 2 3] [0 0 100 0 200]
}

2.2 切片的使用

2.2.1 切片的创建和初始化

slice和数组的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符。

1
2
3
4
5
6
7
8
9
10
11
   var s1 []int //声明切片和声明array一样,只是少了长度,此为空(nil)切片
s2 := []int{}

//make([]T, length, capacity) //capacity省略,则和length的值相同
var s3 []int = make([]int, 0)
s4 := make([]int, 0, 0)

s5 := []int{1, 2, 3} //创建切片并初始化

// 初始元素个数为5,元素初始值为0,并预留10个元素的存储空间
s5 := make([]int, 5, 10)

注意:make只能创建slicemapchannel,并且返回一个有初始值(非零)。

2.2.2 切片截取

1
2
3
4
5
6
7
8
s[n]	切片s中索引位置为n的项
s[:] 从切片s的索引位置0len(s)-1处所获得的切片
s[low:] 从切片s的索引位置low到len(s)-1处所获得的切片
s[:high] 从切片s的索引位置0到high处所获得的切片,len=high
s[low:high] 从切片s的索引位置low到high处所获得的切片,len=high-low
s[low:high:max] 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low
len(s) 切片s的长度,总是<=cap(s)
cap(s) 切片s的容量,总是>=len(s)

2.3 字典的使用

2.3.1 字典的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
var m1 map[int]string //只是声明一个map,没有初始化, 此为空(nil)map
fmt.Println(m1 == nil) //true
//m1[1] = "mike" //err, panic: assignment to entry in nil map

//m2, m3的创建方法是等价的
m2 := map[int]string{}
m3 := make(map[int]string)
fmt.Println(m2, m3) //map[] map[]

m4 := make(map[int]string, 10) //第2个参数指定容量
fmt.Println(m4) //map[]
}

2.3.2 字典的初始化

1、定义同时初始化

1
2
var m1 map[int]string = map[int]string{1: "mike", 2: "yoyo"}
fmt.Println(m1) //map[1:mike 2:yoyo]

2、自动推导类型 :=

1
2
m2 := map[int]string{1: "mike", 2: "yoyo"}
fmt.Println(m2)

2.3.3 字典的操作

2.3.3.1 赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
m1 := map[int]string{1: "mike", 2: "yoyo"}
m1[1] = "xxx" //修改
m1[3] = "lily" //追加, go底层会自动为map分配空间
fmt.Println(m1) //map[1:xxx 2:yoyo 3:lily]

m2 := make(map[int]string, 10) //创建map
m2[0] = "aaa"
m2[1] = "bbb"
fmt.Println(m2) //map[0:aaa 1:bbb]
fmt.Println(m2[0], m2[1]) //aaa bbb

}

2.3.3.2 遍历

1
m1 := map[int]string{1: "mike", 2: "yoyo"}

迭代遍历1,第一个返回值是key,第二个返回值是value

1
2
3
4
5
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
//1 ----> mike
//2 ----> yoyo
}

迭代遍历2,第一个返回值是key,第二个返回值是value(可省略)

1
2
3
4
5
for k := range m1 {
fmt.Printf("%d ----> %s\n", k, m1[k])
//1 ----> mike
//2 ----> yoyo
}

2.3.3.3 删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
m1 := map[int]string{1: "mike", 2: "yoyo", 3: "lily"}

for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
}

fmt.Println("------------------")
delete(m1, 2) //删除key值为3的map

for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
}

}

输出结果为

1
2
3
4
5
6
1 ----> mike
2 ----> yoyo
3 ----> lily
------------------
1 ----> mike
3 ----> lily

2.3.3.4 判断是否存在

1
2
3
4
5
6
7
m1 := map[int]string{1: "mike", 2: "yoyo", 3: "lily"}

value, ok := m1[1]
fmt.Println("value = ", value, ", ok = ", ok) //value = mike , ok = true

value2, ok2 := m1[3]
fmt.Println("value2 = ", value2, ", ok2 = ", ok2) //value2 = , ok2 = false

输出的结果为

1
2
value =  mike , ok =  true
value2 = lily , ok2 = true

2.4 内置函数

2.4.1 append

append函数向 slice 尾部添加数据,返回新的slice对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

func main() {
var s1 []int //创建nil切换
//s1 := make([]int, 0)
s1 = append(s1, 1) //追加1个元素
s1 = append(s1, 2, 3) //追加2个元素
s1 = append(s1, 4, 5, 6) //追加3个元素
fmt.Println(s1) //[1 2 3 4 5 6]

s2 := make([]int, 5)
s2 = append(s2, 6)
fmt.Println(s2) //[0 0 0 0 0 6]

s3 := []int{1, 2, 3}
s3 = append(s3, 4, 5)
fmt.Println(s3) //[1 2 3 4 5]
}

输出结果为

1
2
3
[1 2 3 4 5 6]
[0 0 0 0 0 6]
[1 2 3 4 5]

append函数会智能地底层数组的容量增长,一旦超过原底层数组容量,通常以2倍容量重新分配底层数组,并复制原来的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main() {
s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}
}

输出结果为

1
2
3
4
5
6
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64

2.4.2 copy

函数 copy 在两个 slice 间复制数据,复制⻓度以 len 小的为准,两个 slice 可指向同⼀底层数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main() {
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[8:] //{8, 9}
s2 := data[:5] //{0, 1, 2, 3, 4}
copy(s2, s1) // dst:s2, src:s1

fmt.Println(s2) //[8 9 2 3 4]
fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]
}

输出结果为

1
2
[8 9 2 3 4]
[8 9 2 3 4 5 6 7 8 9]

三 go并发编程

3.1 go同步

在编写并发执行的代码时,你需要特别的关注在哪里和如何读写一个值。出于某些原因,例如没有垃圾回收的语言,需要你从一个新的角度去考虑你的数据,总是警惕着可能存在的危险。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"time"
)

var counter = 0

func main() {
for i := 0; i < 2; i++ {
go incr()
}
time.Sleep(time.Millisecond * 10)
}

func incr() {
counter++
fmt.Println(counter)
}

如果你觉得输出是12,不能说你对或者错。如果你运行上面的代码,确实如此。你很有可能得到那样的输出。但是,实际上这个输出是不确定的。为什么?因为我们可能有多个(这个例子中是2个)go协程同时写同一个变量counter。或者更糟的情况是一个协程正在读counter,而另一个协程正在写counter

这确实危险吗?绝对是的。counter++似乎看起来只是一行简单的代码,但是实际上它被拆分为很多汇编指令,具体依赖于你运行的软件和硬件平台。在上面的例子中,确实在大多数情况下运行良好。然而,另外一个可能的结果是counter等于0 时被2个协程同时读取,那么你将得到一个输出是1,1。还有更坏的结果,例如系统崩溃或者得到一个任意值然后自增。

在并发程序中,如果想安全的操作一个变量,唯一的手段就是读取该变量。你可以有任意多的程序去读,但是写必须是同步的。这里有几种方式实现,包括使用依赖于特殊cpu架构的一些真正的原子操作。然而,大多数时候都是使用一个互斥锁:

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
package main

import (
"fmt"
"sync"
"time"
)

var (
counter = 0
lock sync.Mutex
)

func main() {
for i := 0; i < 2; i++ {
go incr()
}
time.Sleep(time.Millisecond * 10)
}
func incr() {
lock.Lock()
defer lock.Unlock()
counter++
fmt.Println(counter)
}

互斥锁可以使你按顺序访问代码。因为sync.Mutex默认值是没有锁的,所以我们简单的定义了一个锁lock sync.Mutex

看起来似乎很简单?上面的例子带有欺骗性。当做并发编程时会发现一些列很严重的bug。首先,那些代码需要被保护一直都不是容易发现。虽然它可能是想使用一个低级锁(这个锁涉及了很多代码),这些潜在出错的地方是我们做并发编程首先要去考虑的。我们常常想要精确的锁,或者我们最终由一个10车道的高速突然转变成一个单车道道路。

另外一个问题是如何处理死锁。当使用一个锁时,这没有问题,但是如果你在代码中使用2个或者更多的锁,很容易出现一种危险的情况,即协程A拥有锁lockA,想去访问锁lockB,同时协程B拥有lockB并需要访问锁lockA

实际上使用一个锁也有可能发生死锁问题,即当我们忘记释放它时。但是这和多个锁引起的死锁为比起来,危害性不大(因为这真的很难发现),但是当你试着运行下面代码时,你可以看见发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"sync"
"time"
)

var (
lock sync.Mutex
)

func main() {
go func() { lock.Lock() }()
time.Sleep(time.Millisecond * 10)
lock.Lock()
}

迄今为止有很多并发编程我们都还没用见过。首先,由于我们可以同时有多个读操作,有一种常见的锁叫读写锁。它主要提供2中锁功能:一个锁定读和一个锁定写。在go语言中,sync.RWMutex就是这种锁。另外sync.Mutex结构不但提供了LockUnlock方法,也提供了RLockRLock方法,这里的R代表Read。虽然读写锁很常用,但是他们也给开发者带来一些额外的负担:我们不但要关注我们正在访问的数据,而且也要关注如何访问。

此外,部分并发编程不只是通过为数不多代码按顺序的访问变量,也需要协调多个go协程。例如,休眠10毫秒不是一种优雅的方法。如果一个go协程消耗的时间不止10毫秒呢?如果go协程消耗少于10毫秒,我们只是浪费了cpu?又或者可以等待go协程运行完毕,我们告诉另外一个go协程:嗨,我有一些新数据给你处理?

所有的这些事在没有通道(channels)的情况下都是可以实现的。当然,对于更简单的例子,我认为你应该使用基本的功能例如sync.Mutexsync.RWMutex

3.2 go通道

并发编程的挑战主要在于数据共享。如果你的go协程没有共享数据,你就不需要担心同步他们。但是,对于所有的系统,这不是一个选择。实际上,很多系统以完全相反的目标构建:在多个请求中共享数据。内存缓存或者数据库都是这方面的好例子。这正变得越来越流行的事实。

通道通过解决数据共享问题,让并发编程变得更加清晰。通道是一个通信管道,它用于go协程之间传递数据。换句话说,go协程可以通过通道,传递数据给另外一个go协程。其结果就是,在任何时候,仅有一个go协程可以访问数据。

通道与所有其他的东西一样,也有类型。这个类型,就是将要在通道中传递的数据的类型。例如,创建一个通道,这个通道可以用来传递一个整数,我们可以这样:

1
c := make(chan int)

这个通道的类型是chan int。因此,将这个通道传递给一个函数,可以这样声明:

1
func worker(c chan int) { ... }

通道支持2种操作:接收和发送。我们可以使用下面方式往通道发送数据:

1
CHANNEL <- DATA

可以使用下面方式从通道接收数据:

1
VAR := <-CHANNEL

箭头的方向就是数据的流动方向。当发送数据时,数据流入通道。当接收数据时,数据流出通道。

最后,在查看我们的第一个例子之前,我们需要知道从一个通道接收或者发送数据时会阻塞。当我们从一个通道接收数据时,直到数据可用, go协程才会继续执行。类似的,往一个通道发送数据时,在数据被接收之前, go协程也不会继续执行。

假设这种情况:对输入数据,我们想通过不同的协程去处理。这是一种常见的需求。如果通过go协程接收输入的数据,并进行数据密集型处理,那么,客户端会有超时风险。首先,我们将写出worker。这可以是一个简单的函数,但是我会让它变成一个结构体的部分,因为我们之前从来没有这样使用过go协程:

1
2
3
4
5
6
7
8
9
10
type Worker struct {
id int
}

func (w Worker) process(c chan int) {
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
}
}

我们的worker很简单。它会一直等待数据,直到数据可用, 然后处理它。它在一个循环中,永远尽职的等待更多的数据并处理。

为了使用上面的worker,我们首先要做的是启动一些worker

1
2
3
4
5
c := make(chan int)
for i := 0; i < 4; i++ {
worker := Worker{id: i}
go worker.process(c)
}

然后我们可以给它们一些工作:

1
2
3
4
for {
c <- rand.Int()
time.Sleep(time.Millisecond * 50)
}

这是完整的代码,运行它:

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
package main

import (
"fmt"
"math/rand"
"time"
)

func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
worker := &Worker{id: i}
go worker.process(c)
}

for {
c <- rand.Int()
time.Sleep(time.Millisecond * 50)
}
}

type Worker struct {
id int
}

func (w *Worker) process(c chan int) {
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
}
}

我们不知道哪个worker将获得数据。我们所知道的是,go语言确保,往一个通道发送数据时,仅有一个单独的接收器可以接收。

注意:通道是唯一共享的状态,通过通道,可以安全的,并发发送和接收数据。通道提供了我们需要的所有同步代码,并且也确保,在任意的特定时刻,只有一个go协程,可以访问数据的特定部分。

3.3 带缓存的通道

在上面的代码中,如果输入的数据,超过我们的处理能力,会发生什么?你可以模拟这种场景,在worker接收到数据后,让worker执行time.Sleep

1
2
3
4
5
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
time.Sleep(time.Millisecond * 500)
}

main函数中会发什么呢?接收用户的输入数据(这里通过一个随机的数字生成器模拟)会被阻塞,因为往通道发送数据时,没有可用的接收者。

在这种情况下,你需要确保数据被处理,你可能想要让客户端阻塞。在其他情况下,你可能愿意不确保数据被处理。这里有一些流行的策略能完成此事。首先是将数据缓存起来。如果没有worker可用,我们想将数据,暂时存放在一个有序的队列中。通道实现了这种缓存功能。当我们使用make创建一个通道时,我们可以指定通道的长度:

1
c := make(chan int, 100)

你可以这样调整,但是你将注意到,处理过程仍然不顺利。带缓存通道没有提供更多的功能;它只不过是为挂起的作业提供一个队列,以一种更好的方式处理数据突然飙升。在我们示例中,我们可以连续不断的发送更多的、超出worker处理能力的数据。

然而,通过查看通道的长度,我们可以了解到,带缓存通道中有待处理的缓存数据:

1
2
3
4
5
for {
c <- rand.Int()
fmt.Println(len(c))
time.Sleep(time.Millisecond * 50)
}

你可以看到,带缓存通道的长度在不断增长,直到装满为止,到时,往通道发送的数据又开始被阻塞。

3.4 select

即使借助缓存,我们还是需要开始丢弃一些消息。我们不能使用一个无限大的内存,并指望人工的释放它。所以我们使用go语言的select

在语法结构上,select看起来有点类似switch。通过select,我们能写出解决通道不可写问题的代码。首先,让我们去掉通道的缓存,这样可以更清晰的看到select是如何工作的。

1
c := make(chan int)

接下来,我们修改for循环:

1
2
3
4
5
6
7
8
9
10
for {
select {
case c <- rand.Int():
//可选的代码
default:
//这里可以留下空行以丢弃数据
fmt.Println("dropped")
}
time.Sleep(time.Millisecond * 50)
}

我们每秒往通道中发送20个信息,但是我们的程序,每秒只能处理10个信息;因此,有一半的信息被丢弃。

这仅仅只是我们使用select完成一些事的开始。使用select的最主要目的是,通过它管理多个通道。给定多个通道,select将阻塞直到有一个通道可用。如果没有可用的通道,当提供了default语句时,执行该分支。当多个通道都可用时,选择其中的一个通道是随机的。

很难想出一个简单的例子来证明这种行为,因为这是一种高级特性。在下一小节可能有助于说明这个问题。

3.5 超时

我们已经学习了缓存信息,并且丢弃它们的简单做法。另外一种比较流行的做法是使用超时。我们将阻塞一段时间,但是不是一直阻塞。这在go中很容易实现。老实说,这个语法有点难接受,确是非常灵活、有用的特性,我不能不介绍它。

为了使阻塞达到最大值,我们可以使用time.After函数。让我们看看它会发生什么神奇的事。为了使用这种方式,我们数据发送变成:

1
2
3
4
5
6
7
8
for {
select {
case c <- rand.Int():
case <-time.After(time.Millisecond * 100):
fmt.Println("timed out")
}
time.Sleep(time.Millisecond * 50)
}

time.After将返回一个通道,所以我们可以对它使用select语句。这个通道在经过指定的时间后会被写入。就是这样。没有什么比这个更神奇了。如果你依然觉得奇怪,这里实现了一个after,如下所示:

1
2
3
4
5
6
7
8
func after(d time.Duration) chan bool {
c := make(chan bool)
go func() {
time.Sleep(d)
c <- true
}()
return c
}

回到我们的select语句,这里有一些好玩的东西。首先,如果你在后面添加default分之会发生什么?你能猜到吗?试试。如果你不确定会发生什么,记住如果通道不可用的话,default分支会被立即执行。

此外,time.After是一个chan time.Time类型的通道。在上面的例子中,我们将发送给通道的值简单丢弃掉。如果你想,你也可以获取到这个值:

1
2
case t := <-time.After(time.Millisecond * 100):
fmt.Println("timed out at", t)

密切注意我们的select。注意我们正在往c中发送数据,但是从time.After收取。不管我们是从通道中接收数据、发送数据或者收发数据,select工作机制都一样:

  • 第一个可用的通道被选中
  • 如果多个通道可用,随机选中一个通道
  • 如果没有通道可用,default分之被执行
  • 如果没有default分支,select将阻塞

最后,在for循环中使用select也是比较常见的,例如:

1
2
3
4
5
6
7
8
9
for {
select {
case data := <-c:
fmt.Printf("worker %d got %d\n", w.id, data)
case <-time.After(time.Millisecond * 10):
fmt.Println("Break time")
time.Sleep(time.Second)
}
}

四 并发相关

4.1 全局唯一性操作

代码只执行一次,用来保证全局的唯一性操作

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
package main

import (
"fmt"
"sync"
"time"
)

var a string
var once sync.Once

func setup() {
a = "hello world"
fmt.Println("初始化操作")
}

func doPrint() {
once.Do(setup)
fmt.Println("---- doPrint " + a)
}

func main() {
go doPrint()
go doPrint()
time.Sleep(time.Second * 10)
}

输出结果为

1
2
3
初始化操作
---- doPrint hello world
---- doPrint hello world

4.2 异常捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
)

func main() {
var (
a = 10
b = 0
)

defer func() {
if err := recover(); err != nil {
fmt.Println("捕获到异常了")
fmt.Println(err)
}
}()

c := a / b
fmt.Println(c)
}

输出的结果为

1
2
捕获到异常了
runtime error: integer divide by zero