Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

浙江大学期末考试——Go语言

2021/12/30 Go 考试
Word count: 16,820 | Reading time: 70min

浙江大学期末考试——Go语言

期末复习对学习知识的帮助是巨大的,相比于看视频,本篇点到为止的知识点形的期末复习笔记,我相信更能让人学会。我相信,只要把本笔记全部看完了必然就会Go了。如果你做到了但还没有学会Go,那你大可来找我/doge。

概论

  • 指令:计算机的一个最基本的功能,如实现一次加法运算或实现一次大小的判别
  • 计算机的指令系统:计算机所能实现的指令的集合
  • 程序:一系列计算机指令的有序组合

算法:

求解特定问题的一组有限的操作序列

  • 目的性:算法有运算结果,程序强调过程性
  • 抽象性:算法独立于编程语言和指令系统
  • 研究性:算法是理论研究,载体可以是伪码,文字,图片等,载体为某一编程语言时就是程序

基本特征:

  • 有限性:一个算法在执行有限步之后必须会终止。
  • 确定性:一个算法的每个步骤都必须精确地定义,可以严格地、无歧义地执行。
  • 输入:一个算法在运行之前赋给它的量,或在运行过程中动态地赋给它的量。
  • 输出:一个算法运行结束时的结果。
  • 有效性:一个算法在运行过程中,所有运算必须是充分基本的,是可行的,原则上人们可以用笔和纸在有限的时间内精确地完成这些运算。

结构化程序设计

  • 程序=算法+数据结构 ----获得图灵奖的Pascal之父Nicklaus Wirth
  • 将复杂程序划分为若干个相互独立的模块
  • 模块:一条语句(Statement)、一段程序或一个函数(子程序)等

结构化程序设计特点

  • 自顶向下
  • 模块化设计
  • 结构化编码

OOP

  • 封装和数据隐藏
  • 继承和重用
  • 多态性

GO 特性

静态编译型
语法强调少即是多
强调组合,更简洁的OOP
Duck模型的非侵入式接口
原生支持并发编程
支持多种操作系统和体系结构的交叉编译
大量使用内置函数和接口来提高代码复用度
支持和C 语言相互调用的机制(CGO)

语言环境变量

$GOROOT GO 语言环境在计算机的安装位置
$GOPATH GO 语言工作目录,可以有多个

Go语言的源码文件有三大类:

  • 命令源码文件,可执行的程序的入口
  • 库源码文件,集中放置各种待被使用的程序实体
  • 测试源码文件,用于对前两种源码文件中的程序实体的功能和性能进行测试

Go Token(标记)

  • 关键字:25个
  • 标识符:40个
  • 分隔符
  • 字面量

变量

  • 变量代表某块内存区域
  • 变量的使用包括两个分开的步骤:变量声明、变量赋值
  • 变量声明后会立即分配存储空间
  • 变量声明后会初始化为该类型的零值
  • 同一代码块内不能多次声明同名变量
  • 子代码块可声明父代码块同名变量,子遮盖父
  • 变量未声明直接使用会出现编译错误
  • 变量在函数中声明了但未使用也会出现编译错误
  • GO 是强类型语言,编译器会确认每个变量应有的类型,错误使用将引发错误
  • GO 是静态语言,但支持编译时自动推断类型
  • 变量声明后需按对应类型赋值
  • 变量声明赋值可以同时进行
  • 变量声明赋值同时进行可以忽略类型,由编译器推断
  • 可在函数内部使用 := 进行短类型声明赋值

注:

  • 多变量同时赋值,只能在函数体内。a, b = 3, 4
  • 短类型声明赋值,只能在函数体内。c := true
  • 全局变量可以不被使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
var a int = 1 //声明赋值同时进行
//a =bool //错误赋值
var b = 2 // 声明未加类型,自动推断类型
a, b = 3, 4 //多变量同时赋值,只能在函数体内
//c := true //短类型声明赋值,只能在函数体内
func main() {
a, b = 3, 4 //多变量同时赋值,只能在函数体内
c := true //短类型声明赋值,只能在函数体内

fmt.Printf("a address: %v value: %v \n", &a, a)
fmt.Printf("b address: %v value: %v \n", &b, b)
fmt.Printf("c address: %v value: %v \n", &c, c)
}

iota枚举

Go里面的关键字iota,可以在声明enum时采用,它默认开始值是0,每调用一次加1:

1
2
3
4
5
6
7
8
9
10
11
12
const(
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)

const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0

const (
e, f, g = iota, iota, iota //e=0,f=0,g=0 iota在同一行值相同
)

除非被显式设置为其它值或iota,每个const分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是iota,则它也被设置为iota

语法

条件语句

if

  • GO不支持 ?: 三元运算符
  • if 后面的条件不需要( )
  • if 可带一个初始化子语句用;跟条件分开

switch

case 按照从上到下的顺序进行求值,直到找到匹配的项后执行并退出switch(除非使用fallthrough)。如果 switch 没有表达式,则对 true 进行匹配,因此,可以将 if else-if else 改写成一个 switch。

  • switch 一样可以带初始化子语句
  • switch 条件表达式不要求必须为整型,类型本身也可作条件判断
  • case 后的break 可以省略
  • 多个case连在一起是为了满足连续的条件范围
  • 也可以直接把多个case条件连在一起,在最后一个case写执行语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var a = "hello"
switch a {
case "hello":
fmt.Println(1)
case "world":
fmt.Println(2)
default:
fmt.Println(0)
}
// second
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s != "world":
fmt.Println("world")
}
/**
hello
world
*/

注:新编写的代码,不建议使用 fallthrough。

循环语句

for

  • GO 的循环语句只有for ,没有while/do while
  • ★:for 语句后面不能加( )
  • for语句的三个部分,省略任何一个时,分号不能省略
  • 只留条件判断时,可以不用分号 (相当于while语句)
  • 全部省略,变为无限循环

转移语句

  • break:语句用于跳出代码块或循环, 除了用在switch之外,还用于结束整个循环,不再进行循环条件判断

  • continue:语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环

  • 标签

    如果存在多重循环,默认情况下break语句和continue语句都只针对最内层循环。

    所以Go提供了标签,标签允许指定跳出的循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 使用标签
        flag := false
    end:
        for i := 1; i < 100; i++ {
            for j := 1; j < 100; j++ {
                if i*j == 651 {
                    flag = true
    //一次跳出
    break end 
                }
            }
        }
        fmt.Println(flag)

数据类型

分类

  • 命名类型:
    • 基础数据类型(整型、浮点型等)
    • 用户自定义类型(type关键字定义的结构、接口等)
  • 未命名类型
    • 集合类型(数组、切片、映射等)
    • 函数等

Bool类型

  • 布尔值包括true、false,类型长度为1字节
  • 布尔类型无法被其他类型赋值,也不支持类型转换
  • 布尔类型不支持用0和1表示真假

注:由于 Go语言是强类型的语言,如果不满足自动转换的条件,则必须进行强制类型转换。(C/C++等语言有隐式类型转换,golang中没有,即无法自动强转)

字符类型

  • byte ,对应整型里的uint8 ,代表 ASCII 码一个字符
  • rune,对应整型里的int32 ,代表Unicode码一个字符

格式化输出时,可用%c 输出对应值,如

1
2
//浙
fmt.Printf("%c",27993)

字符串类型

  • 字符串类型string,采用UTF-8编码格式的不可改变的字符序列
  • 字符串单行用双引号 ,多行可用反引号`,空格和缩进都会被保留

字符串标准库:

  • strings包提供了很多操作字符串的简单函数
  • strconv包提供了基本数据类型和字符串之间的转换
  • regexp包提供了正则表达式功能
  • unicode包及其子包 utf8、utf16 中,提供了对 Unicode 相关编码、解码的支持

strings包

1
2
3
4
5
6
7
8
9
10
11
// 子串 substr 在 s 中,返回 true
func Contains(s, substr string) bool
// chars 中任何一个 Unicode 代码点在 s 中,返回 true
func ContainsAny(s, chars string) bool
// Unicode 代码点 r 在 s 中,返回 true
func ContainsRune(s string, r rune) bool

/** 子串出现次数 */
func Count(s, sep string) int
// 字符串重复几次
func Repeat(s string, count int) string

unicode 包主要包含3个部分:

  • unicode :基本的字符判断函数
  • utf8 :负责 rune 和 byte 之间的转换
  • utf16 :负责 rune 和 uint16 数组之间的转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func IsControl(r rune) bool  // 是否控制字符
func IsDigit(r rune) bool  // 是否阿拉伯数字字符,即 0-9
func IsGraphic(r rune) bool // 是否图形字符
func IsLetter(r rune) bool // 是否字母
func IsLower(r rune) bool // 是否小写字符
func IsMark(r rune) bool // 是否符号字符
func IsNumber(r rune) bool // 是否数字字符,比如罗马数字Ⅷ也是数字字符
func IsOneOf(ranges []*RangeTable, r rune) bool // 是否是 RangeTable 中的一个
func IsPrint(r rune) bool // 是否可打印字符
func IsPunct(r rune) bool // 是否标点符号
func IsSpace(r rune) bool // 是否空格
func IsSymbol(r rune) bool // 是否符号字符
func IsTitle(r rune) bool // 是否 title case
func IsUpper(r rune) bool // 是否大写字符
func Is(rangeTab *RangeTable, r rune) bool // r 是否为 rangeTab 类型的字符
func In(r rune, ranges ...*RangeTable) bool  // r 是否为 ranges 中任意一个类型的字符

运算符

  • 算术运算符
    • 自增、自减,直支持a++,不支持++a ==> 设计思想是保证只有一种写法
  • 关系运算符
    • 因为bool类型不能转为整型,所以不支持不等式连写:比如 x<y<z 这种语法是错误的
  • 逻辑运算符
    • 注意短路
  • 赋值运算符
  • 位运算符

注:Go中的运算优先级跟C不太一样,比如Go中位移运算符的优先级是高于算数运算符的,而C中相反

1
2
3
4
5
// c++ --> 2
int res = 1 + 4 >> 1;
cout << res << endl;
// go --> 3
println( 1 + 4 >> 1)

见:

集合数据类型

数组(Array)

  • 数组是同类元素的集合,它的元素排列在连续的空间中,按下标来标记和访问
  • 数组类型定义包括元素类型,数组长度(元素个数)
  • 元素类型相同的两个数组,数组长度不同则类型不同,相互不能复制
  • 数组变量声明后,其元素类型、数组长度均不可变 (定长性)

注:数组传参问题——实参拷贝一份给形参,二者相互独立,传递大数组时效率较低,尽量用指向数组的指针来传参

数组声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//只声明未赋值
//数组元素都被初始化为对应类型零值
var arr1 [5]int
//声明3个元素的整型数组
//直接赋值
arr2 := [3]int{111213}
//声明整型数组
//直接赋值
//数组长度由初始化值的数量来确定
arr3 := [...]int{1112131415} //...不可省略
//声明4个元素的整型数组
//对下标为0和3的元素直接赋值
//其余元素保持零值
arr4 := [4]int{0993100}
fmt.Printf("%v,%v,%v,%v", arr1, arr2, arr3, arr4)
//[0 0 0 0 0],[11 12 13],[11 12 13 14 15],[99 0 0 100]

切片( Slice)

数组的定长性和值拷贝限制其使用

  • 切片封装底层的数组,提供长度可变的数组引用
  • 切片是引用类型,不支持==运算(除了nil)
  • 切片包括三个变量
    • 底层数组指针
    • 切片当前长度
    • 切片容量(小于等于底层数组长度,超过时要变更底层数组)

切片使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建指定的底层数组,然后创建切片
a := [5]int{7677787980}
s1 := a[0:4// from a[0] to a[3]
s2 := a[:4]  // from a[0] to a[3]
//不指定数组大小,同时创建匿名数组和返回切片引用 
d := []int{678}  
// 直接通过make函数创建切片
i := make([]int55) // 为什么不用构造函数

// 切片动态增加
sli = append(sli, 20)
// 切片合并:内置函数 append() 还支持切片的合并,用...运算符把对应切片所有元素都取出
veggies := []string{"potatoes""tomatoes""brinjal"}
fruits := []string{"oranges""apples"}
food := append(veggies, fruits...) //... 不可忽略

语法糖...

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

func MySum(p ...int) (sum int){ // 第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数。
sum = 0
for _, v := range p{
sum += v
}
return
}

func main() {
arr := []int{2,1,3}
i := MySum(arr...) // 第二个用法是将slice打散进行传递。
fmt.Printf("i: %v\n", i)
}

映射( Map)

映射用于存储一系列无序的键值对

  • 映射的键(key) 只支持值类型(可以使用==运算符作比较)
  • 映射的值(value)不限制,但所有元素的值类型一致
  • GO 映射的底层实现是哈希数组链表,不是 C++的红黑树

注:

  • 只声明不初始化的映射为nil值,未分配底层存储空间,不能添加元素

  • 用字面量或make函数进行初始化后可以添加元素

    即:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var m1 map[string]int
    fmt.Println(m1 == nil)
    //true
    //m1["a"] = 1 //error

    m2 := map[string]int{}
    fmt.Println(m2 == nil)
    //false
    m2["a"] = 1 //ok

    m3 := make(map[string]int)
    fmt.Println(m3 == nil)
    //false
    m3["a"] = 1 //ok

Map映射元素查找

  • 映射元素通过下标直接查找访问

    • 存在对应key的,返回对应value
    • 不存在对应key的,返回value 类型的零值

    ==> Q: 如果返回零值,那如何判断是否存在呢?

    A:映射元素通过下标访问其实可以返回两个值

    • 对应的value
    • 对应的key是否存在的布尔值

映射元素删除:使用内置函数delete()删除映射元素,如delete(personSalary,"steve")

二维映射map创建:

1
2
3
4
5
6
7
8
9
10
comEmp := map[string] map[string]int {
"IBM": {
"steve"12000,
"jamie"15000,
},
"HP": {
"mike"15000,
"joe":  9000,
},
}

若顶级类型只是一个类型名,你可以在文法的元素中省略它。

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

import "fmt"

type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}

func main() {
fmt.Println(m)
}

// demo2.
package main

import "fmt"
import "strconv"

type IPAddr [4]byte

// TODO: 给 IPAddr 添加一个 "String() string" 方法
func (f IPAddr) String() string {
str := ""
for _ ,v:= range f{
if str !="" {
str += ","
}
fmt.Printf(strconv.Itoa(int(v)))
str += strconv.Itoa(int(v))
}
return str
}

func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}

函数

概念:

函数是程序执行的基本语法结构:

  • 函数或方法编译成程序体代码区的一段指令序列
  • 进程执行模型大多基于“栈堆”
  • 函数抽象逻辑模块
  • 通过函数调用函数,层层叠叠的函数构成树结构组织代码
  • 函数效率高则程序效率高,建议多用标准库函数

函数为第一等公民(与其他数据类型地位平等)

  • 赋值给变量
  • 作为参数传递给其他函数
  • 作为函数的结果被返回
  • 支持闭包

语法格式:

1
2
3
func add(a, b int) int {
    return a + b
}
  • 函数可以有多个返回值
  • 返回值可以有变量名
  • 不支持函数重载

函数参数:

  • 参数传递方式都是值拷贝
  • 形参与实参是值传递时,形参的变化不影响实参
  • 形参与实参传递指针时,形参的变化有可能会影响实参
    • ===> 注意:引用类型其实传的就是指针
  • 不支持默认值参数

不定参数

  • 不定参数,形参数目可变、不确定
  • 不定参数声明语法格式:param … type
  • 不定参数类型必须相同
  • 不定参数必须是函数的最后一个参数
  • 不定参数的形参在函数内是切片
  • 切片传递给不定参数时,要用…运算符取出各元素 ==>多于函数定参数的参数将会被放到不定参数中
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

//不定参数函数
func sum(items ...int) (sum int) {
for _, v := range items { //items 相当于切片
sum += v
}
return
}

//切片参数函数
func sumS(items []int) (sum int) {
for _, v := range items {
sum += v
}
return
}

func main() {

slice := []int{1, 2, 3, 4, 5}
//array := [...]int{1, 2, 3, 4, 5}
fmt.Println(sum(1, 2, 3, 4, 5))
fmt.Println(sum(slice...)) //不定参数函数参数为切片时,需用...运算符
//fmt.Println(sum(array...)) //数组不支持...运算符
fmt.Println(sumS(slice)) //切片参数函数可直接用切片变量,不用...运算符
//fmt.Println(sumS(array)) //切片参数函数不能用数组传参
fmt.Printf("%T\n", sum)
fmt.Printf("%T\n", sumS)

}

函数类型

  • 函数类型又叫函数签名,显示函数类型fmt.Printf("%T\n", funcname)
  • 函数类型包括形参列表和返回值列表
    • 形参列表:形参的次序、个数和类型(形参名无关)
    • 返回值列表:返回值的次序、个数和类型(返回值名无关)
  • 可以使用type定义函数类型
  • 函数类型是引用类型,未初始化的零值为nil
  • 标准定义的函数名为常量,不可修改指向
  • 函数是第一公民,函数变量可赋值、传参等

匿名函数

匿名函数相当于函数字面量,可以使用函数变量的地方就可以使用匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//匿名函数直接调用
func(a,b int )int{
    return a-b
}(5,4)
//匿名函数赋值给函数变量
var sum = func(a,b int )int{
    return a+b
}
//函数作为返回值
func getFun(op string) func(a,b int )int {
    return func(a,b int )int{
        return a+b
    }
}

闭包

闭包=函数+引用环境,常见于匿名函数引用了函数定义环境的变量

  • 如果函数返回的闭包引用了该函数的局部变量
    • 每次调用函数都会为局部变量分配内存
    • 每次使用闭包都会影响局部变量

返回值

允许返回指定变量名

1
2
3
4
5
6
7
8
9
10
11
12
func addT1(a, b int) (int, bool) { //多值返回,返回值不命名
c := a + b
d := a > b
return c, d //按顺序输入返回值
}

func addT2(a, b int) (c int, d bool) { //多值返回,返回值命名
// 注, c,d变量的类型声明在函数签名中已经声明了,因此直接赋值即可,不用:=
c = a + b
d = a > b
return //直接返回
}

函数作为类型,以及函数赋值给变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func add(a, b int )int{
return a + b
}

func change(f func(a, b int)int ){
f = func(a, b int) int {
return a - b
}
}

func main() {
var f func(a, b int)int = add
change(f) // 未起作用
i := f(1, 2)
fmt.Printf("i: %v\n", i)
}

defer

Go 函数支持defer进行延迟调用
defer 类似OO语言异常处理中的finally子句,常用来保证系统资源的回收和释放

  • 在注册defer函数时,会把当时的实参值传递给形参,后续实参的变化不影响函数结果,如
1
2
3
4
5
6
a:=5
defer fmt.Println("defer注册函数时的a值",a) // a=5的时候,记录了defer
a=10
fmt.Println("普通函数的a值",a) // 后续变化了
//普通函数的a值10
//defer注册函数时的a值5
  • 使用多个defer时,这些defer 调用 以**先进后出(FILO)**顺序在函数返回前被执行

结构体

  • 结构把有内在联系的不同类型的数据统一成一个整体,使它们相互关联
  • 结构是变量的集合,从外部看是一个实体
  • 结构支持嵌套
  • 结构的字段类型不限
  • 结构的存储空间连续,按声明时的顺序存放

使用:

命名类型结构

1
2
3
4
5
6
7
8
9
10
11
12
type Employee struct {
    firstName string
    lastName  string
    age       int
    salary    int
}


type Employee struct {
    firstName, lastName string
    age, salary         int
}

匿名类型结构(直接创建结构变量)

1
2
3
4
var myemployee struct {  
    firstName, lastName string
    age, salary         int
}

带标签的结构体

  • 标签是一个附属于字段的字符串,用于描述字段信息
  • 标签还可以按key1:“value1” key2:“value2”键值对进行修饰,来提供编码、解码、ORM等转化辅助,比如JSON、BSON 等格式序列化

结构变量初始化

  • 用命名类型结构或匿名类型结构声明的结构变量,各字段初始化为相关类型的零值

  • 字段名初始化,不用按顺序未指定的字段为零值

    • 1
      2
      3
      4
      5
      6
      emp1 := Employee{
      firstName: "Sam",
      age:       25,
      salary:    500,
      lastName:  "Anderson", //逗号不能忽略
      }
  • 字面量初始化,按字段类型声明顺序并全部设置,顺序不对或遗漏字段报错

    • emp2 := Employee{"Thomas", "Paul", 29, 800}

访问结构体数据

  • 采用 结构变量.字段

    • 1
      2
      emp := Employee{"Thomas""Paul"29800}
      fmt.Println(emp.age)
  • 采用(*结构变量指针).字段 *为间访符

    • 1
      2
      emp := &Employee{"Sam""Anderson"556000}
      fmt.Println("First Name:", (*emp).firstName)
  • 采用 结构变量指针.字段,不支持->

    • 1
      2
      emp := &Employee{"Sam""Anderson"556000}
      fmt.Println("First Name:", emp.firstName)

匿名字段:结构体字段也可以省略字段名,字段名默认为对应数据类型名称(数据类型不能重复)

1
2
3
4
5
6
type Person struct {
string
int
}
p := Person{"Naveen"50}
p.int =60

嵌套结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Address struct {
    city, state string
}
type Person struct {
    name    string
    age     int
    address Address
}

func main() {
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.address = Address{
        city:  "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.address.city)
    fmt.Println("State:", p.address.state)
}

子结构字段提升

匿名子结构字段可以像父结构的字段一样被父访问(没有同名父结构字段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Address struct {  
    city, state string
}
type Person struct {  
    name string
    age  int
    Address //子结构匿名
}
func main() {  
    var p Person
    p.name = "Naveen"
    p.age = 50
    p.Address = Address{
        city:  "Chicago",
        state: "Illinois",
    }
    fmt.Println("Name:", p.name)
    fmt.Println("Age:", p.age)
    fmt.Println("City:", p.city)  //city 提升, 可以直接以父.city的形式使用
    fmt.Println("State:", p.state)  //state 提升
}

方法

方法是对具体类型行为的封装,本质上是绑定到该类型的函数

  • 非命名类型不能定义方法

  • OO语言的方法通常有个隐藏的this或self指针来指向对象

  • GO 把这个隐藏指针暴露出来,称为接收者(receiver)

  • 接收者可自定义名称,类型有值类型和指针类型两种,语法格式:

    • 值类型:func (t Type) funcName(paramList)(resultList)
    • 指针类型 :func (t *Type) funcName(paramList)(resultList)

    方法接收者

    • 方法接收者的本质是形参

      • 方法接收者为值时,方法修改对象属性将不能成功
      • 方法接收者为值时,需要在内存复制一份对象,效率低
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
          //方法接收者是值
      func (e Employee) changeName(newName string) {
      e.name = newName
      fmt.Printf("\nEmployee name in func is: %s", e.name)
      }

      //方法接收者是指针
      func (e *Employee) changeAge(newAge int) {
      e.age = newAge
      }
      // 实际上是拷贝了一个e对象,然后吧这个对象的name改掉了
      // Print(e.Name)实际上还是没有修改
      e.changeName("Michael")
      // 而接收者为指针的函数就能修改
      e.changeAge(18)


      // 除了上述调用以外,还可以通过类型调用的方式
      Employee.changeName(e, 52) //类型调用
      (*Employee).changeAge(&e, 52) //类型调用

总结:Go中的方法实现跟C比较类似,都是在体外完成对方法的具体实现。C是在类中声明函数原型,在类外以Class::FuncMethod(){ xxx }中具体实现函数。

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
type Employee struct {
    name     string
    salary   int
    currency string
}
//定义方法
func (e Employee) displaySalary() {
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {
    emp1 := Employee{
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    emp1.displaySalary() 
}
// Target:实际等价
type Employee struct {  
    name     string
    salary   int
    currency string
}
func displaySalary(e Employee) {  
    fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}
func main() {  
    emp1 := Employee{
        name:     "Sam Adolf",
        salary:   5000,
        currency: "$",
    }
    displaySalary(emp1)
}

Q:方法可用等价函数实现,为什么还要方法?

  • GO 的函数不能重载(即不准存在只是参数不同的同名函数),导致不同类型不能用同名函数,而不同类型的方法可以同名
  • GO 不支持class ,使用结构代替类,结构字段用来封装对象属性,方法用来封装对象的行为

方法提升

匿名子结构的方法可以像父结构的方法一样被父使用(没有同名父结构方法)

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
type Address struct {
city string
state string
}
type Person struct {
firstName ptring
lastName string
Address
}
func (a *Address) fullAddress() { //接收者是子结构指针
fmt.Printf("Full Address: %s, %s\n", a.city, a.state)
}
func main() {
p := Person{
firstName: "Elon",
lastName: "Musk",
Address: Address{
city: "Los Angeles",
state: "California",
},
}
p.Address.fullAddress() //完整调用方法
p.fullAddress() //父结构对象直接调用子结构方法
}

// 1. 修改匿名字段Address变成指针,则2
type Person struct {
firstName string
lastName string
*Address
}

func main() {
p := &Person{
firstName: "Elon",
lastName: "Musk",
Address: &Address{ // 2. 则这边传的时候应该是传地址
city: "Los Angeles",
state: "California",
},
}
}

自定义类型扩展方法

方法并非结构体专有,所有自定义类型都可以定义方法

1
2
3
4
5
6
7
8
9
type myInt int 		// 命名类型

func (a *myInt) add(b myInt) myInt {
    return *a + b
}

num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)

方法值

  • 方法本质上还是函数,所以方法可以赋值给函数变量
  • 方法值是对象变量初始化后的方法
  • 方法值其实是带有闭包的函数变量,接收者被隐式地保存在闭包里
  • 方法值赋给函数变量后,函数变量可直接调用
  • 方法也可以直接通过类型来调用,把接收者作为第一个参数e.play() <==>Employee.play(e), (*Employee).changeAge(&e, 52) //类型调用

接口

  • 接口是编程规约,一组方法签名集合
    • 方法声明的两个组件构成了方法签名 - 方法的名称参数类型
  • 传统OO里,接口通常用来抽象定义对象的行为,具体过程在类里实现 ,类在定义时要声明实现了哪些接口
  • GO 接口采用非侵入式,即具体类型实现接口不用显式声明,只要其方法集是接口的超集,编译时会进行对应校验
  • GO 接口只有方法签名,没有数据字段,没有函数体代码
  • 类型的方法集是多个接口的超集,则实现多个接口

类型定义:

  • 命名接口类型

    • 1
      2
      3
      4
      type interfaceName interface{//接口类型命名通常以er为后缀
          methodName(paramList)(resultList)
          otherInterfaceName
      }
  • 匿名接口类型

    • 1
      2
      3
      4
      interface{
          methodName(paramList)(resultList)
          interfaceName
      }

空接口

  • 空接口 interface{} 是一种匿名接口类型
  • 空接口的方法集为空,所有类型都实现了空接口,都可以赋值或传递给空接口
  • 非命名类型不能定义自己的方法,其方法集为空,只能传递给空接口==>map,slice
    • 方法需要接收类型参数,所以非命名类型不能定义自己的方法

接口初始化

  • 只声明未赋值的接口变量为nil
  • 接口变量初始化需要把接口绑定到具体类型实例
  • 未初始化的接口变量不能调用其方法
  • 方法的接收者才能给接口变量赋值
  • 接口变量的值包括底层类型的值和具体类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Printer interface { 
Print()
}
type S struct{}
func (s S) Print() { 
fmt.Println("print")
}   
var i interface{} = S{}
switch v := i.(type) { //通过v才能调用接口函数
case Printer:
v.Print()
default:
fmt.Printf("unknown type\n")
}

在内部,接口值可以看做包含值和具体类型的元组:(value, type)

接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。

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
type I interface {
M()
}

type T struct {
S string
}
func (t *T) M() {
fmt.Println(t.S)
}

type F float64
func (f F) M() {
fmt.Println(f)
}

func main() {
var i I

i = &T{"Hello"}
describe(i)
i.M()

i = F(math.Pi)
describe(i)
i.M()
}

func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
/**
(&{Hello}, *main.T)
Hello
(3.141592653589793, main.F)
3.141592653589793
*/

GO 的面向对象

Go 没有类,而是松耦合的类型、方法对接口的实现

封装

  • 用结构代替类
  • 用New函数代替构造函数

继承

  • 用类型组合来实现继承
  • 多重继承通过内嵌多个类型实现

多态

类型断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量:value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

下面有个例子。只有当某个interface{}的类型 存储的是int时才打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
var v interface{}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < 10; i++{
v = i
if (r.Intn(100) % 2) == 0 {
v = "hello"
}
if _, ok := v.(int); ok {
fmt.Printf("%d\n", v)
}
}
}

一个比较典型的应用场景就是:errors.(*MyError)

反射

  • 反射可以在运行时检查变量的类型和值,是元编程的一种形式,在没有源代码时帮助调试程序
  • 反射包 ” reflect” 通过空接口获取变量的类型和值
  • 用接口实现多态
  • 实例可以赋给它所实现的任意接口类型的变量
  • 反射包 ” reflect” 通过空接口获取变量的类型和值
    • func TypeOf(i interface{}) Type
    • func ValueOf(i interface{}) Value
  • reflect.Type 和 reflect.Value 各有许多方法,比如kind方法用于返回底层类型名称的常量
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
func add(a, b int) int {
return a + b
}
// 将函数包装为反射值对象
funcValue := reflect.ValueOf(add)
// 生成函数参数, 传入两个整型值
paramList := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)}
// 反射调用函数
retList := funcValue.Call(paramList)
// 获取第一个返回值, 取整数值
fmt.Println(retList[0].Int())



type Money float64

func main() {
var x Money = 58.9
fmt.Println("type:", reflect.TypeOf(x))
v := reflect.ValueOf(x)
fmt.Println("value:", v)
fmt.Println("type:", v.Type())
fmt.Println("kind:", v.Kind()) //查看底层类型
fmt.Println("settability of v:", v.CanSet()) //能否被修改 x的地址不能修改

/*
type: main.Money
value: 58.9
type: main.Money
kind: float64
settability of v: false
*/
}

错误处理

  • 传统OO里,异常是一种程序控制机制,依附于栈结构,却可以同时设置多个异常类型作为网捕条件,从而以类型匹配在栈机制中跳跃回馈
  • GO 语言里没有异常机制,只有错误处理,错误通过函数的多返回值来处理
  • GO 语言的错误主要有:
    • 编译错误
    • 运行时错误
    • 逻辑错误
  • GO错误处理方式
    • 错误可处理,通过函数返回错误进行处理
    • 错误不可处理,通过panic抛出错误,退出程序

错误实现

通过error 接口 实现错误处理的标准模式,打印错误时自动调用Error()函数

1
2
3
4
> type error interface{
>     Error() string
> }
>

实际使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type PathError struct {  
    Op   string
    Path string
    Err  error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }

f, err := os.Open("/test.txt")
// 类型断言
if perr, ok := err.(*os.PathError); ok {
fmt.Println("File at path", perr.Path, "failed to open")
return
}
fmt.Println(f.Name(), "opened successfully")

创建错误

  • 根据errors 包对错误的基本定义

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      package errors
      func New(text string) error {
          return &errorString{text}
      }
      type errorString struct {
          s string
      }
      func (e *errorString) Error() string {
          return e.s
      }
  • fmt包的Errorf 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
func circleArea1(radius float64) (float64, error) {
if radius < 0 {
return 0, errors.New("Area calculation failed, radius is less than zero")
}
return math.Pi * radius * radius, nil
}

func circleArea2(radius float64) (float64, error) {
if radius < 0 {
return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
}
return math.Pi * radius * radius, 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
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
package main

import "fmt"

type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}

// 实现接口
func (e *areaError) Error() string {
return e.err
}

//error的方法用来明确错误原因
func (e *areaError) lengthNegative() bool {
return e.length < 0
}

func (e *areaError) widthNegative() bool {
return e.width < 0
}

func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
// 1. 返回的是对象的地址
return 0, &areaError{err, length, width}
} //err 文本用来给错误提示信息
return length * width, nil
}

func main() {
length, width := -5.0, -9.0
// 2. 所以这边err是指针
area, err := rectArea(length, width)
if err != nil {
fmt.Print(err)
// 3. 所以这边类型断言得判断是不是areaError的指针
if err, ok := err.(*areaError); ok {
if err.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zero\n", err.length)
}
if err.widthNegative() {
fmt.Printf("error: width %0.2f is less than zero\n", err.width)

}
return
}
}
fmt.Println("area of rect", area)
}

自定义错误的实现逻辑:

  1. 自定义的错误都会重写Error() string的方法,即实现了error 接口。
  2. 在抛出自定义错误的函数中错误的返回值类型都是error接口
  3. 因此,在外部使用的时候data, err = Myfunc(),这边得到的err是error接口,至于捕捉自定义的错误就是通过对接口的类型断言来判断的了,即自定义的错误都能看到下面有if errObj, ok := err.(*MyError); ok的代码

使用 goto 集中处理错误——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
// 常规的写法
err := firstCheckError()
if err != nil {
    fmt.Println(err)
    exitProcess()
    return
}
err = secondCheckError()
if err != nil {
    fmt.Println(err)
    exitProcess()
    return
}

// Go借助标签特殊的写法
err := firstCheckError()
if err != nil {
    goto onExit
}
err = secondCheckError()
if err != nil {
    goto onExit
}
// 正常处理代码
onExit:
fmt.Println(err)
exitProcess()

panic(恐慌)

  • 通常情况下,向报告错误状态的方式是返回一个额外的error类型值。但是,当遇到不可恢复的错误状态,导致程序不能简单继续执行时引发panic
  • 引发panic的两种情况
    • 主动调用panic 函数,会产生一个运行时错误,该错误提供RuntimeError() 方法用于区别普通错误
    • 程序运行时出现未处理错误自动触发,比如当发生像数组下标越界或类型断言失败等运行时错误时,Go 运行时会自动触发panic
  • 不应通过调用panic()函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式

panicking终止过程

  • panic 类似异常会逐级上传
  • 在多层嵌套的函数调用中触发或调用panic,会马上中止当前函数的执行,逐级冒泡上传到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况

recover

  • panic一旦被引发就会导致程序崩溃,但无法保证程序不会发生任何运行时错误。
  • recover专用于“拦截”运行时panic,让进入恐慌的程序恢复过来并重新获得流程控制权。
  • recover 可以阻止panic继续向上传递
  • ▲.为确保捕获panic, recover 必须在延迟函数(defer)中执行

总结:

  • 程序发生的错误导致程序不能容错继续执行,应主动调用panic或由运行时抛出panic
  • 程序发生错误,但能容错继续执行的,正常情况用错误返回值,运行时错误非关键分支用recover 捕获panic
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 recoverName() {
if r := recover(); r != nil {
fmt.Println("recovered from fullName", r)
}
}
func recoverMain() {
if r := recover(); r != nil {
fmt.Println("recovered from main", r)
}
}

func fullName(firstName *string, lastName *string) {
defer recoverName()
if firstName == nil {
panic("runtime error: first name cannot be nil")
}
if lastName == nil {
panic("runtime error: last name cannot be nil")
}
fmt.Printf("%s %s\n", *firstName, *lastName)
fmt.Println("returned normally from fullName")
}

func main() {
defer recoverMain()
defer fmt.Println("deferred call in main")
//firstName := "Elon"
lastName := "Potter"
//fullName(&firstName, &lastName)
fullName(nil, &lastName)
fmt.Println("returned normally from main")
}

  • GO使用包来组织源代码和代码编译,实现代码复用
  • 任何源代码必须属于某个包,同时源码文件的第一行有效代码必须是package pacakge packageName 语句,声明自己所在的包。
  • ▲.包名为 main 为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件
  • 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下

包的引用格式

  • 标准引用格式

    • 1
      2
      import "fmt"
      fmt.Printf("Hello world!")

      通过:库/包.func的方式调用

  • 自定义别名引用格式

    • 1
      2
      import F "fmt"
      F.Printf("Hello world!")

      通过:定义的名称,如F.func的方式调用

  • 省略引用格式

    • 1
      2
      3
      import . "fmt"
      //不需要加前缀 fmt.
      Printf("Hello world!")
  • 匿名引用格式

    • 引用包,但是代码中却没有使用包,编译器会报错
    • 在引用某个包时,如果只是希望执行包初始化的 init 函数,而不使用包内部的数据时,可以使用匿名引用格式(_)
    • 1
      import _ "fmt"

init( )

  • init( )是特殊的函数,不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高
  • init( )常用于在开始执行程序之前对数据进行检验或修复,或者在程序开始之前调用后台执行的 goroutine
  • 每个源码可以使用 1 个 init() 函数,一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序

包加载顺序

  • 程序从 main 函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图
  • 每个包会先①初始化常量,然后②是全局变量,③最后执行包的 init 函数

包引用顺序

包内标识符导出——向外暴露

  • 一个包里的标识符(如类型、变量、常量等)要被外部访问,需将要导出的标识符的首字母大写

    1
    2
    3
    4
    var myVar = 100 //内部引用
    const MyConst = "hello" //导出
    type MyStruct struct {  //导出
    }
  • 在被导出的结构体或接口中,如果它们的字段方法首字母是大写,外部可以访问这些字段和方法

    1
    2
    3
    4
    5
    6
    7
    8
    type MyStruct struct {			//结构体要被导出    
        ExportedField int // 包外可以访问的字段    
        privateField int  // 仅限包内访问的字段
    }
    type MyInterface interface { //接口要被导出   
        ExportedMethod()  // 包外可以访问的方法    
        privateMethod() // 仅限包内访问的方法
    }

    总结:Go没有提供权限控制符,而是通过了首字母是否大小写的统一规定来加上权限控制,这个跟Python中私有权限加__,保护权限加_颇为类似

go.mod

  • 使用GOPATH 不太方便

  • go.mod是Go1.11版本新引入的官方包管理工具

  • 在项目目录下用go.mod 文件来记录依赖包具体版本,方便依赖包、源代码和版本控制的管理、

  • https://github.com/golang/go/wiki/Modules

  • go.mod 文件内容

    • module:指定包的名字
    • go:用于标识当前模块的 Go 语言版本,值为初始化模块时的版本
    • require:指定的依赖项模块
    • replace:可以替换依赖项模块
    • exclude:可以忽略依赖项模块
  • go mod命令

    gomod

go.sum 文件

  • go.sum 文件 在执行 go get xxxx之后或直接编译使用第三方包的源代码时自动生成
  • 详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改

go.mod 使用基本过程

  • 创建项目目录Dir
  • 在项目目录运行 go mod init projectName,生成go.mod 文件
  • 在项目目录执行 go get xxxx 下载第三方包, 会生成go.sum文件 (可选)
  • 在项目目录下新建子目录,创建项目子包(可选,无需再生成go.mod)

并发

  • 并行是在任一粒度的时间瞬间都同时执行,比如多机并行
  • 并发是在规定的时间期限内多个任务都在执行,实际底层是分时操作
  • 并行强调瞬时性、并发强调过程性
  • 并行关键在于执行、并发关键在于结构
  • 单机运行时,并行通过使用多处理器以提高速度,并发程序可以是并行的,也可以不是
  • 应用程序具备好的并发结构,操作系统才能更好地利用硬件并行执行

进程( process )、线程( thread )和协程( coroutine )

  • 进程是程序在内存中运行时操作系统对其进行资源分配和调度的独立单位
  • 线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位
  • 每个进程至少包括一个线程
  • 每个进程的初始线程被称为主线程,主线程终止,进程终止
  • 协程是轻量级的线程,一个线程可以拥有多个协程
  • 进程和线程是操作系统级的,协程是编译器级的。协程不被操作系统内核管理,而完全由程序控制,因此没有线程切换的开销。
  • 和多线程比,数量越多,协程的性能优势就越明显。协程的最大优势在于其轻量级,可以轻松创建上万个而不会导致系统资源衰竭

go routine 特性

  • 多数语言在语法层不支持协程,而是通过库方式,效率不高,容易阻塞
  • Go 在语言级别支持协程
    • 命名为goroutine,关键字go
    • 由Go语言运行时统一调度,合理分配给各个CPU
  • 各goroutine非阻塞,不会等待
  • goroutine 可以并行执行
  • goroutine执行的函数返回值被忽略===>因此需要得到返回结果的话需要通过chan
  • 运行时不保证各goroutine的执行顺序
  • goroutine之间被平等地调度和执行
  • main函数会单独创建和分配一个go routine

协程间的通信——通道

  • 通道是一种特殊的类型,同时只能有一个 goroutine 访问通道进行发送和获取数据。
  • 通道是一个队列,遵循先入先出(FIFO)的规则
  • 通道默认是阻塞的,使goroutine有效通信,不需要使用其他语言的显式锁条件变量
  • 通道是引用类型,需要使用chan关键字和内置函数make 进行创建
  • 通道写入和读取使用 <- 运算符
    • 写入 :通道<-变量
    • 读取: 变量<-通道
  • 通道包括无缓冲通道和有缓冲通道
    • 无缓冲通道 make(chan datatype)
    • 有缓冲通道 make(chan datatype,capacity)
  • 无缓冲通道只能存储一条消息,有缓冲通道可以根据make函数的capacity参数存储n条消息,按FIFO读出
  • 缓冲与阻塞
    • 无缓冲通道,写入等待读取,读取等待写入,在双方准备好之前是阻塞的
    • 有缓冲通道,通道已满时的写入会等待,通道已空的读取会等待

单向通道

  • 通道默认为双向的,单向通道只能用于发送或接收数据
  • 所谓单向通道只是对通道作为函数参数的一种使用限制,跟C语言使用const修饰函数参数为只读类似
  • 通常先创建双向通道,在函数形参中利用<-运算符修饰通道,使之变为只读或只写通道
    • func pull(ch <-chan int) //只读 func pump(ch chan<- int) //只写
  • 关闭通道:
    • 关闭通道使用内置函数close(),实际上是关闭写入,即发送者告诉接收者不会再有数据发往通道
    • 接收者能够在通道接收数据的同时,获取通道是否已关闭的参数 v, ok := <-ch
    • for range语句能自动判断通道是否已关闭

channel底层实现

src/runtime/chan.go:hchan定义了channel的数据结构

缓冲区是一个环形队列

一个channel只能传递一种类型的值,类型信息存储在hchan数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列长度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写入时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列,读阻塞的goroutine会被向channel写入数据的goroutine唤醒
sendq waitq // 等待写消息的goroutine队列,写阻塞的goroutine会被从channel读数据的goroutine唤醒
lock mutex // 互斥锁,一个channel同时仅允许被一个goroutine读写
}

通道底层实现 向channel写数据

  • 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
  • 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
  • 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒

chan_send

通道底层实现 从channel读数据

  • 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
  • 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
  • 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  • 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;

chan_recv

定时器

Timer

协程间的通信需设置超时等辅助机制

  • 一次性定时器:定时器只计时一次,结束便停止

  • 主要方法

    1
    2
    3
    4
    5
    func NewTimer(d Duration) *Timer // 指定一个时间创建一个TimerTimer一经创建便开始计时,不需要额外的启动命令
    func (t *Timer) Stop() bool // 停止计时器,返回值true:定时器超时前停止, false: 定时器超时后停止
    func (t *Timer) Reset(d Duration) bool // 停掉定时器,再启动,返回值同上
    func After(d Duration) <-chan Time // 创建匿名不需控制的计时器
    func AfterFunc(d Duration, f func()) *Timer // 延迟方法调用

    注:Timer一经创建便开始计时,不需要额外的启动命令

Ticker

周期性定时器:定时器周期性进行计时,除非主动停止,否则将永久运行

1
2
func NewTicker(d Duration) *Ticker // 指定一个时间创建一个TickerTicker一经创建便开始计时,不需要额外的启动命令
func (t *Ticker) Stop() // 停止计时,但管道不会被关闭

并发函数

WaitGroup

WaitGroup提供多个协程同步(平级)的机制,用来等待多个协程完成

信号量, Unix中保护共享资源的机制,用于防止多个线程同时访问某个资源

  • 信号量>0,表示资源可用,获取信号量时系统自动将信号量减1
  • 信号量==0时,表示资源暂不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒

WaitGroup 的方法

  • Add(delta int) 添加等待信号量
  • Done() 释放等待信号,每次减少1
  • Wait() 阻塞调用该方法的协程,直到等待信号量为0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func process(i int, wg *sync.WaitGroup) {
fmt.Println("started Goroutine ", i)
time.Sleep(2 * time.Second)
fmt.Printf("Goroutine %d ended\n", i)
wg.Done() //goroutine执行结束后将信号量减1
}

func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1) //增加信号量
go process(i, &wg)
}
wg.Wait() //主goroutine阻塞调用该方法的协程,直到等待信号量为0
fmt.Println("All go routines finished executing")
}

select

多路复用是在一个信道上传输多路信号或数据流,比如网线

  • select 借用网络多路复用的概念,用于监听多个通道,同时响应多个通道
  • 多个通道都没有可写或可读的状态,select 会阻塞
  • 有一个通道是可写或可读的, select 会执行该通道语句
  • 有多个通道是可写或可读的, 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

func server1(ch chan string) {
time.Sleep(2 * time.Second) //可取消
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second) //可取消
ch <- "from server2"

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

context

  • WaitGroup用来控制多个平级goroutine同时完成

  • goroutine本身是平等的,但逻辑上可能有父子关系,context 表示程序上下文,是程序的运行状态,用来控制具有逻辑父子关系的多个goroutine

  • Context接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type Context interface {
        //返回超时时间和是否已设置超时时间
        Deadline() (deadline time.Time, ok bool)
        //返回信道,当Context被撤销或过期时,该信道是关闭的
        Done() <-chan struct{}
        //Done信道关闭后,Err方法表明Context被撤销的原因
        Err() error
        //协程间的数据共享
        Value(key interface{}) interface{}
    }

context 使用

  • 用context的树结构来给平等的goroutine 设置父子逻辑

  • context树的根节点通常是一个空的context ,由第一个goroutine用Background() 函数创建,该context不能被取消、没有值、也没有过期时间

  • 创建子节点:

    1
    2
    3
    4
    5
    6
    7
    8
    //将父节点复制到子节点,返回一个可以主动撤销Context的函数
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    //过期时间由deadlineparent的过期时间共同决定,parent过期时间优先
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    //与WithDeadline类似,只不过传入的是从现在开始Context剩余的生命时长
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    //将父节点复制到子节点,子节点可扩展新的key存储的值
    func WithValue(parent Context, key interface{}, val interface{}) Context

e.g.

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
func HandelRequest(ctx context.Context) {
go WriteLog(ctx)
go WriteDB(ctx)
for {
select {
case <-ctx.Done():
fmt.Println("请求处理完毕")
return
default:
fmt.Println("请求处理中……")
time.Sleep(2 * time.Second)
}
}
}
func WriteLog(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("写日志完成")
return
default:
fmt.Println("写日志中……")
time.Sleep(2 * time.Second)
println("日志ing")
}
}
}
func WriteDB(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("写数据库完成")
return
default:
fmt.Println("写数据库中……")
time.Sleep(2 * time.Second)
}
}
}
func main() {
//WithCancel 一旦触发该子context的cancel, 那么该context绑定上的子协程都会被关闭
/*
ctx, cancel := context.WithCancel(context.Background())
go HandelRequest(ctx)
time.Sleep(5 * time.Second)
fmt.Println("所有子协程都需要结束!")
cancel()
//Just for test whether sub goroutines exit or not
time.Sleep(5 * time.Second) */

//WithTimeout 子context会在5s后过期, 所以其执行的协程最多只能运行5s,相当于绑定了一个定时器
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
go HandelRequest(ctx)
time.Sleep(10 * time.Second)
}

注: 关于context.WithValue和context.Value的使用方式见:https://blog.csdn.net/u012190809/article/details/107700495

mutex

  • 多个线程同时竞争使用某个变量可能会导致结果失控
  • mutex,互斥锁,用来保证某个变量在任一时刻,只能有一个线程访问
  • mutex 用Lock()和Unlock()来创建资源的临界区,这一区间内的代码是线程安全的,任何一个时间点都只能有一个goroutine执行这段区间的代码
  • mutex 也可以用通道来代替,二者无优劣之分,通常不涉及线程交互数据的用mutex,其他用通道
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

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1 //锁定后访问全局变量
m.Unlock()
wg.Done()
}
// 用Mutex可以用Chan中自带的Mutex来实现互斥
func incrementByChan(wg *sync.WaitGroup, ch chan bool) {
ch <- true
x = x + 1 //锁定后访问全局变量
<- ch
wg.Done()
}
func main() {
var w sync.WaitGroup
// var m sync.Mutex
ch := make(chan bool, 1)
for i := 0; i < 1000; i++ {
w.Add(1)
// go increment(&w, &m)
go incrementByChan(&w, ch)
}
w.Wait()
fmt.Println("final value of x", x) //结果确定
}

RWMutex

  • RWMutex在Mutex的基础之上增加了读、写的信号量,并使用了类似引用计数的读锁数量

  • 可以同时申请多个读锁

  • 有读锁时申请写锁将阻塞

  • 只要有写锁,后续申请读锁和写锁都将阻塞

  • 主要方法:

    1
    2
    3
    4
    func (rw *RWMutex) Lock() //申请写锁
    func (rw *RWMutex) Unlock() //释放写锁
    func (rw *RWMutex) RLock() //申请读锁
    func (rw *RWMutex) RUnlock()//释放读锁

e.g.

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
func main() {
var wg sync.WaitGroup
var rm sync.RWMutex
wg.Add(2)
go func() {
time.Sleep(20 * time.Millisecond)
for i := 0; i < 1000; i++ {
rm.Lock()
j++
fmt.Printf("Write lock %d\n", j)
rm.Unlock()
}
wg.Done()
}()

go func() {
time.Sleep(18 * time.Millisecond)
for i := 0; i < 1000; i++ {
rm.RLock()
k++
//fmt.Printf("Read lock %d\n", j)
fmt.Printf("Read lock %d\n", k)
rm.RUnlock()
}
wg.Done()
}()
wg.Wait()
}

Cond ( condition 条件变量)

  • 互斥锁保证在同一时刻仅有一个线程访问某一个共享数据

  • 条件变量在共享数据的状态发生变化时,通知其他因此而被阻塞的线程

  • 条件变量需要用互斥锁来创建

  • 主要方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 实例化一个带有Locker的Cond变量
    func NewCond(l Locker) *Cond
    //Unlock()->阻塞等待通知(等待Signal()Broadcast()的通知)->收到通知->Lock()
    func (c *Cond) Wait()
    //激活执行Wait()的通知队列的第一个协程
    func (c *Cond) Signal()
    //激活执行Wait()的通知队列所有协程 
    func (c *Cond) Broadcast()
    //使用内置的互斥锁
    cond.L.Lock()cond.L.Unlock()

e.g.

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 locker = new(sync.Mutex)
var cond = sync.NewCond(locker)

func main() {
for i := 0; i < 5; i++ {
go func(x int) {
cond.L.Lock() // wait 前,必须要先加锁
defer cond.L.Unlock() //保障释放资源
cond.Wait()
fmt.Println(x)
time.Sleep(time.Second * 1)
}(i)
}
time.Sleep(time.Second * 1)
fmt.Println("Signal....")
cond.Signal() // 下发一个通知给已经获取锁的goroutine
time.Sleep(time.Second * 3)
fmt.Println("Signal....")
cond.Signal() // 3 秒之后,下发一个通知给已经获取锁的goroutine
time.Sleep(time.Second * 3)
fmt.Println("Broadcast...")
cond.Broadcast() // 3 秒之后,下发通知给所有已经获取锁的goroutine

time.Sleep(time.Second * 3)
}

附:

语言设计思想

  • 保证只有一种写法

  • 少即是多

  • GO 不支持class ,使用结构代替类,结构字段用来封装对象属性,方法用来封装对象的行为

  • 用类型组合来实现继承

  • 多态:Go中只要结构体实现了接口的部分方法,就可以通过接口指向该结构体,并且使用被实现的方法。因此,可以看到Go的代码中函数传参很多都是接口,但真正使用的时候传入的是实现了该接口的struct,这就是Go中多态提现的一个例子。

new 和 make区别

  • new和make是内建的两个函数,主要用来在堆上创建分配类型的内存
  • new用于普通类型的内存分配,内存清零,返回该类型指针
  • make(专门)用于slice、map以及channel的初始化,返回引用

golang中分为值类型和引用类型

  • 值类型分别有:int系列、float系列、bool、string、数组和结构体

  • 引用类型有:指针、slice切片、管道channel、接口interface、map、函数等

  • 值类型的特点是:变量直接存储值,内存通常在栈中分配

  • 引用类型的特点是:变量存储的是一个地址,这个地址对应的空间里才是真正存储的值,内存通常在堆中分配

问题

Go中引用类型有哪些?

在 Go 语言中,引用类型有 切片 (slice) 、 字典 (map) 、 接口 (interface) 、 函数 (func) 以及 通道 (chan) 。

引用类型之所以可以引用,是因为我们创建引用类型的变量,其实是一个标头值,标头值里包含一个指针,指向底层的数据结构,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层结构并没有被复制传递,这也是引用类型传递高效的原因。

注:用户自定义类型不是引用类型===>所以函数传参的时候都传的是地址,形参都是指针类型。And: 引用类型不支持==运算符,无法直接比较

总结:Go中规定的引用类型就5个:切片 (slice) 、 字典 (map) 、 接口 (interface) 、 函数 (func) 以及 通道 (chan),但传参为这几个类型的时候,不需要传指针,直接用相应的类型即可。

课上训练题

自定义排序

标准库的 sort 包采用的是快速排序

请模仿其实现,基于Sorter 接口实现冒泡排序

请按如下要求设计协程交互代码

  • 协程一随机给出0~100的数
  • 协程二猜协程一的数,猜中的话协程一返回信号0,结束程序;猜小了协程一返回信号-1,猜大了协程一返回信号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
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

package gg

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

var up = 100
var down = 0
var last = -1
func guest(ch chan int){
for {
signn := <- ch
fmt.Printf("协程得到的sign: %v\n", signn)
var ths int
switch signn{
case -1:
// down ~ last
down = last
ths = rand.Intn(up - down) + down
fmt.Printf("协程随机到的ths: %v\n", ths)
ch <- ths
last = ths
case 1:
// last ~ up
up = last
ths = rand.Intn(up-down) + down
fmt.Printf("协程随机到的ths: %v\n", ths)
ch <- ths
last = ths
default:
break
}
}
}

func main() {
rand.Seed(time.Now().UnixNano())
num := rand.Intn(100)
fmt.Printf("随机到的数为: %v\n", num)
ch := make(chan int, 1)
ch <- -1
var n int
go guest(ch)

for {
n = <- ch
fmt.Printf("主线程得到数值为%v\n", n)
if ( n == num ){
ch <- 0
fmt.Printf("协程猜中啦, 数值为%v\n", n)
break
}else if (n < num){
ch <- -1
}else if (n> num){
ch <- 1
}
}
}

Go复习题目:

考试中容易出的坑题:

  • ▲与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换
  • ▲与 C 不同,Go 没有指针运算。

容易踩的坑:

  • nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。

  • 允许对值为 nil 的 slice 添加元素,但对值为 nil 的 map添加元素则会造成运行时 panic

  • string 类型的变量值不能为 nil ==> string 的零值为""

  • 访问 map 中不存在的 key,Go中返回的是零值,而不是报错

  • range后迭代对象:数组、通道、string、map

    • 数组:for i, v := range arr,i为索引,v为arr[i]的值
    • 通道:for data := ch, data为数据
    • range 迭代 string 得到的值:
      • range 得到的索引是字符值(Unicode point / rune)第一个字节的位置,与其他编程语言不同,这个索引并不直接是字符在字符串中的位置。
      • 注意一个字符可能占多个 rune,比如法文单词 café 中的 é。操作特殊字符可使用norm 包。
      • for range 迭代会尝试将 string 翻译为 UTF8 文本,对任何无效的码点都直接使用 0XFFFD rune(�)UNicode 替代字符来表示。如果 string 中有任何非 UTF8 的数据,应将 string 保存为 byte slice 再进行操作。
    • range 迭代 map
      • 如果你希望以特定的顺序(如按 key 排序)来迭代 map,要注意每次迭代都可能产生不一样的结果。
      • Go 的运行时是有意打乱迭代顺序的,所以你得到的迭代结果可能不一致。但也并不总会打乱,得到连续相同的 5 个迭代结果也是可能的
      • 如果你去 Go Playground 重复运行上边的代码,输出是不会变的,只有你更新代码它才会重新编译
  • string 与 byte slice 之间的转换:Go 在 string 与 byte slice 相互转换上优化了两点,避免了额外的内存分配:

    • 在 map[string] 中查找 key 时,使用了对应的 []byte,避免做 m[string(key)] 的内存分配
    • 使用 for range 迭代 string 转换为 []byte 的迭代:for i,v := range []byte(str) {…}
  • 在多行 array、slice、map 语句的末尾缺少 , 号

  • switch 中的 fallthrough 语句:switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。 <=等价于=>改写成 case 为多条件判断:

  • 自增和自减运算: Go 特立独行,去掉了前置操作,同时 ++、-- 只作为运算符而非表达式。

  • 不导出的 struct 字段无法被 encode。以小写字母开头的字段成员是无法被外部直接访问的,所以 struct 在进行 json、xml、gob 等格式的 encode 操作时,这些私有字段会被忽略,导出时得到零值:

  • Go程序默认不等所有 goroutine 都执行完才退出

  • 常用解决办法:使用 “WaitGroup” 变量,它会让主程序等待所有 goroutine 执行完毕再退出。

  • 向已关闭的 channel 发送数据会造成 panic

  • 在一个值为 nil 的 channel 上发送和接收数据将永久阻塞:

  • 若函数 receiver 传参是传值方式,则无法修改参数的原有值

  • 从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法:

  • 跳出 for-switch 和 for-select 代码块:没有指定标签的 break 只会跳出 switch/select 语句,若不能使用 return 语句跳出的话,可为 break 跳出标签指定的代码块:

  • Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)

  • 对Slice使用append,当长度超出时会返回新的Slice,因此无法在递归的时候不能直接传引用对象,而是需要传指针*[]int指针,并且在append的时候改成*ans = append(*ans, root.Val)

    • Leetcode94树的中层遍历:

      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
      func inorderTraversal(root *TreeNode) []int {
      if root == nil {
      return []int{}
      }
      ans := []int{}
      OrderTravel(root, &ans)
      return ans
      }
      /**
      如果ans不传指针,而是直接ans []int传参,关注ans地址变化,输出地址得到:
      append前:0xc000004078
      append后:0xc000004078
      append前:0xc0000040c0
      append后:0xc0000040c0
      append前:0xc0000040a8
      append后:0xc0000040a8
      */
      func OrderTravel(root *TreeNode, ans *[]int){
      if root == nil {
      return
      }
      OrderTravel(root.Left, ans)
      *ans = append(*ans, root.Val)
      OrderTravel(root.Right, ans)
      }
      /** 修改成传指针后ans的地址就始终是一致的了
      append前: 0xc000004078
      append后: 0xc000004078
      append前: 0xc000004078
      append后: 0xc000004078
      append前: 0xc000004078
      append后: 0xc000004078
      */
      func main() {
      root := &TreeNode{
      Val: 1,
      Left: nil,
      Right: &TreeNode{
      Val: 2,
      Left: &TreeNode{Val: 3},
      },
      }
      res := inorderTraversal(root)
      fmt.Println(res)
      }

from:Golang新手可能会踩的50个坑

Author: Mrli

Link: https://nymrli.top/2021/11/02/浙江大学期末考试——Go语言/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Linux iptables学习
NextPost >
k8s之kube-proxy源码分析
CATALOG
  1. 1. 浙江大学期末考试——Go语言
    1. 1.1. 概论
      1. 1.1.1. 算法:
      2. 1.1.2. 基本特征:
      3. 1.1.3. 结构化程序设计特点
      4. 1.1.4. OOP
      5. 1.1.5. GO 特性
      6. 1.1.6. 语言环境变量
      7. 1.1.7. Go语言的源码文件有三大类:
      8. 1.1.8. Go Token(标记)
    2. 1.2. 变量
    3. 1.3. iota枚举
  2. 2. 语法
    1. 2.1. 条件语句
      1. 2.1.1. if
      2. 2.1.2. switch
    2. 2.2. 循环语句
      1. 2.2.1. for
    3. 2.3. 转移语句
  3. 3. 数据类型
    1. 3.1. Bool类型
    2. 3.2. 字符类型
    3. 3.3. 字符串类型
  4. 4. 运算符
  5. 5. 集合数据类型
    1. 5.1. 数组(Array)
    2. 5.2. 切片( Slice)
    3. 5.3. 映射( Map)
  6. 6. 函数
    1. 6.1. 语法格式:
    2. 6.2. 函数参数:
      1. 6.2.1. 不定参数
    3. 6.3. 函数类型
    4. 6.4. 匿名函数
    5. 6.5. 闭包
    6. 6.6. 返回值
  7. 7. defer
  8. 8. 结构体
    1. 8.1. 使用:
      1. 8.1.1. 命名类型结构
      2. 8.1.2. 匿名类型结构(直接创建结构变量)
      3. 8.1.3. 访问结构体数据
    2. 8.2. 嵌套结构
      1. 8.2.1. 子结构字段提升
  9. 9. 方法
    1. 9.1. 方法提升
    2. 9.2. 方法值
  10. 10. 接口
    1. 10.1. 空接口
    2. 10.2. 接口初始化
    3. 10.3. GO 的面向对象
  11. 11. 类型断言
  12. 12. 反射
  13. 13. 错误处理
    1. 13.1. 错误实现
      1. 13.1.1. 创建错误
      2. 13.1.2. 创建自定义错误
    2. 13.2. panic(恐慌)
      1. 13.2.1. panicking终止过程
    3. 13.3. recover
  14. 14.
    1. 14.1. 包的引用格式
    2. 14.2. init( )
    3. 14.3. 包加载顺序
    4. 14.4. 包内标识符导出——向外暴露
    5. 14.5. go.mod
      1. 14.5.1. go.sum 文件
  15. 15. 并发
    1. 15.1. 进程( process )、线程( thread )和协程( coroutine )
    2. 15.2. go routine 特性
    3. 15.3. 协程间的通信——通道
      1. 15.3.1. channel底层实现
    4. 15.4. 定时器
      1. 15.4.1. Timer
      2. 15.4.2. Ticker
    5. 15.5. 并发函数
      1. 15.5.1. WaitGroup
      2. 15.5.2. select
      3. 15.5.3. context
        1. 15.5.3.1. context 使用
      4. 15.5.4. mutex
      5. 15.5.5. RWMutex
      6. 15.5.6. Cond ( condition 条件变量)
  16. 16. 附:
    1. 16.1. 语言设计思想
    2. 16.2. new 和 make区别
    3. 16.3. golang中分为值类型和引用类型
    4. 16.4. 问题
      1. 16.4.1. Go中引用类型有哪些?
    5. 16.5. 课上训练题
      1. 16.5.1. 自定义排序
      2. 16.5.2. 请按如下要求设计协程交互代码
    6. 16.6. Go复习题目:
    7. 16.7.
    8. 16.8. 考试中容易出的坑题: