Go学习之路

零、写在前面

Go 语言最主要的特性:

  • 自动垃圾回收
  • 更丰富的内置类型
  • 函数多返回值
  • 错误处理
  • 匿名函数和闭包
  • 类型和接口
  • 并发编程
  • 反射
  • 语言交互性

由于Go和C/C++在最基本的语言方面还是由很多类似的地方的,所以我在学习的时候,对前面的部分仅仅是简单略过。主要抓住该语言最重要的特点,如切片、方法、接口、goroutine、通道等进行深入学习

一、基础语法篇

注意,与C不同的是:

花括号{不能单独用于一行

行分隔符

在Go语言中,不需要像;进行语句划分的。

1
2
fmt.Println("Hello, World!")
fmt.Println("Hello, Wuhlan")

当然,如果硬是要写在同一行,可以使用;进行划分,但不推荐

1
fmt.Println("Hello, World!");fmt.Println("Hello, Wuhlan")

变量声明方式

方法一:指定类型

1
var identifier type

如:

1
2
var num int
var str string

如果没有初始化,则设为零值

1
2
3
var num int            // 0
var str string // ""
var b bool // false

以下的值,都设为nil,类似于C/C++中的NULL

1
2
3
4
5
6
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口

方法二:自动判断类型

1
var num = 1

方法三:省略var

使用:=,这是一个声明语句(左侧如果没有声明新的变量,就产生编译错误)

1
2
3
var num int = 1
//相等于
num := 1

格式化输出字符串

  • 使用Printf

    1
    fmt.Printf("%d,%s",666,"Wuhlan")
  • 使用SprintfPrintln

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        var str = "%d,%s"
    var num int = 666
    var name string = "Wuhlan"
    str = fmt.Sprintf(str,num,name)
    fmt.Println(str)
    /******************************************/
    //也可以这样写
    var num int = 666
    var name string = "Wuhlan"
    str := fmt.Sprintf("%d,%s", num, name)
    fmt.Println(str)

数据类型

类型和描述
1 布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。
2 数字类型 整型 int 和浮点型 float32、float64等,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
3 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
4 派生类型: 包括:(a) 指针类型(b) 数组类型(c) 结构化类型(d) Channel 类型(e) 函数类型(f) 切片类型(g) 接口类型(h) Map 类型

Go是一个强类型语言,混合n使用类型是不允许的。比如:

1
2
3
4
5
6
7
8
package main

func main() {
var defaultName = "Sam" // 允许
type myString string //自定义一个myString类型
var customName myString = "Sam" // 允许
customName = defaultName // 不允许
}

并行赋值

这是一个很有意思的GO语言的特点。

如果想交换两个变量的值,我相信大部分人第一时间想到的是使用一个临时变量temp作为中介绍。但是go语言并不需要,你可以猜猜下面这段代码会发生什么?

1
2
3
4
var a int = 1
var b int = 2
a,b = b,a
fmt.Println(a,b)

没错,a和b竟然可以直接交换!!!它是通过并行的方式交换的,这种方式还常用于函数的返回值中。

另外,根据GO语言的特点,我们必须使用到所有声明的函数,所以,如果你不想使用某个变量,可以用_来替代

1
2
var _,b = 2,3
fmt.Println(b)

分支语句

if-else注意的点如下:

  • 分支判断条件可以不使用(),当然,也可以使用
  • 花括号必须与ifelse在同一行
  • 无论有几条条件语句,左右 {}花括号不能省略

GO语言没有三目运算符? :

switch-case注意的点如下:

  • 支持多条件匹配

    1
    2
    3
    4
    switch{
    case 1,2,3,4:
    default:
    }
  • 不同的 case 之间不使用 break 分隔,默认只会执行一个 case。

  • 如果想要执行多个 case,需要使用 fallthrough 关键字,也可用 break 终止。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    switch{
    case 1:
    ...
    if(...){
    break
    }

    fallthrough // 此时switch(1)会执行case1和case2,但是如果满足if条件,则只执行case 1

    case 2:
    ...
    case 3:
    }
  • 没有switch的switch,默认switch(true),这种形式能将一长串if-then-else写得更加清晰。

循环语句

一般情况注意的点如下:

  • 初始化语句和后置语句是可选的。比如:for ;i<100;{}
  • Go中没有while,将for中的;去掉,便相当于while
  • 无限循环还可以直接省略掉条件,写成for{}

for-each range可以对数组和切片进行迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"

func main() {
strings := []string{"Wuhlan", "miaomiaomiao"}
for i, s := range strings {
fmt.Println(i, s)
}
numbers := [6]int{1, 2, 3, 5}
for i,x:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}
}

输出如下:

1
2
3
4
5
6
7
8
0 Wuhlan
1 miaomiaomiao
0 位 x 的值 = 1
1 位 x 的值 = 2
2 位 x 的值 = 3
3 位 x 的值 = 5
4 位 x 的值 = 0
5 位 x 的值 = 0

defer语句

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

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

import "fmt"

func main() {
fmt.Println("counting")

for i := 0; i < 10; i++ {
defer fmt.Println(i)
}

fmt.Println("done")
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
counting
done
9
8
7
6
5
4
3
2
1
0

函数

函数定义方式:

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

基本的函数

一个简单的加法函数:

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

func add(a,b int) int {
return a + b
}

func main() {
var a = 6
var b = 7
fmt.Println(add(a,b))
}

多返回值函数

返回多个值的函数(随便写的,写的很爽嘿嘿😋

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"

func add(a,b int) (int,int) {
a,b = b+b,a+a
return a , b
}

func main() {
var a = 6
var b = 7
fmt.Println(add(a,b))
}

输出:

1
14 12

有的时候我们可能并不需要多个返回值中的某几个值,可以使用空白符

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

import "fmt"

func add(a, b int) (int, int) {
a, b = b+b, a+a
return a, b
}

func main() {
var a = 6
var b = 7
c, _ := add(a, b) //使用空白符来接收不想要的值
fmt.Println(c)
}

命名返回值

从函数中可以返回一个命名值。一旦命名了返回值,可以认为这些值在函数第一行就被声明为变量了。

我们稍微对上面的例子进行修改:

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

import "fmt"

func add(a, b int) (ret1, ret2 int) {
ret1, ret2 = b+b, a+a
return
}

func main() {
var a = 6
var b = 7
fmt.Println(add(a, b))
}

二、进阶语法篇

其实是基础的太多了,用这个来划分一下。下面的还是很基础喔!

基本编译与运行

1
2
3
4
5
6
7
8
// geometry.go
package main //指定该文件属于main包

import "fmt" //导入一个已存在的包

func main() {
fmt.Println("Geometrical shape properties")
}

在执行go install geometry之后,会在bin文件夹里生成一个geometry.exe

结构如下:

1
2
3
4
5
src
geometry文件夹
gemometry.go
bin
geometry.exe

调用自定义包

文件夹结构如下:

1
2
3
4
5
src
geometry
geometry.go
rectangle
rectprops.go

rectprops.go如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// rectprops.go
package rectangle

import "math"

func Area(len, wid float64) float64 {
area := len * wid
return area
}

func Diagonal(len, wid float64) float64 {
diagonal := math.Sqrt((len * len) + (wid * wid))
return diagonal
}

可以看到,rectangle包里的函数都是首字母大写的。在Go中,任何以大写字母开头的变量或者函数都是被导出的变量名/函数名。只有这些名字才可以在main包中进行访问。

init函数

每一个包里都可以包含一个init函数。是在一个包初始化的时候自动运行的。

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
// geometry.go
package main

import (
"fmt"
"geometry/rectangle" // 导入自定义包
"log"
)

/*
* 1. 包级别变量
*/
var rectLen, rectWidth float64 = 6, 7

/*
*2. init 函数会检查长和宽是否大于0
*/
func init() {
println("main package initialized")
if rectLen < 0 {
log.Fatal("length is less than zero")
}
if rectWidth < 0 {
log.Fatal("width is less than zero")
}
}

func main() {
fmt.Println("Geometrical shape properties")
fmt.Printf("area of rectangle %.2f\n", rectangle.Area(rectLen, rectWidth))
fmt.Printf("diagonal of the rectangle %.2f ", rectangle.Diagonal(rectLen, rectWidth))
}

如果将rectLen将改为-6。则会有日志报错:

1
2
3
4
PS E:\go\src> go run geometry    
main package initialized
2021/10/09 14:28:40 length is less than zero
exit status 1

导入包而不使用

一般情况下,导入某个包而不使用它,将会报错。有两种方法可以先导入而暂不使用:

1.使用错误屏蔽器

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

import (
"geometry/rectangle"
)

var _ = rectangle.Area // 错误屏蔽器

func main() {

}

2.使用空白标识符

1
2
3
4
5
6
7
8
package main 

import (
_ "geometry/rectangle"
)
func main() {

}

数组

初始化

1
var arr = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

快速初始化数组:

1
arr := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

长度不确定时:

1
2
3
var arr = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

arr := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

使用下标来初始化元素:

1
balance := [5]float32{1:2.0,3:7.0}

指针

与C类似,需要注意的有两点:

  • * int,这是反过来的!!!
  • 空指针,nil
  • Go并不支持指针运算。比如说如果p是一个指针,p++是不被允许的。

切片

Go 语言切片是对数组的抽象。切片本身不拥有任何数据。它们只是对现有数组的引用。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片的定义

  • 使用未指定大小的数组来定义:var 标识符 []类型

  • 使用make()函数来创建切片:

    1
    2
    3
    var slice1 []int = make([]int, len)

    slice1 := make([]int, len)
  • 也可以指定容量make([]int, len, capacity)

切片初始化

1
2
3
4
5
6
7
s := int {1,2,3}                //直接初始化切片,[]是切片类型
s := arr[:] //对arr的引用
s := arr[startIndex:endIndex] //将 arr 中从下标 startIndex 到 endIndex-1 创建一个新的切片。
s := arr[startIndex:]
s := arr[:endIndex]
s1 := s[startIndex:endIndex] //切片初始化为s1
s :=make([]int,len,cap)

切片的长度和容量

len()cap()可以获取切片的长度和容量

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

func main() {
var arr = []int{0,1,2,3,4,5,6,7}
slice1 := arr[1:4]
fmt.Println(slice1)
fmt.Println(len(slice1))
fmt.Println(cap(slice1))

var slice2 = make([]int, 4, 7)
slice2 = arr[2:6]
fmt.Println(slice2)
fmt.Println(len(slice2))
fmt.Println(cap(slice2))
}

输出:

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

可以观察一下,cap到底是怎么计算的

切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。

切片的扩充和拷贝

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

import "fmt"

func main() {
var numbers []int
printSlice(numbers)

// 向切片添加一个元素
numbers = append(numbers, 0)
printSlice(numbers)

// 同时添加多个元素
numbers = append(numbers, 1,2,3,4)
printSlice(numbers)

//创建切片 numbers1 是之前切片的两倍容量
numbers1 := make([]int, len(numbers), (cap(numbers))*2)

// 拷贝 numbers 的内容到 numbers1
copy(numbers1,numbers)
printSlice(numbers1)
}

func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

输出:

1
2
3
4
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

切片与数组的区别

切片文法类似于没有长度的数组文法。

这是一个数组文法:

1
[3]bool{true, true, false}

下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:

1
[]bool{true, true, false}
  • 切片并不存储任何数据,它只是描述了底层数组中的一段。
  • 更改切片的元素会修改其底层数组中对应的元素。
  • 与它共享底层数组的切片都会观测到这些修改。

观察下面的程序,猜猜会发生什么?

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

import (
"fmt"
)

func change(s ...string) {
s[0] = "Go"
s = append(s, "playground")
fmt.Println(s)
}

func main() {
welcome := []string{"hello", "world"}
change(welcome...)
fmt.Println(welcome)
}

内存优化

切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中

一种解决方法是使用 copy 函数 func copy(dst,src[]T)int 来生成一个切片的副本。这样我们可以使用新的切片,原始数组可以被垃圾回收

Map

Map的创建与初始化

1
2
3
4
5
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对。

所以必须进行初始化

使用range迭代

使用键进行遍历即可

查看是否Key存在

可以使用key,ok := mymap[value]

若key存在,则ok == true;否则ok == false

范围Range

range 可用于迭代数组、切片、通道、集合

delete()函数

1
delete(mymap, Key)

参考例程

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

import "fmt"

func main() {
//创建并初始化
countryCapitalMap := make(map[string]string)

countryCapitalMap [ "France" ] = "巴黎"
countryCapitalMap [ "Italy" ] = "罗马"
countryCapitalMap [ "Japan" ] = "东京"
countryCapitalMap [ "India " ] = "新德里"
//使用range进行遍历
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}
//可以判断key是否存在
capital, ok := countryCapitalMap [ "American" ]
if (ok) {
fmt.Println("American 的首都是", capital)
} else {
fmt.Println("American 的首都不存在")
}
//删除某个map
delete(countryCapitalMap, "France")
fmt.Println()
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}
}

字符串

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 printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}

func printChars(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%c ",s[i])
}
}

func main() {
name = "Señor"
printBytes(name)
fmt.Printf("\n")
printChars(name)
}

运行结果如下:

1
2
53 65 c3 b1 6f 72
S e à ± o r

可以看到,出现了一个小bug。这是因为ñ在UTF-8中是占用了两个字节的。那么我们该如何获取每一个字符呢?

rune

rune 是 Go 语言的内建类型,它也是 int32 的别称。在 Go 语言中,rune 表示一个代码点。代码点无论占用多少个字节,都可以用一个 rune 来表示。

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"
)

func printBytes(s string) {
for i:= 0; i < len(s); i++ {
fmt.Printf("%x ", s[i])
}
}

func printChars(s string) {
runes := []rune(s)
for i:= 0; i < len(runes); i++ {
fmt.Printf("%c ",runes[i])
}
}

func main() {
name := "Señor"
printBytes(name)
fmt.Printf("\n")
printChars(name)
}

更简单的遍历字符串方法是使用for range

1
2
3
4
5
func printCharsAndBytes(s string) {
for index, rune := range s {
fmt.Printf("%c starts at byte %d\n", rune, index)
}
}

字符串是不可修改的

要想修改,需要先将字符串转换为一个rune切片。然后这个切片可以进行任何想要的改变,然后再转化为一个字符串。

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

import (
"fmt"
)

func mutate(s []rune) string {
s[0] = 'a'
return string(s)
}
func main() {
h := "hello"
fmt.Println(mutate([]rune(h)))
}

方法

Go 没有类。不过你可以为结构体类型定义方法。(当然也可以为非结构体类型定义方法)

方法就是一类带特殊的 接收者 参数的函数。

方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。

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

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}

值接收器和指针接收器

经过测试,我总结了他们的特性:

  • 无论是值接收器还是指针接收器,既可以使用指向结构体的指针来调用,也可以直接使用结构体来调用。(Go语言或许是想减少我们的错误,两种方法都可以)
  • 而最终的效果——方法内部的改变对于调用者是否可见,取决于方法是值接收器指针接收器,而和调用者没有关系。
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"
)

type Employee struct {
name string
age int
}

/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
e.name = newName
}

/*
使用指针接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}

func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)

fmt.Printf("\n\nEmployee age before change: %d", e.age)
e.changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}

输出:

1
2
3
4
5
Employee name before change: Mark Andrew
Employee name after change: Mark Andrew

Employee age before change: 50
Employee age after change: 51

接口

接口主要是用于对不同的结构体,赋予同一个名称的函数(该函数针对不同结构体可以有不同的操作)

通过下面这个程序,我们可以看看到调用Test接口的类型为main.MyFloat

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

import (
"fmt"
)

type Test interface {
Tester()
}

type MyFloat float64

func (m MyFloat) Tester() {
fmt.Println(m)
}

func describe(t Test) {
fmt.Printf("Interface type %T value %v\n", t, t)
}

func main() {
var t Test
f := MyFloat(89.7)
t = f
describe(t)
t.Tester()
}

输出:

1
2
Interface type main.MyFloat value 89.7  
89.7

空接口

没有包含方法的接口称为空接口。空接口表示为 interface{}。由于空接口没有方法,因此所有类型都实现了空接口。

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 describe(i interface{}) {
fmt.Printf("Type = %T, value = %v\n", i, i)
}

func main() {
s := "Hello World"
describe(s)
i := 55
describe(i)
strt := struct {
name string
}{
name: "Naveen R",
}
describe(strt)
}

输出:

1
2
3
Type = string, value = Hello World  
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}

三、并行相关

Goroutine

goroutine是由Go运行时管理的轻量级线程。

  • Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。
  • 使用信道(channel)来进行通信

当我们直接运行以下代码的时候,会发现和我们想象的不一样,程序只输出了"main",这是为什么捏?这是因为go hello()启动了一个新的协程,然后main()运行在一个特殊的协程上,称为 Go 主协程Main Goroutine

如果 Go 主协程终止,则程序终止,于是其他 Go 协程也不会继续运行。

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
)
func hello(){
fmt.Println("Hello")
}

func main() {
go hello()
fmt.Println("main")
}

解决方法,可以是使主协程延迟一段时间,等其他协程运行结束后,再终止主协程。

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

import (
"fmt"
"time"
)
func hello(){
fmt.Println("Hello")
}

func main() {
go hello()
fmt.Println("main")
time.Sleep(1 * time.Second)
}

使用多个goroutine同样可以使用延迟的方式来控制运行的顺序

信道channel

什么是信道

信道可以想象成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。

信道的定义

1
2
3
4
 var a chan int            //定义一个int类型的信道
a = make(chan int) //分配内存
//或
a := make(chan int)

信道的发送与接收

1
2
data := <- a // 读取信道 a  
a <- data // 写入信道 a

信道的发送与接收默认是阻塞

即:写入信道时,若没有读取,就会阻塞; 读取信道时,若没有写入,就会阻塞

我们可以将这个特性应用到前面的代码中,不再需要时间延迟

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

import "fmt"
func hello(done chan bool){
fmt.Println("Hello")
done <- true //写入信道,说明该go协程已经运行完毕
}

func main() {
done := make(chan bool) //定义一个bool类型的信道
go hello(done)
<- done //当信道尚未被写入时,该读取信道的语句将会被阻塞
fmt.Println("main")
}

死锁

需要特别注意的是,如果只写入信道或者只读取信道,就会造成死锁,程序会触发panic并报错。

如:

1
2
3
4
5
6
package main

func main() {
ch := make(chan int)
ch <- 5
}

输出:

1
2
3
4
5
6
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
E:/go/src/main.go:5 +0x57
exit status 2

单向信道

  • 只收信道(Receive Only) chan<- int
  • 只送信道(Send Only) <-chan int

那么为什么需要单向信道呢?如果一个通道只收不送或者只送不收,哪还有什么意义!事实上,双向通道可以转换为单向信道,所以可以正常使用(个人认为,大概是用于协程传参的时候,单向信道可以限制某个协程对通道的操作,减少出错。)

上面的程序可以简单修改为:

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

import "fmt"
func hello(done chan<- bool){ //这是单向的 只送信道
fmt.Println("Hello")
done <- true //写入信道,说明该go协程已经运行完毕
}

func main() {
done := make(chan bool) //定义一个bool类型的信道
go hello(done)
<- done //当信道尚未被写入时,该读取信道的语句将会被阻塞
fmt.Println("main")
}

双向信道可以转换为单向信道,但是反过来不可以

关闭信道

使用多一个变量来判断信道是否已经关闭

1
v, ok := <- ch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for {
v, ok := <-ch
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
}

使用for range遍历

使用for range,会自动判断信道是否已经关闭,若已经关闭,则会自动结束循环

上述代码可以修改为:

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

import "fmt"
func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("Received ",v)
}
}

缓冲信道和工作池

缓冲信道特点

  • 只有在缓冲已满的情况,才会阻塞向缓冲信道发送数据。
  • 只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。

创建缓冲信道

1
ch := make(chan type, capacity)

如:

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

import (
"fmt"
)


func main() {
ch := make(chan string, 2)
ch <- "naveen"
ch <- "paul"
fmt.Println(<- ch)
fmt.Println(<- ch)
}

waitGroup来实现工作池

WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕。

waitGroup使用的基本流程

  • 首先需要调用sync
  • 声明一个waitGroup:var wg sync.WaitGroup
  • 每运行一个子协程,传递int给waitGroup,使其计数器增加:wg.Add(1)
  • waitGroup的地址来传递函数:go process(i, &wg)
  • 每一个子协程运行结束后,计数器自减:wg.Done()
  • 当所有子协程运行结束后,才能继续运行:wg.Wait()

工作池的作用是什么呢?

在golang编写服务程序过程中,如果每次都启动一个goroutine去处理任务,处理一个任务后就退出,这样势必会造成资源浪费。构建一个工作goroutine池来处理任务相对资源利用会少些,具体的情况需对比测试。

示例,使用工作池来计算,每个随机数每一位加起来的和:

这里工作池的作用是:

  • 创建一个 Go 协程池,监听一个等待作业分配的输入型缓冲信道。
  • 将作业添加到该输入型缓冲信道中。
  • 作业完成后,再将结果写入一个输出型缓冲信道。
  • 从输出型缓冲信道读取并打印结果。
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package main

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

type Job struct {
id int
randomno int
}
type Result struct {
job Job
sumofdigits int
}

var jobs = make(chan Job, 10)
var results = make(chan Result, 10)
//对一个数字的每位数求和
func digits(number int) int {
sum := 0
no := number
for no != 0 {
digit := no % 10
sum += digit
no /= 10
}
time.Sleep(2 * time.Second)
return sum
}
//调用digits函数,将结构体输入到results信道中,并且waitGroup的计数器减一
func worker(wg *sync.WaitGroup) {
for job := range jobs {
output := Result{job, digits(job.randomno)}
results <- output
}
wg.Done()
}
//创建工作池,调用worker函数(进行计算并输出),当所有go程都运行结束后才停止
func createWorkerPool(noOfWorkers int) {
var wg sync.WaitGroup
for i := 0; i < noOfWorkers; i++ {
wg.Add(1)
go worker(&wg)
}
wg.Wait()
close(results)
}
//初始化,生成随机数并且输入到jobs信道中
func allocate(noOfJobs int) {
for i := 0; i < noOfJobs; i++ {
randomno := rand.Intn(999)
job := Job{i, randomno}
jobs <- job
}
close(jobs)//关闭信道,告诉接收方不再有数据发来
}
//输出结果,当全部输出完成后,done信道才会接收true信息,主go程<-done才能运行。这是用来避免过早地结束主程序。
func result(done chan bool) {
for result := range results {
fmt.Printf("Job id %d, input random no %d , sum of digits %d\n", result.job.id, result.job.randomno, result.sumofdigits)
}
done <- true
}
func main() {
startTime := time.Now()
noOfJobs := 20
go allocate(noOfJobs)
done := make(chan bool)
go result(done)
noOfWorkers := 10
createWorkerPool(noOfWorkers)
//go result(done)也可以放在这里,没有影响
<-done
endTime := time.Now()
diff := endTime.Sub(startTime)
fmt.Println("total time taken ", diff.Seconds(), "seconds")
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Job id 3, input random no 983 , sum of digits 20
Job id 1, input random no 636 , sum of digits 15
Job id 7, input random no 998 , sum of digits 26
Job id 0, input random no 878 , sum of digits 23
Job id 5, input random no 735 , sum of digits 15
Job id 2, input random no 407 , sum of digits 11
Job id 9, input random no 150 , sum of digits 6
Job id 8, input random no 904 , sum of digits 13
Job id 6, input random no 520 , sum of digits 7
Job id 4, input random no 895 , sum of digits 22
Job id 19, input random no 914 , sum of digits 14
Job id 10, input random no 212 , sum of digits 5
Job id 17, input random no 506 , sum of digits 11
Job id 11, input random no 538 , sum of digits 16
Job id 12, input random no 750 , sum of digits 12
Job id 16, input random no 630 , sum of digits 9
Job id 15, input random no 215 , sum of digits 8
Job id 18, input random no 20 , sum of digits 2
Job id 13, input random no 362 , sum of digits 11
Job id 14, input random no 436 , sum of digits 13
total time taken 4.0083858 seconds

Select

直接来看一下参考程序

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

import (
"fmt"
"time"
)

func server1( str1 chan string){
time.Sleep(3*time.Second)
str1 <- "message from server1"
}
func server2( str2 chan string){
time.Sleep(1*time.Second)
str2 <- "message from server2"
}
func main(){
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select {
case s1 := <- output1:
fmt.Println(s1)
case s2 := <- output2:
fmt.Println(s2)
}
}

输出:

1
message from server2

主程序当运行到select就会一直阻塞,直到出现符合要求的case出现。由于server1延迟3秒,server2延迟1秒,所以select会先接收到信道output2,进行输出后,程序结束。(不会管server1啦)

这有什么用呢?假设我们现在向两台服务器发送申请,并等待它们的回复。为了节省时间,当我们接收到最快的回复时,就继续接下来的步骤。这可以使反应达到最快的速度。

类似于switch,使用default关键字来表示默认的情况。可用来避免死锁的出现。

!!!当信道仅含有nil时,也会触发dafault!!!

随机选取

当多个case被同时选取时,会随机选择一个去执行。

下面这个程序使用了延迟一秒来同步两个go协程。

输出结果是from server1from server2两种都有可能。

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

import (
"fmt"
"time"
)

func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"

}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}

Mutex

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

import (
"fmt"
"sync"
)
var x = 0
func increment(wg *sync.WaitGroup){
x++
wg.Done()
}
func main(){
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println(x)
}

输出:

1
982/984/992

使用mutex解决临界区问题

外部定义互斥锁var m sync.Mutex

1
2
3
m.Lock()
x++
m.Unlock()

输出:

1
1000

使用信道解决临界区问题

1
2
3
ch <- true
x++
<- ch

输出:

1
1000

关于互斥锁和信道的选择问题。

当 Go 协程需要与其他协程通信时,可以使用信道。而当只允许一个协程访问临界区时,可以使用 Mutex。

参考资料

https://www.runoob.com/go

https://tour.go-zh.org/

https://studygolang.com/subject/2


Go学习之路
https://wuhlan3.gitee.io/2021/10/08/Go学习之路/
Author
Wuhlan3
Posted on
October 8, 2021
Licensed under