目录
  1. 1. Function
    1. 1.1. 函数声明
    2. 1.2. 递归
    3. 1.3. 函数值
      1. 1.3.1. 匿名函数
        1. 1.3.1.1. 迭代变量陷阱
      2. 1.3.2. 可变参数
      3. 1.3.3. Deferred函数
      4. 1.3.4. panic & recover
  2. 2. Method
    1. 2.1. 方法声明
    2. 2.2. 指针对象的方法
    3. 2.3. 嵌套扩展方法
    4. 2.4. 方法值和方法表达式
  3. 3. Interface
    1. 3.1. 接口声明
    2. 3.2. 实现接口的条件
      1. 3.2.1. 指针对象方法接口实现
      2. 3.2.2. 接口断言
      3. 3.2.3. 接口值
      4. 3.2.4. 类型断言
      5. 3.2.5. type switch
  4. 4. Goroutines & Channels
    1. 4.1. Goroutines
    2. 4.2. Channels
      1. 4.2.1. channel声明
      2. 4.2.2. channel缓存
      3. 4.2.3. pipeline
      4. 4.2.4. 单方向channel
      5. 4.2.5. select多路复用
  5. 5. 基于共享变量的并发
    1. 5.1. sync.Mutex互斥锁
    2. 5.2. sync.RWMutex读写锁
    3. 5.3. sync.Once初始化
      1. 5.3.1. 竞争条件检测
    4. 5.4. Goroutines和线程

Function

函数声明

函数声明包含:

  • 函数名

  • 形参列表(变量名+类型)

    无默认参数值

    函数最外层局部变量

    调用函数必须按照声明顺序为所有形参提供实参

    实参通过值方式传递形参是实参的值拷贝。若实参是引用类型(pointer/slice/map/function/channel),即便形参是值拷贝,也可以修改实参

  • 返回值列表(可省略)

    函数最外层局部变量

    多个返回值在()内,只返回一个无名变量()可省略。每个返回值被声明为一个局部变量,并被初始化为对应类型零值

    若有返回值列表,函数则必须以return语句结尾

    Go支持函数多返回值,若函数返回值列表都显示命名变量,则return语句可省略操作数(bare return)

  • 函数体

1
2
3
4
5
func hypot(x, y float64) float64 {	// hypot 函数名;x, y float64 形参列表;float64 返回值列表的无名变量类型
// func hypot(x float64, y float64) float64 {
return math.Sqrt(x*x + y*y) // 函数体
}
fmt.Println(hypot(3,4)) // "5"

判断两个函数是否为相同函数类型:

  1. 函数形参列表变量类型(变量名不影响)一一对应
  2. 函数返回值列表变量类型(变量名不影响)一一对应

递归

Go允许函数递归,递归实现Fibonacci数列

1
2
3
4
5
6
7
8
9
10
11
12
func Fibonacci(n int) int {
if n < 2 {
return n
}
return Fibonacci(n-2) + Fibonacci(n-1)
}

func main() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", Fibonacci(i))
}
}

函数值

Go中函数为第一类值(first-class values),即函数和其他类型一样,拥有类型(函数类型),可被赋值给变量,可传递给函数做实参,可做函数的返回值

1
2
3
func square(n int) int { return n * n }
f := square // function value 函数值
fmt.Println(f(3)) // 9

函数类型的零值为nil。调用值为nil的函数值会panic

1
2
var f func(int) int		// f为函数类型变量,值为nil		// 函数类型变量声明
f(3) // panic !!!

函数值之间不能进行比较,故函数值无法作为map的key。函数值可与nil比较

关于函数类型的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Calculate func(int, int)		// 声明函数类型

func (c *Calculate) Echo() { // 函数类型实现Echo()方法
fmt.Println("function type")
}

func add(a, b int) { // 普通函数
fmt.Println(a + b)
}

func mul(a, b int) { // 普通函数
fmt.Println(a * b)
}

func main() {
x := Calculate(add) // 将add函数强制转换为Calculate函数类型,能转换的前提是他们的底层类似都是相同类型的函数(形参类型、返回值类型相同)
y := Calculate(mul) // 将mul函数强制转换为Calculate函数类型

x(1, 1) // 保持原有的函数的功能
y(2, 2)
x.Echo() // 新增Calculate类型的功能
y.Echo()
}

关于函数值做形参列子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type CalculateOps func(int, int) int		// 声明函数类型

func Add(a, b int) int { // 普通函数
return a + b
}

func Mul(a, b int) int { // 普通函数
return a * b
}

func Calculate(a, b int, f CalculateOps) int { // 函数类型形参 // Add、Mul和CalculateOps为同一类型函数(形参和返回值类型都相同)
return f(a, b) // 执行传入函数
}

func main() {
fmt.Println(Calculate(1, 1, Add))
fmt.Println(Calculate(2, 2, Mul))
}

匿名函数

普通命名函数只能在包级别进行声明,匿名函数(anonymous function)可在函数内进行,并且可访问函数的完整词法域

1
func (形参列表) [返回值列表] { 函数体 }

匿名函数实现闭包(closures)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func squares() func() int {		// 函数squares返回一个匿名函数
var x int
return func() int { // 匿名函数
x++
return x * x
}
}

func main() {
f := squares() // f 为函数值 // squares函数返回后,变量x仍隐式存在与f
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
迭代变量陷阱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var printDirs []func() // 匿名函数slice
dirs := []string{"one", "two", "three", "four", "five"}

for _, d := range dirs {
//d := d // solution
printDirs = append(printDirs, func() { // 匿名函数
fmt.Printf("%s ", d) // wrong
})
}

for _, dir := range printDirs {
dir() // five five five five five
}
}

for…range引入新的词法域,变量d在该词法域中被声明,匿名函数中记录的是变量d的内存地址而非某次循环的值

闭包函数记录的是闭包变量的内存地址,闭包变量的值仅在闭包函数执行时确定
for...range引入新的词法域,变量d在该词法域中被声明,并只会被初始化一次(多次迭代d只会有一个内存地址),而并非每次迭代都分配新地址,产生一个新的变量d。所以最后printDirs中的d都指向同一个内存地址,最后打印出来的就都是d最后迭代值。
解决办法d := d,是重新声明一个变量d,重新分配内存给新变量d,接下来的词法域中新变量d覆盖迭代dprintDirs中5个匿名函数里的d都是指向5个不同的内存地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var echo []func()
var print []int

for i := 0; i < 5; i++ {
print = append(print, i) // ok
echo = append(echo, func() {
fmt.Printf("%d ", i) // wrong
})
}

for _, p := range print {
fmt.Printf("%d: ", p) // 0: 1: 2: 3: 4:
}

for _, e := range echo {
e() // 5 5 5 5 5
}

可变参数

在声明可变参数函数时,在形参列表最后一个参数类型前加上,表示该参数可接收任意数量该类型的实参。

对于传入的多个实参,Go先创建一个array,并将多个实参复制到array中,在把array的一个slice作为实参传入,所以函数接收到的是[]T类型的slice。若以slice作为多个实参传入,则传入参数写成slice…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func sum(vals ...int) int {
total := 0
for _, v := range vals { // vals == []int{1, 2, 3}
total += v
}
return total
}

func main() {
fmt.Println(sum(1, 2, 3)) // 6

values := []int{1, 2, 3}
fmt.Println(sum(values...)) // 6 // 以slice作为可变参数传入
}

Deferred函数

Go使用defer语句来延迟执行函数,延迟发生在defer所在函数执行完毕时,不管是return正常结束还是panic异常结束。(defer在return/panic前执行)

defer后接的必须是函数调用(即必须要有())

1
2
3
4
5
// WRONG
defer func() {...}

// RIGHT
defer func() {...}() // 必须有()

使用defer需要注意的点:

  1. defer执行顺序为先进后出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func 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
    */

    }
  2. defer函数的参数在被defer声明时就已经确定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func 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
    */

    执行分析:

    1. 程序执行到defer calc("1", a, calc("10", a, b))时,由于defer函数的参数在声明时就要确定,所以需要先计算出外层calc()b值,于是执行calc(“10”, a, b)calc(“10”, 1, 2)输出10 1 2 3,返回3
    2. 第一个defer函数参数确定为defer calc(“1”, 1, 3)
    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
    4. 第二个defer函数参数确定为defer calc(“2”, 0, 2)
    5. b=1,由于第二个defer函数参数已经确定,确定后既是修改变量b的值也不影响第二个defer函数的参数
    6. 按照defer先进后出规则,先执行第二个deferdefer calc(“2”, 0, 2)输出2 0 2 2
    7. 执行第一个deferdefer calc(“1”, 1, 3)输出1 1 3 4
  3. defer可读改有名返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func 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
    8
    func 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
    37
    func 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func badCall() {
panic("bad end")
}

func test() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("recover %s\r\n", e)
}
}()
badCall()
fmt.Printf("After bad call\r\n") // 不会执行,即使已经recover(),但panic()后直接return
}

func main() {
fmt.Printf("calling test\r\n")
test()
fmt.Printf("recover completed\r\n") // 若无recover(), 此句不会执行
}

Method

对象简单为一个值或变量,此对象包含方法,方法是和特殊类型关联的函数

面向对象程序是通过方法来表达其属性和对应的操作,如此使用对象时,不需要直接操作对象而是借助方法完成操作

方法声明

在函数声明时,在函数名前加上一个变量,即方法的声明。此变量称之为receiver(接收器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Point struct {
X, Y float64
}

// 函数声明
func Distance(p, q Point) float64 {
return math.Hypot(q.X - p.X, q.Y - p.Y)
}

// 方法声明
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X - p.X, q.Y - p.Y)
}

p := Point{1, 2}
q := Point{4, 6}
Distance(p, q) // 函数调用
p.Distance(q) // 方法调用

方法和struct成名变量都在同一个命名空间中,所以方法名和成员名不能相同,若在声明一个X方法,则会和成员变量X冲突

Go中除了底层类型为pointerinterface的命名类型外,其余的所有命名类型都可定义方法(数值/字符串/slice/map……)

指针对象的方法

1
2
3
4
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}

指针对象的方法,即方法的receiver(接收器)为指针对象。上面方法的方法名为(*Point).ScaleBy

只有类型(T)指向类型的指针(*T)才能作为receiver(接收器),若类型名本身是指针无法作为receiver(接收器)。正如上面所讲,底层类型为指针的命名类型不可定义方法

1
2
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type

如何调用指针对象的方法

1
2
3
4
5
6
7
8
9
10
11
12
type Point struct {
X, Y float64
}

func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}

r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r) // "{2, 4}"

不管方法的receiver是T还是*T类型,都可通过T*T类型对象调用,编译器会自动识别。但个人最好明白是编译器帮忙做了转换

1
2
3
4
5
6
7
8
9
10
11
12
type Student struct{}

func (stu *Student) Speak() {
fmt.Println("hello")
}

func main() {
s := &Student{} // *T
ss := Student{} // T // 虽然T编译器会自动转换成*T,但需明白&Student{}才是最正确的用法
s.Speak()
ss.Speak()
}

嵌套扩展方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Point struct {
X, Y float64
}

type ColoredPoint struct {
Point
Color color.RGBA
}

func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}

var cp ColoredPoint
red := color.RGBA{255, 0, 0, 255}
cp = ColoredPoint{Color: red}
cp.X = 1 // struct匿名嵌套,struct章节内容
cp.Y = 2
fmt.Println(cp)
cp.ScaleBy(2) // ColoredPoint类型对象可执行调用Point类型对象的ScaleBy方法
fmt.Println(cp.X, cp.Y)

Point类的方法也被引入了ColoredPoint,但一个ColoredPoint并不是一个Point,而是ColoredPoint has Point

方法值和方法表达式

方法值和函数值类似,一般使用方法常规为object.method()object.method称为选择器(没有()),选择器返回一个方法值,即将方法绑定到特定接收器变量的函数,直接通过该函数接收参数调用方法

1
2
3
4
var cp ColoredPoint
cp = ColoredPoint{Point{1, 2}, red}
methodValue = cp.ScaleBy // 方法值
fmt.Println(methodValue(2)) // 通过方法值调用方法
1
2
3
4
5
6
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
// time.AfterFunc(timeout, function) 需要接收函数
time.AfterFunc(10 * time.Second, func() { r.Launch() }) // 因为参数需要是函数,所以要构造出一个匿名函数
time.AfterFunc(10 * time.Second, r.Launch) // 使用方法值即可省略匿名函数

方法表达式

1
2
3
4
5
p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance // 方法表达式,不需要通过对象实例,但在真正调用方法时需要传入receiver
fmt.Println(distance(p, q)) // "5" // 需要传入p对象实例作为receiver

Interface

接口声明

接口是方法的集合,只要实现了接口内所有的方法就实现了接口,即当一个具体类型实现了接口内要求的所有方法,则该类型就是该接口的实例

接口支持内嵌

1
2
3
4
5
6
7
8
9
10
11
12
13
type Reader interface {		// 接口声明
Read(p []byte) (n int, err error) // 方法
}

type Closer interface {
Close() error
}

type ReadWriteCloser interface {
Reader // 接口内嵌
Writer
Closer
}

实现接口的条件

指针对象方法接口实现

对于指针对象的方法,需要特别注意一点:指针类型的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
2
3
4
5
6
7
8
9
10
11
12
13
// 无论是`T`还是`*T`类型都可调用`T`和`*T`方法
type Student struct{}

func (stu *Student) Speak() {
fmt.Println("hello")
}

func main() {
s := &Student{} // *T
ss := Student{} // T // 虽然T编译器会自动转换成*T,但需明白&Student{}才是最正确的用法
s.Speak()
ss.Speak()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type People interface {
Speak()
}

type Student struct{}

func (stu *Student) Speak() {
fmt.Println("hello")
}

func main() {
var peo People = Student{} // WRONG // T类型Student{}并没有实现(stu *Student) Spea()方法,故也无实现People接口
// var peo People = &Student{} // RIGHT
peo.Speak()
}

接口断言

由于接口的实现采用隐式实现,即只要实现了接口内所有方法,就实现了接口。当要判断一个类型是否实现接口时,可通过如下方法:

1
var _ io.Writer = (*bytes.Buffer)(nil)	// nil转换为*bytes.Buffer类型,并尝试赋值操作(空标识符,类型为io.Writer)。无编译报错,则*bytes.Buffer实现了io.Writer接口

接口值

接口本质上是由2部分组成:

  1. 指向值类型的指针
  2. 指向值内容的指针
1
2
3
4
type InterfaceStruct struct {
pointType uintptr // 指向值类型的指针
pointValue uintptr // 指向值内容的指针
}

接口值是可进行比较的,但当接口的pointType类型是不可比较类型(slice/map)则无法进行比较操作。这里尤其注意接口和nil的比较

判断接口是否等于nil,需要接口的pointTypepointValue都为nil时才等于nil

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
type People interface {
Show()
}

type Student struct{}

func (stu *Student) Show() {}

func live() People { // BBBBBBB
var stu *Student
return stu
}

func main() {
if live() == nil {
fmt.Println("nil")
} else {
fmt.Println("not nil")
}
// not nil
/*
虽然live()返回People的value为nil,但People的type为*Student,不为nil。所以People接口类型变量不等于nil
只有当type和value都为nil时,接口才等于nil
*/

}

类型断言

类型断言语法:x.(T) (x必须为接口类型且不为nil)

类型断言主要分2种情况:

  • T为具体类型,x.(T)检查x的类型是否和T类型相同

    判断成功后会返回x的pointValue

    1
    2
    3
    4
    w := 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
2
3
4
5
6
7
8
9
10
11
12
i := 1

switch interface{}(i).(type) { //由于x.(T)中x必须为接口类型,所以需将i进行类型转换interface{}(i)
case int:
println("int")
case string:
println("string")
case interface{}:
println("interface")
default:
println("unknown")
}

Goroutines & Channels

Goroutines

Go语言中,每一个并发的执行单元叫作一个goroutine。程序启动时,main()函数即在一个goroutine中运行,此gouroutine称之为main goroutine

创建一个gouroutine

1
go function()
1
2
3
4
5
6
7
8
9
10
11
func main() {
fmt.Println("main start")
go func() { // 创建一个goroutine执行匿名函数
fmt.Println("groutine")
}() // 此处必须要有(),go 语句紧跟的是一个函数调用
fmt.Println("main end")
}
/*
main start
main end
*/

上面代码在main()函数中创建了一个goroutine来执行一个匿名函数,但输出的结果仅有main startmain end并没有新建goroutine的输出。因为新建完goroutine后,main()函数继续往下执行,执行完后main goroutine直接就结束了,由于main goroutine结束其所创建的goroutine也会被杀掉,导致新建的goroutine还没来得及输出就已经被结束了。改进方案就是需要用到channelsync等来解决

  • channel方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    func 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
    22
    func 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
    12
    func 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
    17
    func 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func main() {
naturals := make(chan int)
squares := make(chan int)

// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x // 向naturals channel发送100个整数
}
close(naturals)
}()

// Squarer
go func() {
for x := range naturals { // 不断从naturals channel中接收整数
squares <- x * x // 计算后向squares channel发送数据
}
close(squares)
}()

// Printer (in main goroutine)
for x := range squares { // 不断从squares channel中接收数据
fmt.Println(x)
}
}

单方向channel

默认情况下channel都是双向的,但可创建只能发送或只能接收的单方向channel

1
2
3
ch := make(chan int)
var send chan<- int = c // 只能发送channel
var recv <-chan int = c // 只能接收channel
1
2
3
4
5
6
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}

select多路复用

Go语言中select...case用于同时监听多个channel,case语句必须是一个面向channel的操作

1
2
3
4
5
6
7
8
9
select {

case <-chan1: // 如果chan1成功读到数据,则进行该case处理语句

case chan2 <- 1: // 如果成功向chan2写入数据,则进行该case处理语句

default: // 如果上面都没有成功,则进入default处理流程

}

select…case执行流程:

  1. select中只要有一个case能执行成功,则立刻执行
  2. 如果同一时间有多个case均能执行成功则伪随机方式抽取任意一个case执行
  3. 如果所有case都不能执行成功,则执行default。如果没有default,则一直阻塞,直到有一个case能执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
runtime.GOMAXPROCS(1) // 设置单核CPU执行

int_chan := make(chan int, 1) // 新建channel,必须是有缓存channel
string_chan := make(chan string, 1)

int_chan <- 1
string_chan <- "hello"

select {
case value := <-int_chan:
fmt.Println(value)
case value := <-string_chan:
panic(value)
}
}
// 有可能panic。因为int_chan和string_chan都可以成功执行,select会随机选取其中一个case执行,无法确定选择哪个case

基于共享变量的并发

sync.Mutex互斥锁

sync.Mutex包中提供了Lock()Unlock()方法,当需要访问共享变量时,通过调用Lock()/Unlock()方法获取互斥锁。如果互斥锁被其他goroutine调用Lock()先获取,那么访问该共享变量的goroutine便会阻塞,阻塞一直到其他goroutine调用Unlock()释放互斥锁。

Lock()Unlock()之间的代码段,称之为临界区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var (
mu sync.Mutex // guards balance
balance int
)

func Deposit(amount int) {
mu.Lock()
balance = balance + amount
mu.Unlock()
}

func Balance() int {
mu.Lock()
b := balance
mu.Unlock()
return b
}

sync.RWMutex读写锁

sync.RWMutex包提供多读单写锁,读锁:sync.RWMutex.Rlock()/sync.RWMutex.RUnlock();写锁:sync.RWMutex.Lock()/sync.RWMutex.Unlock()

Rlock()只能在临界区共享变量没有任何写入操作时才可用。

1
2
3
4
5
6
7
var mu sync.RWMutex
var balance int
func Balance() int {
mu.RLock() // readers lock
defer mu.RUnlock()
return balance
}

sync.Once初始化

sync.Once中的Do()方法能在并发环境下,确保函数只被执行一次,sync.Once.Do(f)。常用于做初始化操作

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
var a string

var once sync.Once
var c = make(chan int)

func setup() {
fmt.Println("in setup")
a = "hello world" // 初始化变量a
}

func doprint() {
once.Do(setup) //无此行则会打印两次"in setup"
fmt.Println(a)
<-c
}

func main() {
// 两个goroutine,虽然doprint()函数中有调用setup(),但由于使用了once.Do(),所以两个goroutine也只会执行一次setup(),既只输出一次"in setup"
go doprint()
go doprint()
c <- 1
c <- 2

defer close(c)
}

竞争条件检测

Go提供竞争条件检测分析工具,只要在go buildgo run或者go test命令后面加上-race即可

1
2
3
go test -race mypkg    // test the package
go run -race mysrc.go // compile and run the program
go build -race mycmd // build the command

Goroutines和线程

os线程使用固定大小的栈(一般2MB);goroutine的栈大小不是固定的,可动态伸缩最大为1GB

os线程被操作系统内核调度,进行上下文切换;goroutine由Go调度器调度,Go调度器调度不需要进行上下文切换,调度代价小

Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数

进程有进程id(pid),线程有线程id(tid),goroutine没有id

Powered: Hexo, Theme: Nadya remastered from NadyMain