Go基础语法
Go基础语法
第一部分《基础概念》
类型
Go语言中有以下基本类型:
布尔类型(bool):表示真(true)或假(false)的值。
整数类型:
- 有符号整数类型:int8、int16、int32、int64、int。
- 无符号整数类型:uint8、uint16、uint32、uint64、uint。
浮点数类型:
- 浮点数类型:float32、float64。
复数类型:
- 复数类型:complex64、complex128。
字符串类型(string):表示一串字符。
字符类型(rune):表示一个Unicode字符。
错误类型(error):表示错误的接口类型。
数组类型(array):表示具有固定长度的同类型元素的集合。
切片类型(slice):表示可变长度的同类型元素的序列。
映射类型(map):表示键值对的集合。
结构体类型(struct):表示多个字段的集合。
接口类型(interface):表示一组方法的集合。
函数类型(func):表示函数的类型。
通道类型(channel):用于协程之间的通信。
除了基本类型,Go语言还提供了指针类型(pointer)、切片类型(slice)、映射类型(map)、结构体类型(struct)、接口类型(interface)和函数类型(func)等复合类型,用于组合和抽象数据。此外,还可以使用关键字type
来定义自定义类型。
需要注意的是,Go语言是静态类型语言,变量在声明时需要指定其类型。同时,Go语言还支持类型推断,可以根据变量的初始值自动推断其类型。
是的,还有一些常用的类型在Go语言中:
字节类型(byte):表示一个字节的值,等价于
uint8
类型。字节切片类型([]byte):表示一个字节的切片,常用于处理二进制数据。
缓冲区类型(bytes.Buffer):提供了操作字节切片的缓冲区,可以方便地进行读写操作。
输入输出类型(io):提供了用于输入输出操作的接口和函数,包括读写数据、复制数据、处理流等。
这些类型在Go语言中广泛应用于处理字节数据、缓冲区操作和输入输出操作。字节类型和字节切片类型常用于处理二进制数据,缓冲区类型(bytes.Buffer)提供了方便的缓冲区操作方法,输入输出类型(io)提供了处理输入输出的接口和函数,方便进行数据的读写和流处理。
需要注意的是,这些类型都属于Go语言标准库中的类型,可以通过import
语句引入相应的包来使用。
初始化
以下是一些常见数据结构、对象和数组的初始化代码模板:
切片(Slice)初始化:
1
2
3
4
5
6
7
8// 初始化一个空切片
var slice []Type
// 使用字面量初始化切片
slice := []Type{value1, value2, value3}
// 使用make函数初始化指定长度和容量的切片
slice := make([]Type, length, capacity)映射(Map)初始化:
1
2
3
4
5
6
7
8
9// 初始化一个空映射
var m map[KeyType]ValueType
// 使用字面量初始化映射
m := map[KeyType]ValueType{
key1: value1,
key2: value2,
key3: value3,
}结构体(Struct)初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13// 定义结构体类型
type StructName struct {
field1 Type1
field2 Type2
// ...
}
// 使用字面量初始化结构体
object := StructName{
field1: value1,
field2: value2,
// ...
}数组(Array)初始化:
1
2
3
4
5
6
7
8// 静态初始化
array := [length]Type{value1, value2, value3}
// 动态初始化
var array [length]Type
array[0] = value1
array[1] = value2
array[2] = value3
需要注意的是,Go语言中没有提供类似Java中的ArrayList和HashSet这样的动态数据结构,而是使用切片和映射来实现类似的功能。同时,根据实际需求,可以根据需要添加更多的元素或键值对。
Go 运算符
除去算数运算符(自增/子减)和 指针运算符 ,其余运算符大致一样
运算符优先级表
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | || | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | | | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等 | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
注意:优先级值越大,表示优先级越高。
算术 运算符
语法
运算符 | 说明 | 范例 | 结果 |
---|---|---|---|
+ | 正号 | +3 | 3 |
- | 负号 | -4 | -4 |
+ | 加法运算 | 5 + 5 | 10 |
- | 减法运算 | 10 - 5 | 5 |
* | 乘法运算 | 5 * 2 | 10 |
/ | 除法运算 | 10 / 3 | 3 |
% | 取模运算 | 10 % 3 | 1 |
++ | 自增运算 | a = 2; a++ | 3 |
– | 自减运算 | a = 10; a– | 9 |
+ | 字符串拼接 | “Hai” + “Coder” | “HaiCoder” |
1 |
|
赋值运算符
十进制运算
运算符 | 说明 | 范例 | 结果 |
---|---|---|---|
= | 将一个表达式的值赋给另一个 | C = A + B | C = A + B |
+= | 相加后再赋值 | C += A | C = C + A |
-= | 相减后再赋值 | C -= A | C = C - A |
*= | 相乘后再赋值 | C *= A | C = C * A |
/= | 相除后再赋值 | C /= A | C = C / A |
%= | 求余后再赋值 | C %= A | C = C % A |
二进制运算
运算符 | 说明 | 范例 | 结果 |
---|---|---|---|
<<= | 左移后赋值 | C <<= 2 | C = C << 2 |
>>= | 右移后赋值 | C >>= 2 | C = C >> 2 |
&= | 按位与后赋值 | C &= 2 | C = C & 2 |
^= | 按位异或后赋值 | C ^= 2 | C = C ^ 2 |
|= | 按位或后赋值 | C |= 2 | C = C | 2 |
比较运算符语法
运算符 | 说明 | 范例 | 结果 |
---|---|---|---|
== | 相等 | 4 == 3 | false |
!= | 不等于 | 4 != 3 | true |
< | 小于 | 4 < 3 | false |
> | 大于 | 4 > 3 | true |
<= | 小于等于 | 4 <= 3 | false |
>= | 大于等于 | 4 >= 3 | true |
针运算符详解
语法
运算符 | 说明 | 范例 |
---|---|---|
& | 返回变量的地址 | &A |
* | 获取指针变量对应的值 | *A |
Go 格式化输出
格式化占位符教程
在 Golang 中,格式化的输入与输出,都需要使用到格式化占位符。比如获取用户输入的数据的函数 fmt.Scanf() ,格式化输出的函数 **fmt.Printf()**。
Go 语言的格式化占位符大概可分为普通占位符、**布尔** 占位符、**整数** 占位符、**浮点数** 和 复数 占位符、**字符串** 与 字节切片 占位符、**指针** 占位符以及其它标记等。
普通占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%v | 相应值的默认格式 | Printf(“%v”, webSite) | {HaiCoder} |
%+v | 打印结构体时,会添加字段名 | Printf(“%+v”, webSite) | {Name:HaiCoder} |
%#v | 相应值的Go语法表示 | Printf(“#v”, webSite) | main.WebSite{Name:”HaiCoder”} |
%T | 相应值的类型的Go语法表示 | Printf(“%T”, webSite) | main.WebSite |
%% | 字面上的百分号,并非值的占位符 | Printf(“%%”) | % |
布尔占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%t | true 或 false | Printf(“%t”, true) | true |
整数占位符
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%b | 二进制表示 | Printf(“%b”, 5) | 101 |
%c | 相应 Unicode 码点所表示的字符 | Printf(“%c”, 0x4E2D) | 中 |
%d | 十进制表示 | Printf(“%d”, 0x12) | 18 |
%o | 八进制表示 | Printf(“%d”, 10) | 12 |
%q | 单引号围绕的字符字面值,由Go语法安全地转义 | Printf(“%q”, 0x4E2D) | ‘中’ |
%x | 十六进制表示,字母形式为小写 a-f | Printf(“%x”, 13) | d |
%X | 十六进制表示,字母形式为大写 A-F | Printf(“%x”, 13) | D |
%U | Unicode格式:U+1234,等同于 “U+%04X” | Printf(“%U”, 0x4E2D) | U+4E2D |
浮点数和复数的组成部分(实部和虚部)
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%b | 无小数部分的,指数为二的幂的科学计数法,与 strconv.FormatFloat 的 ‘b’ 转换格式一致。例如 -123456p-78 | ||
%e | 科学计数法,例如 -1234.456e+78 | Printf(“%e”, 10.2) | 1.020000e+01 |
%E | 科学计数法,例如 -1234.456E+78 | Printf(“%e”, 10.2) | 1.020000E+01 |
%f | 有小数点而无指数,例如 123.456 | Printf(“%f”, 10.2) | 10.200000 |
%g | 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0) | 输出 Printf(“%g”, 10.20) | 10.2 |
%G | 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0) | 输出 Printf(“%G”, 10.20+2i) | (10.2+2i) |
字符串与字节切片
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%s | 输出字符串表示(string类型或[]byte) | Printf(“%s”, []byte(“Go语言”)) | Go语言 |
%q | 双引号围绕的字符串,由Go语法安全地转义 | Printf(“%q”, “Go语言”) | “Go语言” |
%x | 十六进制,小写字母,每字节两个字符 | Printf(“%x”, “golang”) | 686a6c61164a |
%X | 十六进制,大写字母,每字节两个字符 | Printf(“%X”, “golang”) | 686F6A616C61 |
指针
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
%p | 十六进制表示,前缀 0x | Printf(“%p”, &people) | 0x4a56a0 |
其它标记
占位符 | 说明 | 举例 | 输出 |
---|---|---|---|
+ | 总打印数值的正负号;对于%q(%+q)保证只输出ASCII编码的字符。 | Printf(“%+q”, “中文”) “\u4e2d\u6587” | |
- | 在右侧而非左侧填充空格(左对齐该区域) | ||
# | 备用格式:为八进制添加前导 0(%#o)为十六进制添加前导 0x(%#x)或 0X(%#X),为 %p(%#p)去掉前导 0x;如果可能的话,%q(%#q)会打印原始 (即反引号围绕的)字符串;如果是可打印字符,%U(%#U)会写出该字符的Unicode 编码形式(如字符 x 会被打印成 U+0078 ‘x’) | Printf(“%#U”, ‘中’) | U+4E2D |
’ ‘ | 空格)为数值中省略的正负号留出空白(% d) | 以十六进制(% x, % X)打印字符串或切片时,在字节之间用空格隔开 | |
0 | 填充前导的0而非空格;对于数字,这会将填充移到正负号之后 |
Go语言格式化占位符总结
Go语言的格式化占位符大概可分为普通占位符、布尔占位符、整数占位符、浮点数和复数占位符、字符串与字节切片占位符、指针占位符以及其它标记等。
Go 函数
可变参数
1 |
|
函数变量
在 Go 语言 中,**函数** 也是一种类型,可以和其他 数据类型 一样保存在 变量 中。
1、定义
1 |
|
说明
我们首先定义了一个 fun 的函数,接着我们声明了一个类型是 func 的函数变量 f,并且将 fun 函数赋值给变量 f。
2、定义
1 |
|
说明
我们首先定义了一个 fun 的函数,函数的 参数 为一个 int 类型 的参数, 函数的 返回值 为一个 string 类型的值,接着我们声明了同样类型的函数变量 f,并且将 fun 函数赋值给变量 f。
匿名函数
的匿名函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递。匿名函数定义:
1 |
|
匿名函数调用:
1 |
|
案例
1 |
|
函数闭包
概念:在 Go 语言 中闭包是引用了 自由变量 的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。
记忆功能: Go 语言中,被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆功能。
闭包记忆效应
Go 语言闭包记忆效应,实现累加
1 |
|
程序运行后,控制台输出如下:
defer
在我们编写 函数 时,经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时释放资源,**Go 语言** 设计者提供了 defer(延时机制)。
如果一个函数里面有多个 defer 语句,那么这些 defer 语句将会按照书写的逆序进行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
多个defer执行顺序
多个 defer 执行顺序是逆序执行
1 |
|
程序运行后,控制台输出如下:
备注:如果同一个 函数 中,既有 defer 语句,同时也有 return 语句,那么 defer 语句会在 return 语句的后面执行。
函数异常/错误
语法
1 |
|
函数中止 panic
在 Go 语言 中,如果我们的 函数 或者程序出现了非常严重的问题,遇到此类问题程序不应该继续往下运行了,那么我们就应该终止程序的执行。
在 Go 语言中,处理类似致命的错误的方法一般是通过 panic 的方式来终止我们程序的执行。
Recover
在 Go 语言 中,如果我们的 函数 或者程序出现了非常严重的问题,或者说我们的程序遇到了 panic 异常,此时我们的程序会终止运行。
但是,我们希望我们程序在发生错误后,我们能够做一些处理,保证程序可以继续运行,那么这时候,我们就需要使用异常恢复,即 recover。Golang 中的 recover 一般都是配套 defer 一起使用。
语法
1 |
|
说明
我们在 defer 中,使用 if 判断 ,如果程序出现了异常,那么我们使用 recover 尝试恢复,并且打印异常信息。
panic和recover使用原则
- defer 需要放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。
- recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
- 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用
Go语言panic和recover的关系
如果有 panic 但没有 recover,那么程序会宕机。如果有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。
(因此,panic不仅会关闭当前函数,还会导致整个程序的终止。为了避免程序因为panic而终止,可以在适当的地方使用recover函数来捕获panic,并进行错误处理或者恢复程序的正常执行。)
1 |
|
虽然 panic/recover 能模拟其他语言的异常机制,但并不建议在编写普通函数时也经常性使用这种特性。在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃。
main
我们在 main 函数 里面,通过 Go 语言 的 os.Args 可以获取命令行参数。
语法
1 |
|
flag 获取参数
Go 语言 程序的命令行,除了可以使用 os.Args 来获取,还可以通过 flag 包相关的 函数 来获取。
os.Args 获取方式只能根据索引一个个去遍历获取,但 flag 包提供的方式更为灵活,可以通过 key 和 value 的形式来获取。
语法
( key : default value — comment)
-u root “xxxx”
1 |
|
参数
参数 | 描述 |
---|---|
StringVar | 获取命令行参数的对应的类型,常用的有 IntVar,StringVar,BoolVar,Float64Var,Int64Var,Uint64Var,UintVar 等,分别用户获取 **int,string,bool,float64**,int64,uint64 和 uint 类型的参数。 |
&user | 所获取的值,存储的变量。 |
u | 命令行的 key。 |
root | 命令行的默认值。 |
账号 | 文字说明。 |
说明
我们分别通过了 flag.StringVar 和 flag.IntVar 解析了命令行的字符串类型的参数和 int 类型的参数。
方案二
通过 flag 包解析命令行参数
1 |
|
程序运行后,控制台输出如下:
我们在 main 函数里面解析命令行参数,最后运行的时候,我们发现,我们指定了参数 u 和 p ,因此最终的 user 和 password 参数的值就是我们指定的值,而我们没有指定的参数 h 和 P ,就使用的是默认值。
方案二
通过 flag 包解析命令行参数
1 |
|
程序运行后,控制台输出如下:
这一种方法类似第一种,同样实现了命令行参数的解析。
总结
flag 包提供的命令行参数解析的方式可以通过 key 和 value 的形式来获取。Go 语言解析命令行参数语法:
1 |
|
也可以使用:
1 |
|
init 函数
Go 语言 程序每一个源文件都可以包含一个 init 函数,该 函数 会在 main 函数之前执行,被 Go 语言框架调用,也就是说 init 会在 main 函数之前被调用。
如果一个文件同时包含 全局变量定义 ,init 函数和 main 函数,那么最先执行的是全局变量的定义,接着是 init 函数,最后执行的时候 main 函数
语法
1 |
|
说明
init 函数可以有 **返回值**,也可以没有返回值。
总结
Go 语言程序每一个源文件都可以包含一个 init 函数,该函数会在 main 函数之前执行,被 Go 语言框架调用,也就是说 init 会在 main 函数之前被调用。
如果一个文件同时包含全局变量定义 ,init 函数和 main 函数,那么最先执行的是全局变量的定义,接着是 init 函数,最后执行的时候 main 函数。
第二部分《数据结构 - 函数》
在程序开发的过程中,很多场景,我们需要保存很多的数据,或者说我们需要保存一组数据,使用普通的 数据类型 是不能满足我们需求的。
Go 语言 为开发者提供了内置的四种常用数据结构:**数组、切片(slice)、列表(list)** 以及 字典(map) 用来保存一组数据。
数组
数组初始化
1 |
|
创建数组时,即给数组设置初值
1 |
|
程序运行后,控制台输出如下:
我们创建了一个有三个元素,每个元素都是 string 类型的数组,定义数组的同时,我们直接给数组赋初值。最后,我们使用 print 打印数组的内容,我们发现,数组的内容就是我们所设置的三个值。
数组初始化
创建数组时,即给数组设置初值
1 |
|
程序运行后,控制台输出如下:
我们创建了一个有三个元素,每个元素都是 string 类型 的数组,定义数组的同时,我们直接给数组赋初值。
数组初始化
创建数组时,即给数组设置初值
1 |
|
程序运行后,控制台输出如下:
我们创建了一个有三个元素,每个元素都是 string 类型的数组,定义数组的同时,我们直接给数组赋初值。这里定义的数组,我们没有设置元素的个数,而是使用 …,表示是根据元素的个数直接推导。
数组初始化
创建数组时,即给数组设置初值
1 |
|
程序运行后,控制台输出如下:
我们创建了一个有三个元素,每个元素都是 string 类型的数组,定义数组的同时,我们直接给数组赋初值。这里,赋值我们使用的索引的方式。
索引为 0 的元素赋值为 “嗨客网”,索引为 1 的元素赋值为 “Hello”,索引为 2 的元素赋值为 “HaiCoder”。
== 比较
(比较数组内每一个 元素的值是否相同)
Go 语言的数组的比较,是使用 == 的方式,如果数组的元素个数不相同,那么不能比较数组。Go 语言数组比较语法:
1 |
|
多维
Go 语言的数组是支持多维的,如果是二维数组,那么数组的每一个元素都是一个一维数组,如果数组是三维数组,那么每一个元素都是一个二维数组。Go 语言二维数组定义:
1 |
|
Go 语言三维数组定义:
1 |
|
切片
切片的英文是 slice,**Golang** 中的切片是 数组 的一个引用,因此切片是引用类型,在进行传递时,遵守引用的传递机制。
切片的使用和数组类似,遍历切片、访问切片的元素和求切片的长度 len 与数组都一样。但切片的长度是可以变化的,不像数组是固定的,因此也可以说切片是一个可以动态变化的数组
定义
Golang 中的切片是数组的一个引用,因此切片是引用类型。Go 语言切片定义:
1 |
|
创建切片
总结
Go 语言创建切片有三种方式,分别为:从数组创建切片、使用 make 创建切片和指定数组创建切片。从数组创建切片语法:
1 |
|
使用 make 创建切片语法:
1 |
|
指定数组创建切片语法:
1 |
|
数组创建切片
语法
1 |
|
参数
参数 | 描述 |
---|---|
var | 定义切片变量使用的关键字。 |
sliceName | 切片变量名。 |
arr | 数组名。 |
index1 | 数组的开始索引。 |
index2 | 数组的结束索引。 |
使用make创建切片
语法
1 |
|
参数
参数 | 描述 |
---|---|
var | 定义切片变量使用的关键字。 |
sliceName | 切片变量名。 |
type | 切片的每一个元素的类型。 |
len | 切片的长度。 |
cap | 可选,切片的容量。 |
说明
创建一个切片 sliceName,该切片每一个元素的类型是 type,切片的长度为 len,容量为 cap。make 函数的第三个参数 cap,是可选参数,如果 cap 定义了值,那么 cap 的值必须大于等于 len。
指定数组创建切片
语法
1 |
|
说明
创建一个切片 sliceName,该切片的内容就是后面的数组元素。
案例
从数组创建切片
从已存在的数组的内容创建切片
1 |
|
程序运行后,控制台输出如下:
我们首先创建了一个有三个元素,每个元素都是 string 类型的数组,接着,我们使用数组索引从 1 开始到 3 结束的元素创建一个切片,并使用 print 打印切片内容。
使用make创建切片
使用 make 指定切片的长度和容量创建切片
1 |
|
程序运行后,控制台输出如下:
我们使用 make 函数,创建了一个长度为 3 容量为 3 的切片,接着使用索引的形式给切片赋值。
指定数组创建切片
使用 make 指定切片的长度和容量创建切片
1 |
|
程序运行后,控制台输出如下:
创建切片时,直接指定切片的内容。
访问切片
类似于数组,但是数组的 索引获取的切片,切片
Go 语言 访问 切片 元素可以使用索引的形式或者索引切片的形式。
访问单个切片元素
语法
1 |
|
参数
参数 | 描述 |
---|---|
sliceName | 切片变量名。 |
index | 要访问的切片的索引。 |
说明
获取切片 sliceName 的索引为 index 处的元素。
索引切片获取切片元素
语法
1 |
|
参数
参数 | 描述 |
---|---|
sliceName | 切片变量名。 |
index1 | 要访问的切片的开始索引。 |
index2 | 要访问的切片的结束索引。 |
说明
获取切片 sliceName 的索引为 index1 到 index2 处的元素,不包含索引为 index2 的元素。如果 index1 省略,那么就时默认值 0,如果 index2 省略,那么就是切片的长度。
map
初始化
1 |
|
参数
参数 | 描述 |
---|---|
var | 声明变量使用的关键字。 |
mapName | 声明的 map 变量的变量名。 |
map | 声明 map 变量的关键字。 |
keyType | map 的键的类型。 |
valueType | map 的值的类型。 |
len | map 的长度。 |
说明
语法
1 |
|
sync.Map
Go 语言 中 map 如果在并发读的情况下是线程安全的,如果是在并发写的情况下,则是线程不安全的。Golang 为我们提供了一个 sync.Map 是并发写安全的。
Golang 中的 map 的 key 和 value 的 类型 必须是一致的,但 sync.Map 的 key 和 value 不一定是要相同的类型,不同的类型也是支持的。
Go语言sync.Map特性
Go 语言 sync.Map 无须初始化,直接声明即可使用。
sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,**Store** 表示存储,**Load** 表示获取,**Delete** 表示删除。
使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true ,终止迭代遍历时,返回 false。
创建sync.Map
sync.Map 声明完之后,可以立即使用
1 |
|
程序运行后,控制台输出如下:
我们创建了一个有三个元素的 map,该 map 定义完之后可以直接使用, map 的三个 key 分别为 “Server”,“JavaScript” 和 “Db”。
new 和 make
Go 语言 中 new 和 make 是两个内置 **函数**,主要用来创建并分配内存。Golang 中的 new 与 make 的区别是 new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化。
new和make主要区别
- make 只能用来分配及初始化类型为 slice、map、chan 的数据,而 new 可以分配任意类型的数据。
- new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type。
- new 分配的空间被清零。make 分配空间后,会进行初始化。
new函数
语法
1 |
|
说明
new 函数只接受一个参数,这个参数是一个 Golang 的 **数据类型**,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是类型的零值。
make函数
语法
1 |
|
说明
make 也是用于内存分配的,make 函数的 t 参数必须是 chan(通道)、map(字典)、slice(切片)中的一个。
make 和 new 不同,它只用于 chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
案例
new分配内存空间
使用 new 为 int 类型的数据,分配空间
1 |
|
程序运行后,控制台输出如下:
首先,我们定义了一个 int指针类型的 变量 num,接着,我们使用 new 为变量 num 分配内存,接着为 num 变量赋值为 1024。
最后,我们使用 print 打印 num 的值和 num 变量的地址。
new自定义类型分配内存空间
使用 new 为自定义类型,分配空间
1 |
|
程序运行后,控制台输出如下:
首先,我们定义了一个结构体 Person,该结构体有两个字段,一个是 string 类型的 Name,另一个是 int 类型的 Age。
接着,我们在 main 函数里面定义一个 Person 指针类型的变量 p,并使用 new 为其分配内存,并为结构体变量的字段赋值。最后,我们打印结构体变量 p 。
make分配内存空间
使用 make 为切片分配空间
1 |
|
程序运行后,控制台输出如下:
首先,我们定义了一个 int 切片类型的变量 nums,接着使用 make 为其分配内存,并使用 append 为切片变量添加数据。
最后,我们使用 print 打印打印切片变量 nums 的值。
make分配内存空间
使用 make 为map分配空间
1 |
|
程序运行后,控制台输出如下:
首先,我们定义了一个 map 类型的变量 mNum,接着使用 make 为其分配内存。
Go语言中new和make区别总结
- make 只能用来分配及初始化类型为 slice、map、chan 的数据,而 new 可以分配任意类型的数据。
- new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type。
- new 分配的空间被清零。make 分配空间后,会进行初始化。
第三部分《Go 并发编程》
概念
进程、线程、协程
进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程
线程是指进程内的一个执行单元,也是进程内的可调度实体。线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。从技术的角度来说,“协程就是你可以暂停执行的函数”。协程拥有自己的寄存器上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
线程与进程的区别
地址空间
线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间。
资源拥有
进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源。
调度单位
线程是处理器调度的基本单位,但进程不是。进程与线程二者均可并发执行。
能否单独执行
每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
协程与线程的区别
- 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
- 线程进程都是同步机制,而协程则是异步。
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
- 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
- 协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的 CPU 资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
- 线程是协程的资源。协程通过 Interceptor 来间接使用线程这个资源。
协程 goruntine
概念
goroutine 可以看作是 协程 的 Go 语言 实现。
goroutine特点
goroutine 是协程的 Go 语言实现,它是语言原生支持的,相对于一般由库实现协程的方式,goroutine 更加强大,它的调度一定程度上是由 go 运行时(runtime)管理。
其好处之一是,当某 goroutine 发生阻塞时(例如同步IO操作等),会自动出让 CPU 给其它 goroutine。
goroutine是非常轻量级的,它就是一段代码,一个 函数 入口,以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。所以它非常廉价,我们可以很轻松的创建上万个 goroutine。
go运行时调度
默认的,所有 goroutine 会在一个原生线程里跑,也就是只使用了一个 CPU 核。在同一个原生线程里,如果当前goroutine 不发生阻塞,它是不会让出 CPU 时间给其他同线程的 goroutines 的。除了被系统调用阻塞的线程外,Go 运行库最多会启动 $GOMAXPROCS 个线程来运行 goroutine。
那么 goroutine 究竟是如何被调度的呢?我们从 go 程序启动开始说起。在go程序启动时会首先创建一个特殊的内核线程 sysmon,从名字就可以看出来它的职责是负责监控的,goroutine 背后的调度可以说就是靠它来搞定。
接下来,我们再看看它的调度模型,go 语言当前的实现是 N:M。即一定数量的用户线程映射到一定数量的 OS 线程上,这里的用户线程在 go 中指的就是 goroutine。go语言的调度模型需要弄清楚三个概念:M、P 和 G,如下图表示:
M 代表 OS 线程,G 代表 goroutine,P 的概念比较重要,它表示执行的上下文,其数量由 $GOMAXPROCS 决定,一般来说正好等于处理器的数量。M 必须和 P 绑定才能执行 G,调度器需要保证所有的 P 都有 G 执行,以保证并行度。如下图:
从图中我们可以看见,当前有两个 P,各自绑定了一个 M,并分别执行了一个 goroutine,我们还可以看见每个 P上还挂了一个 G 的队列,这个队列是代表私有的任务队列,它们实际上都是 runnable 状态的 goroutine。当使用go 关键字声明时,一个 goroutine 便被加入到运行队列的尾部。一旦一个 goroutine 运行到一个调度点,上下文便从运行队列中取出一个 goroutine,设置好栈和指令指针,便开始运行新的 goroutine。
goroutine的调度点
调用 runtime·gosched 函数
goroutine 主动放弃 CPU,该 goroutine 会被设置为 runnable 状态,然后放入一个全局等待队列中,而 P 将继续执行下一个 goroutine。使用 runtime·gosched 函数是一个主动的行为,一般是在执行长任务时又想其它goroutine 得到执行的机会时调用。
调用runtime·park函数
goroutine 进入 waitting 状态,除非对其调用 runtime·ready 函数,否则该 goroutine 将永远不会得到执行。而 P 将继续执行下一个 goroutine。使用 runtime·park 函数一般是在某个条件如果得不到满足就不能继续运行下去时调用,当条件满足后需要使用 runtime·ready 以唤醒它(这里唤醒之后是否会加入全局等待队列还有待研究)。像 channel 操作,定时器中,网络 poll 等都有可能 park goroutine。
慢系统调用
这样的系统调用会阻塞等待,为了使该 P 上挂着的其它 G 也能得到执行的机会,需要将这些 goroutine 转到另一个 OS 线程上去。具体的做法是:首先将该 P 设置为 syscall 状态,然后该线程进入系统调用阻塞等待。之前提到过的 sysmom 线程会定期扫描所有的 P,发现一个 P 处于了 syscall 的状态,就将 M 和 P 分离(实际上只有当 Syscall 执行时间超出某个阈值时,才会将 M 与 P 分离)。RUNTIME 会再分配一个 M 和这个 P 绑定,从而继续执行队列中的其它 G。而当之前阻塞的 M 从系统调用中返回后,会将该 goroutine 放入全局等待队列中,自己则sleep 去。
调度点的情况说清楚了,但整个模型还并不完整。我们知道当使用 go 去调用一个函数,会生成一个新的goroutine 放入当前P的队列中,那么什么时候生成别的 OS 线程,各个 OS 线程又是如何做负载均衡的呢?
当 M 从队列中拿到一个可执行的 G 后,首先会去检查一下,自己的队列中是否还有等待的 G,如果还有等待的 G,并且也还有空闲的 P,此时就会通知 runtime 分配一个新的 M(如果有在睡觉的OS线程,则直接唤醒它,没有的话则生成一个新的OS线程)来分担任务。
如果某个 M 发现队列为空之后,会首先从全局队列中取一个 G 来处理。如果全局队列也空了,则会随机从别的 P那里直接截取一半的队列过来(偷窃任务),如果发现所有的 P 都没有可供偷窃的 G 了,该 M 就会陷入沉睡。
整个调度模型大致就是这样子了,和所有协程的调度一样,在响应时间上,这种协作式调度是硬伤。很容易导致某个协程长时间无法得到执行。但总体来说,它带来的好处更加让人惊叹。
初始化
定义
在 Go 程序中使用 go 关键字为一个函数创建一个 goroutine。在 golang 中,创建 goroutine 有两种方法,分别为:使用普通函数创建和使用匿名函数创建。普通函数创建 goroutine 语法:
1 |
|
匿名函数创建 goroutine 语法:
1 |
|
普通函数创建goroutine
使用普通函数,加上 go 关键字,创建 goroutine
1 |
|
程序运行后,控制台输出如下:
匿名函数创建goroutine
使用匿名函数,加上 go 关键字,创建 goroutine
1 |
|
程序运行后,控制台输出如下:
等待goruntine
Go 语言 中要等待 goroutine 的结束,可以使用 sync.WaitGroup
相关的操作,首先,使用 wg.Add
方法增加需要等到的协程的数量,然后没执行完一个协程,使用 wg.Done
表明协程结束,最后使用 wg.Wait
等到所有的协程结束。
Go语言等待协程结束
语法
1 |
|
说明
首先,使用 wg.Add
设置我们需要等到的协程的数量,接着,每执行完一个协程之后,使用 wg.Done
表明协程结束,最后使用 wg.Wait
等待所有的协程结束。
案例
使用 sync.WaitGroup 等待协程结束
1 |
|
程序运行后,控制台输出如下:
首先,我们定义了一个 sync.WaitGroup
类型的 全局变量 wg
,同时,我们创建了一个 printInfo **函数**,该函数接受一个 string 类型的参数,没有任何返回值,在该函数里,我们通过 for 循环执行两次输出语句,每次输出完之后,使用 Sleep 函数实现睡眠一秒钟。
同时,在 printInfo 函数里,我们使用 defer 实现,在该函数执行完毕时,调用 wg.Done()
表明一个协程执行结束。
最后,在 main 函数,我们首先使用 wg.Add
设置我们需要等到的协程数,为 1,接着使用 go 创建一个协程,并使用 wg.Wait
等待协程的结束。
我们看到,程序输出了两次之后,程序退出了,这里,我们使用了 sync.WaitGroup
的相关操作,实现了等待所有的协程退出。
等待多个goroutine结束
使用 sync.WaitGroup 等待多个协程结束
1 |
|
程序运行后,控制台输出如下:
sync
锁的作用就是某个 协程 (线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。
Go 语言 中的 sync 包提供了两种锁类型,分别为:sync.Mutex 和 sync.RWMutex,即互斥锁和 **读写锁**。
1 |
|
案例
1 |
|
程序运行,如下:
chan
概述
chan 是 Go 语言 中的一个核心 类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯。
chan 的本质是一个队列,且 chan 是线程安全的, 也就是自带 锁 的功能。
声明chan
语法
1 |
|
参数
参数 | 描述 |
---|---|
var | 声明变量使用的关键字。 |
chanName | 管道变量名。 |
chan | 声明管道变量类型使用的关键字。 |
chanType | 管道变量的具体类型。 |
说明
我们声明了一个 chan 类型的 变量 ,变量名为 chanName,变量的类型为 chan chanType。注意 chanName 的变量类型为 chan chanType。
此时,chanName 的值为 nil,声明之后需要使用 make 创建完毕之后,才可以使用。
创建通道
通道是引用类型,需要使用 make 进行创建,格式如下:
1 |
|
使用 make 创建一个类型为 chan chanType 的管道 chanName。
案例
创建管道
使用 make 创建管道
1 |
|
程序运行后,控制台输出如下:
首先,我们定义了一个 chan int
类型的管道 变量 chanInt ,接着,我们使用 print 打印该管道,发现该管道为 nil。
接着,我们使用 make 创建该管道,创建完之后,再次打印该管道,此时管道已经不是 nil,因此,我们在使用管道之前一定要先 make,后使用。
初始化(有/无)缓冲区
无缓冲通道和带缓冲通道区别
无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换,而有缓冲的通道没有这种保证。
在无缓冲通道的基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。带缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。
无缓冲通道保证收发过程同步。而无缓冲是异步的收发过程,因此效率可以有明显的提升。
无缓冲区
无缓冲的通道是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。
如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。Go 语言无缓冲 channel 创建:
1 |
|
有缓冲区
带缓冲的通道是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。Go 语言带缓冲 channel 创建:
1 |
|
chan发送数据
语法
1 |
|
参数
参数 | 描述 |
---|---|
msg_chan | 管道变量。 |
msg | 要发送的数据。 |
说明
chan 发送数据的语法格式为 chan 变量 <- 加上要发送的数据,因此,我们这里是使用 chan 类型的 变量 msg_chan 发送一个 字符串 类型的数据。
chan接收数据
语法
1 |
|
参数
参数 | 描述 |
---|---|
msg_chan | 管道变量。 |
msg | 要接受的数据存放的变量。 |
说明
chan 接收数据的语法格式为 待接收的变量 <- chan 变量,因此,我们这里是使用的变量 msg 来接受 chan 类型的变量 msg_chan 的数据。
chan接收数据
语法
1 |
|
参数
参数 | 描述 |
---|---|
msg_chan | 管道变量。 |
msg | 要接受的数据存放的变量。 |
ok | 管道是否已经关闭。 |
说明
msg 为管道接受的数据,ok 表示管道是否 关闭 或者是否为空。
案例
Go语言chan发送字符串数据
使用 Go 语言的 chan 发送一个字符串类型的数据
1 |
|
程序运行后,控制台输出如下:
Go语言chan关闭
语法
1 |
|
参数
参数 | 描述 |
---|---|
msg_chan | 需要关闭的管道。 |
说明
当我们的管道,不需要使用时,需要使用 close 关闭该管道。
Go语言chan是否关闭
语法
1 |
|
说明
我们在使用 变量 接受管道返回的数据后,第二个 bool 类型的返回值表示管道是否关闭,如果为 false,则表明管道已经关闭。
select多路复用
概述
在 Golang 程序中,使用 通道 时,如果想同时接收多个通道的数据是一件很困难的事情。通道在接收数据时,如果没有数据可以接收将会发生阻塞。
Go 语言中提供了 select 关键字,可以同时响应多个通道的操作。select 的用法与 switch 语句非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。
但 select 又与 switch 不同,select 有比较多的限制,其中最重要的一条限制就是每个 case 语句里必须是一个 IO 操作。
并且,select 后面并不带判断条件,而是直接去查看 case 语句。每个 case 语句都必须是一个面向 channel 的操作。当 select 里面有多个 case 都满足条件出发时,则 select 会随机选取一个 case 执行。
Go语言select使用
语法
1 |
|
说明
当操作 operator1 触发时,则执行 statement1 对应的语句,当操作 operator2 触发时,则执行 statement2 对应的语句,这里的操作可以有任意多个。
如果,以上所有的 case 都不满足,那么就执行 default 语句里面的逻辑。
案例
select监听多个channel
select 执行 default case
1 |
|
程序运行后,控制台输出如下:
首先,我们使用 make 创建了两个 无缓冲 的管道 chStr1 和 chStr2,接着, 使用 go 创建了两个个协程,分别为 pythonSender 和 golangSender。
在两个子协程里,我们都使用 sleep 让协程先等待一定的时间,接着,使用 channel 分别都发送一个 字符串 消息。
最后,在 main 协程里,我们使用 select 监听管道 chStr1 和 chStr2 发送的消息,并使用 default case,如果没有接收到数据,那么就执行 default 语句。
最后,程序运行结果,我们发现程序执行了 default 的 case,因为,我们的 select 从第一个 case 依次往下判断,前面两个子协程都使用 sleep 等到了几秒后才执行,因此前面两个 case 不会立刻接收到数据。所以,最后程序执行到 default case 后,程序退出。
select超时处理
在 Golang 中,当我们使用 select 监听多个 channel 时,如果一直没有满足的 case 触发,并且 select 没有 default case 时,那么 select 就会永久的等待,那么如果等待的时间过长,select 很可能就一直阻塞程序。
在 select 中,我们可以使用 time.After
来实现 select 的超时控制,同时,我们还可以使用 break 语句,来结束 select 语句,就像我们之前结束 for 循环 一样。
语法
1 |
|
说明
当 operator1 和 operator2 这两个 case 都不满足时,且等待 second 秒后,会触发 time.After
case,通过该 case 可以避免 select 一直等到而阻塞进程。
第四部分《反射》
使用反射获取变量基本类型的语法:
1 |
|
使用反射获取变量值的基本语法:
1 |
|
1 |
|