-

第一个程序 hello world

1
2
3
4
5
6
package main
import "fmt"

func main() {
fmt.Println("Hello World")
}

go run hello.go 执行程序代码

基础语法,初识函数,基本类型

package

我们看到程序的第一行是 package main,在Go语言中,包(Package) 是一种用于组织代码的机制,用于将相关的函数、类型和变量等组织在一起,以便于模块化开发和代码复用。包的使用能够使程序结构更加清晰、可维护性更高,同时也是Go语言强调的一项重要特性

func

在go里面,当函数名开头是小写的话,意思这个函数只能在当前包用,是私有的,只能在包的内部使用,如果是大写,则公有,可以导出食用。

基本类型

字符串

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(“)中的内容。

1
2
3
4
s1 := "aaa"
fmt.Println(s1)

> aaa
字符

组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:

1
2
var a := '中'
var b := 'x'

字符类型在 go 中是 rune,rune类型实际是一个int32,可以表示一个 unicode 码点

1
2
3
4
s1 := "a"
fmt.Println(s1)

> 97
整形

整型分为以下两个大类: 按长度分为:int8int16int32int64对应的无符号整型:uint8uint16uint32uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

浮点型

Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

布尔值

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)false(假)两个值。

1
2
3
4
5
6
7
注意:

布尔类型变量的默认值为false。

Go 语言中不允许将整型强制转换为布尔型.

布尔型无法参与数值运算,也无法与其他类型进行转换。

变量声明

1
2
3
4
5
6
7
8
9
10
var s1 string
s1 = "aaa"

s2 := "aaa"

var s3, s4 string
s5,s6 := "G","o"

// 使用匿名变量来忽略函数的返回值、临时存储值,以及在需要忽略某些返回值的情况下使用。请注意,匿名变量不能被重新赋值或在其他地方使用,作用仅限于被声明的位置。
_, _ = 10, 20

运算符

Go 语言内置的运算符有:

1
2
3
4
5
算术运算符
关系运算符
逻辑运算符
位运算符
赋值运算符
算数运算符
运算符 描述
+ 相加
- 相减
* 相乘
/ 相除
% 求余

注意: ++(自增)和–(自减)在Go语言中是单独的语句,并不是运算符。

关系运算符
运算符 描述
== 检查两个值是否相等,如果相等返回 True 否则返回 False。
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False。
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False。
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False。
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。
逻辑运算符
运算符 描述
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
ll 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
! 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。
位运算符

位运算符对整数在内存中的二进制位进行操作。

运算符 描述
& 参与运算的两数各对应的二进位相与。(两位均为1才为1)
l 参与运算的两数各对应的二进位相或。(两位有一个为1就为1)
^ 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1)
<< 左移n位就是乘以2的n次方。“a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
>> 右移n位就是除以2的n次方。“a>>b”是把a的各二进位全部右移b位。
赋值运算符
运算符 描述
= 简单的赋值运算符,将一个表达式的值赋给一个左值
+= 相加后再赋值
-= 相减后再赋值
*= 相乘后再赋值
/= 相除后再赋值
%= 求余后再赋值
<<= 左移后赋值
>>= 右移后赋值
&= 按位与后赋值
l= 按位或后赋值
^= 按位异或后赋值

数组Array

Golang Array和以往认知的数组有很大不同。数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。

1
2
3
4
arrs := [3]int{1, 2, 3}
arr := [...]int{1, 2}
c := [5]int{3: 100, 4: 500}
d := [2][3]int{{1, 2, 3}, {4, 5, 6}}

长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。

切片Slice

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案,所以可以说切片是一个动态数组

1
2
3
4
5
s1 := []int{1, 2, 3}
s1 = append(s1, 1234)

s3 := make([]int, 5)
fmt.Println(s1,s3)

指针

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。

取变量指针的语法如下:

1
2
3
4
ptr := &v    // v的类型为T

v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
1
2
3
4
5
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018

指针

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

1
2
3
4
5
6
7
8
9
10
11
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)

type of b:*int
type of c:int
value of c:10

总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:\

  1. 对变量进行取地址(&)操作,可以获得这个变量的指针变量
  2. 指针变量的值是指针地址。
  3. 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func modify1(x int) {
x = 100
}

func modify2(x *int) {
*x = 100
}

func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
空指针
  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil
  • 空指针的判断
1
2
3
4
5
6
7
8
var p *string
fmt.Println(p)
fmt.Printf("p的值是%s/n", p)
if p != nil {
fmt.Println("非空")
} else {
fmt.Println("空值")
}

Map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

map定义

Go语言中 map的定义语法如下

1
map[KeyType]ValueType

其中

1
2
KeyType:表示键的类型。
ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

1
make(map[KeyType]ValueType, [cap])

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

map基本使用

map中的数据都是成对出现的,map的基本使用示例代码如下:

1
2
3
4
5
6
7
8
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
1
2
3
map[小明:100 张三:90]
100
type of a:map[string]int

map也支持在声明的时候填充元素,例如:

1
2
3
4
5
scoreMap := map[string]int{
"A": 100,
"B": 60,
}
fmt.Println(scoreMap)
判断某个键是否存在

Go语言中有个判断map中键是否存在的特殊写法,格式如下:

1
value, ok := map[key]

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	value,ok := scoreMap["A"]
if ok{
fmt.Println(value)
} else {
fmt.Println("not exist")
}

values,okk := scoreMap["C"]
if okk{
fmt.Println(values)
} else {
fmt.Println("not exist")
}

100
not exist
map的遍历

Go语言中使用for range遍历map。

1
2
3
for k, v := range scoreMap {
fmt.Println(k,v)
}

或者只遍历 key

1
2
3
for k := range scoreMap {
fmt.Println(k)
}
使用delete()函数删除键值对

使用delete()内建函数从map中删除一组键值对,delete()函数的格式如下:

1
delete(map, key)

其中,

1
2
map:表示要删除键值对的map
key:表示要删除的键值对的键

结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

结构体的定义

使用type和struct关键字来定义结构体,具体代码格式如下:

1
2
3
4
5
type 类型名 struct {
字段名 字段类型
字段名 字段类型

}

其中:

1
2
3
1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
3.字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Person(人)结构体,代码如下:

1
2
3
4
5
type person struct {
name string
city string
age int8
}

同样类型的字段也可以写在一行,

1
2
3
4
type person1 struct {
name, city string
age int8
}

这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型

结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。

1
var 结构体实例 结构体类型
基本实例化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type person struct {
name string
city string
age int8
}

func main() {
var p1 person
p1.name = "pprof.cn"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={pprof.cn 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"pprof.cn", city:"北京", age:18}
}

我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。

指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

1
2
3
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

1
2
3
4
5
6
7
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "博客"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", city:"成都", age:30}

p3.name = “博客”其实在底层是(*p3).name = “博客”,这是Go语言帮我们实现的语法糖。

占位符

通用型

*printf系列函数都支持format格式化参数,在这里我们按照占位符将被替换的变量类型划分,方便查询和记忆。

占位符 说明
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 打印值的类型
%% 百分号
整形
占位符 说明
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于”U+%04X”
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
布尔型
占位符 说明
%t true或false
浮点数与复数
占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
字符串和[]byte
占位符 说明
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f
%X 每个字节用两字符十六进制数表示(使用A-F)
指针
占位符 说明
%p 表示为十六进制,并加上前导的0x
宽度标识符
占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0
其他 flag
占位符 说明
’+’ 总是输出数值的正负号;对%q(%+q)会生成全部是ASCII字符的输出(通过转义);
’ ‘ 对数值,正数前加空格而负数前加负号;对字符串采用%x或%X时(% x或% X)会给各打印的字节之间加空格
’-’ 在输出右边填充空白而不是默认的左边(即从默认的右对齐切换为左对齐);
’#’ 八进制数前加0(%#o),十六进制数前加0x(%#x)或0X(%#X),指针去掉前面的0x(%#p)对%q(%#q),对%U(%#U)会输出空格和单引号括起来的go字面值;
‘0’ 使用0而不是空格填充,对于数值类型会把填充的0放在正负号后面;

流程控制

条件语句if

条件语句switch

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。 Golang switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。

1
2
3
4
5
6
7
8
switch var1 {
case val1:
...
case val2:
...
default:
...
}

循环语句for

1
2
3
4
5
6
7
8
9
10
11
12
13
for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
println(s[i])
}

n := len(s)
for n > 0 { // 替代 while (n > 0) {}
println(s[n]) // 替代 for (; n > 0;) {}
n--
}

for { // 替代 while (true) {}
println(s) // 替代 for (;;) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

for a := 0; a < 10; a++ {
fmt.Printf("a 的值为: %d\n", a)
}

for a < b {
a++
fmt.Printf("a 的值为: %d\n", a)
}

for i,x:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}

循环控制Goto、Break、Continue

  1. 三个语句都可以配合标签(label)使用
  2. 标签名区分大小写,定以后若不使用会造成编译错误
  3. continue、break配合标签(label)可用于多层循环跳出
  4. goto是调整执行位置,与continue、break配合标签(label)的结果并不相同

函数

golang函数特点

1
2
3
4
5
6
7
8
9
10
• 无需声明原型。
• 支持不定 变参。
• 支持多返回值。
• 支持命名返回参数。
• 支持匿名函数和闭包。
• 函数也是一种类型,一个函数可以赋值给变量。

• 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
• 不支持 重载 (overload)
• 不支持 默认参数 (default parameter)。

函数声明

函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。

函数可以没有参数或接受多个参数。

注意类型在变量名之后 。

当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。

函数可以返回任意数量的返回值。

使用关键字 func 定义函数,左大括号依旧不能另起一行。

1
2
3
4
5
func test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, fmt.Sprintf(s, n)
}

函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。

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

import "fmt"

func test(fn func() int) int {
return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string

func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}

func main() {
s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。

s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)

println(s1, s2)
}

输出结果:

1
100 10, 20

有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。

1
2
package math
func Sin(x float64) float //implemented in assembly language

匿名函数

1
2
3
4
func main() {
func(a, b int) {
fmt.Println(a + b)
}(1, 2)

初始化函数

1
2
3
func init()  {
fmt.Println(123)
}

Init 函数在 main 函数执行之前执行,可定义多个 init 函数,按顺序依次执行

可变参数函数

1
2
3
4
5
6
7
func add(nums ...int) int {
total := 0
for _, v := range nums {
total += v
}
return total
}

函数闭包

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

import (
"fmt"
)

func a() func() int {
i := 0
b := func() int {
i++
fmt.Println(i)
return i
}
return b
}

func main() {
c := a()
c()
c()
c()

a() //不会输出i
}

1
2
3

闭包复制的是原对象指针,这就很容易解释延迟引用现象。

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

import "fmt"

func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)

return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}

func main() {
f := test()
f()
}

输出:

1
2
x (0xc42007c008) = 100
x (0xc42007c008) = 100

在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。

课后作业 ====》Base64 编码解码实现

这里编码解码函数直接用 dongle

一个轻量级、语义化、对开发者友好的 golang 编码解码、加密解密和签名验签库

这边我们选择用 go mod 对导入 dongle 库

  1. go mod init 项目目录 					
    
    1
    2
    3

    2. ```shell
    go get -u github.com/golang-module/dongle

Base64 编码解码

1
2
dongle.Encode.FromString(text).ByBase64().ToString()
dongle.Decode.FromString(cipher).ByBase64().ToString()

并发介绍

进程和线程

1
2
3
A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
C. 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

并发和并行

1
2
A. 多线程程序在一个核的cpu上运行,就是并发。
B. 多线程程序在多个核的cpu上运行,就是并行。

并发

并发

并行

并行

协程和线程

1
2
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。

goroutine 只是由官方实现的超级”线程池”。

每个实力4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。

并发不是并行:

并发主要由切换时间片来实现”同时”运行,并行则是直接利用多核实现多线程的运行,go可以设置使用核数,以发挥多核计算机的能力。

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

Goroutine

在java/c++中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务,同时需要自己去调度线程执行任务并维护上下文切换,这一切通常会耗费程序员大量的心智。那么能不能有一种机制,程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行呢?

Go语言中的goroutine就是这样一种机制,goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine去执行这个函数就可以了,就是这么简单粗暴。

使用goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

启动单个goroutine

启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。

1
2
3
4
5
6
7
func hello()  {
fmt.Println("Hello World")
}
func main() {
go hello()
fmt.Println("Hello World in main")
}

发现 hello 函数好像没有执行,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。

当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。

所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了。

1
time.Sleep(time.Second)

首先为什么会先打印Hello World in main是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。

启动多个goroutine

在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。让我们再来一个例子: (这里使用了sync.WaitGroup来实现goroutine的同步)

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

import (
"fmt"
"sync"
)

var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello World")
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
fmt.Println("Hello World in main")
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

⚠️注意

  • 如果主协程退出了,其他任务还执行吗(运行下面的代码测试一下吧)
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 main() {
// 合起来写
go func() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
if i == 2 {
break
}
}
}
1
2
3
4
main goroutine: i = 1
new goroutine: i = 1
new goroutine: i = 2
main goroutine: i = 2

goroutine与线程

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。

goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • 1.G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • 2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • 3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

runtime包

runtime.Gosched()

让出CPU时间片,重新等待安排任务(大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤)

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

func main() {
// 协程
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("Hello")

// 主协程
for i := 0; i < 2; i++ {
// 切一下,再分配任务
runtime.Gosched()
fmt.Println("hello")
}
}

runtime.Goexit()

退出当前协程(一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了)

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

func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 结束协程
runtime.Goexit()
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
}
}

runtime.GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:

Go语言中的操作系统线程和goroutine的关系:

  • 1.一个操作系统线程对应用户态多个goroutine。
  • 2.go程序可以同时使用多个操作系统线程。
  • 3.goroutine和OS线程是多对多的关系,即m:n。

Channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel类型

channel是一种类型,一种引用类型。声明通道类型的格式如下:

1
2
3
4
  var 变量 chan 元素类型
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

创建channel

通道是引用类型,通道类型的空值是nil。

1
2
var ch chan int
fmt.Println(ch) // <nil>

声明的通道后需要使用make函数初始化之后才能使用。

创建channel的格式如下:

1
make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的。

举几个例子:

1
2
3
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

现在我们先使用以下语句定义一个通道:

1
ch := make(chan int)
发送

将一个值发送到通道中。

1
ch <- 10 // 把10发送到ch中
接收

从一个通道中接收值。

1
2
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
关闭

我们通过调用内置的close函数来关闭通道。

1
close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

1
2
3
4
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。

无缓冲的通道

img

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:

1
2
3
4
5
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

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

goroutine 1 [chan send]:
main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54

为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

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

import "fmt"

func recv(ch chan int) {
ret := <-ch
fmt.Println(ret, "接收成功")
}
func main() {
// 定义一个通道
ch := make(chan int)
go recv(ch)
ch <- 10
fmt.Println(ch, "发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。

img

我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:

1
2
3
4
5
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

close()

可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)

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

import "fmt"

func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main结束")
}

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

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
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}

func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}

其中,

1
2
1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
2.<-chan int是一个只能接收的通道,可以接收但是不能发送。

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

通道总结

channel常见的异常总结,如下图:

通道总结

注意:关闭已经关闭的channel也会引发panic。

定时器

Timer

时间到了,执行只执行1次

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

import (
"fmt"
"time"
)

func main() {
timer := time.NewTimer(time.Second)
fmt.Println("Timer started...")

<-timer.C // 等待定时器触发
fmt.Println("Timer expired!")
}

Ticker

时间到了,多次执行

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

import (
"fmt"
"time"
)

func main() {
ticker := time.NewTicker(1 * time.Second)
i := 0
go func() {
for {
i++
fmt.Println(<-ticker.C)
if i == 5 {
ticker.Stop()
}
}
}()
for {

}
}

select

select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现:

select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"time"
)

func test1(ch chan string) {
time.Sleep(time.Second * 5)
ch <- "test1"
}
func test2(ch chan string) {
time.Sleep(time.Second * 2)
ch <- "test2"
}

func main() {
// 2个管道
output1 := make(chan string)
output2 := make(chan string)
// 跑2个子协程,写数据
go test1(output1)
go test2(output2)
// 用select监控
select {
case s1 := <-output1:
fmt.Println("s1=", s1)
case s2 := <-output2:
fmt.Println("s2=", s2)
}
}
  • 如果多个channel同时ready,则随机选择一个执行
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 main() {
// 创建2个管道
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
go func() {
//time.Sleep(2 * time.Second)
int_chan <- 1
}()
go func() {
string_chan <- "hello"
}()
select {
case value := <-int_chan:
fmt.Println("int:", value)
case value := <-string_chan:
fmt.Println("string:", value)
}
fmt.Println("main结束")
}

并发安全和锁

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被车厢里的人竞争。

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

import (
"fmt"
"sync"
)

var x int64
var wg sync.WaitGroup

func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func sub() {
for i := 0; i < 5000; i++ {
x = x - 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Println(x)
}

上面的代码中我们开启了两个goroutine去加减x的值,这两个goroutine在访问和修改x变量的时候就会存在数据竞争,导致最后的结果不是 0。

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的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
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"sync"
)

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
for i := 0; i < 5000; i++ {
lock.Lock() //加锁
x = x + 1
lock.Unlock() //解锁
}
wg.Done()
}
func sub() {
for i := 0; i < 5000; i++ {
lock.Lock() //加锁
x = x - 1
lock.Unlock() //解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go sub()
wg.Wait()
fmt.Println(x)
}

方法

Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

1
2
3
4
5
• 只能为当前包内命名类型定义方法。
• 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
• 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
• 不支持方法重载,receiver 只是参数签名的组成部分。
• 可用实例 value 或 pointer 调用全部方法,编译器自动转换。

一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。

所有给定类型的方法属于该类型的方法集

方法定义

1
2
3
func (recevier type) methodName(参数列表)(返回值列表){}

参数和返回值可以省略
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
package main

type Test struct{}

// 无参数、无返回值
func (t Test) method0() {

}

// 单参数、无返回值
func (t Test) method1(i int) {

}

// 多参数、无返回值
func (t Test) method2(x, y int) {

}

// 无参数、单返回值
func (t Test) method3() (i int) {
return
}

// 多参数、多返回值
func (t Test) method4(x, y int) (z int, err error) {
return
}

// 无参数、无返回值
func (t *Test) method5() {

}

// 单参数、无返回值
func (t *Test) method6(i int) {

}

// 多参数、无返回值
func (t *Test) method7(x, y int) {

}

// 无参数、单返回值
func (t *Test) method8() (i int) {
return
}

// 多参数、多返回值
func (t *Test) method9(x, y int) (z int, err error) {
return
}

func main() {}

下面定义一个结构体类型和该类型的一个方法:

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

import "fmt"

type User struct {
name string
age int
}

// 方法
func (u User) Notify() {
fmt.Printf("name: %v, age: %v\n", u.name, u.age)
}
func main() {
// 创建对象
user := User{"Tom", 18}
user.Notify()

user2 := User{"Jerry", 20}
user3 := &user2
user3.Notify()

}
1
2
name: Tom, age: 18
name: Jerry, age: 20

注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作。

方法不过是一种特殊的函数,只需将其还原,就知道 receiver T 和 *T 的差别。

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 Data struct {
x int
}

func (self Data) ValueTest() { // func ValueTest(self Data);
fmt.Printf("Value: %p\n", &self)
}

func (self *Data) PointerTest() { // func PointerTest(self *Data);
fmt.Printf("Pointer: %p\n", self)
}

func main() {
d := Data{}
p := &d
fmt.Printf("Data: %p\n", p)

d.ValueTest() // ValueTest(d)
d.PointerTest() // PointerTest(&d)

p.ValueTest() // ValueTest(*p)
p.PointerTest() // PointerTest(p)
}
1
2
3
4
5
Data: 0x1400000e108
Value: 0x1400000e118
Pointer: 0x1400000e108
Value: 0x1400000e120
Pointer: 0x1400000e108

普通函数与方法的区别

1.对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。

2.对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。

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

//普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)

import "fmt"

// 1.普通函数
// 接收值类型参数的函数
func valueIntTest(a int) int {
return a + 10
}

// 接收指针类型参数的函数
func pointerIntTest(a *int) int {
return *a + 10
}

func structTestValue() {
a := 2
fmt.Println("valueIntTest:", valueIntTest(a))
//函数的参数为值类型,则不能直接将指针作为参数传递
//fmt.Println("valueIntTest:", valueIntTest(&a))
//compile error: cannot use &a (type *int) as type int in function argument

b := 5
fmt.Println("pointerIntTest:", pointerIntTest(&b))
//同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
//fmt.Println("pointerIntTest:", pointerIntTest(b))
//cannot use b (variable of type int) as *int value in argument to pointerIntTest
}

//2.方法
type PersonD struct {
id int
name string
}

//接收者为值类型
func (p PersonD) valueShowName() {
fmt.Println(p.name)
}

//接收者为指针类型
func (p *PersonD) pointShowName() {
fmt.Println(p.name)
}

func structTestFunc() {
//值类型调用方法
personValue := PersonD{101, "hello world"}
personValue.valueShowName()
personValue.pointShowName()

//指针类型调用方法
personPointer := &PersonD{102, "hello golang"}
personPointer.valueShowName()
personPointer.pointShowName()

//与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
}

func main() {
structTestValue()
structTestFunc()
}
1
2
3
4
5
6
valueIntTest: 12
pointerIntTest: 15
hello world
hello world
hello golang
hello golang

匿名字段

Golang匿名字段 :可以像字段成员那样访问匿名字段方法,编译器负责查找。

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"

type User struct {
id int
name string
}

type Manager struct {
User
}

func (self *User) ToString() string { // receiver = &(Manager.User)
return fmt.Sprintf("User: %p, %v", self, self)
}

func main() {
m := Manager{User{1, "Tom"}}
fmt.Printf("Manager: %p\n", &m)
fmt.Println(m.ToString())
}
1
2
Manager: 0x140000a4018
User: 0x140000a4018, &{1 Tom}

通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 “override”。

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

import "fmt"

type User struct {
id int
name string
}

type Manager struct {
User
title string
}

func (self *User) ToString() string {
return fmt.Sprintf("User: %p, %v", self, self)
}

func (self *Manager) ToString() string {
return fmt.Sprintf("Manager: %p, %v", self, self)
}

func main() {
m := Manager{User{1, "Tom"}, "Administrator"}

fmt.Println(m.ToString())

fmt.Println(m.User.ToString())
}
1
2
Manager: 0x14000194090, &{{1 Tom} Administrator}
User: 0x14000194090, &{1 Tom}

方法集

Golang方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则。

1
2
3
4
5
• 类型 T 方法集包含全部 receiver T 方法。
• 类型 *T 方法集包含全部 receiver T + *T 方法。
• 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
• 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
• 不管嵌入 T 或 *T,*S 方法集总是包含 T + *T 方法。

用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。

Go 语言中内部类型方法集提升的规则:

表达式

Golang 表达式 :根据调用者不同,方法分为两种表现形式:

1
instance.method(args...) ---> <type>.func(instance, args...)

前者称为 method value,后者 method expression。

两者都可像普通函数那样赋值和传参,区别在于 method value 绑定实例,而 method expression 则须显式传参。

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

import "fmt"

type User struct {
id int
name string
}

func (self *User) Test() {
fmt.Printf("%p, %v\n", self, self)
}

func main() {
u := User{1, "Tom"}
u.Test()

mValue := u.Test
mValue() // 隐式传递 receiver

mExpression := (*User).Test
mExpression(&u) // 显式传递 receiver
}

错误控制

返回错误值

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 (
"errors"
"fmt"
)

func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
} else {
return a / b, nil
}

}

func main() {
d, err := divide(4, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", d)
}
}

自定义错误类型

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 MyError struct {
When string
What string
}

func (e *MyError) Error() string {
return fmt.Sprintf("在 %s 发生错误: %s", e.When, e.What)
}

func run() error {
err := &MyError{
When: "2020-10-20",
What: "程序出错",
}
return err
}

func main() {
error := run()
if error != nil {
fmt.Println(error)
}
}

面向对象

匿名字段

go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

1
2
3
4
5
6
7
8
9
10
type Person struct {
name string
sex byte
age int
}
type Student struct {
Person // 匿名字段,只有类型,没有名字。它继承了Person的所有成员
id int
addr string
}

匿名字段初始化

1
2
3
4
5
6
7
8
9
// 初始化
s1 := Student{Person{"5lmh", "man", 20}, 1, "bj"}
fmt.Println(s1)

s2 := Student{Person: Person{"5lmh", "man", 20}}
fmt.Println(s2)

s3 := Student{Person: Person{name: "5lmh"}}
fmt.Println(s3)
1
2
3
{{5lmh man 20} 1 bj}
{{5lmh man 20} 0 }
{{5lmh 0} 0 }

同名字段的情况

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

import "fmt"

//人
type Person struct {
name string
sex string
age int
}

type Student struct {
Person
id int
addr string
//同名字段
name string
}

func main() {
var s Student
// 给自己字段赋值了
s.name = "5lmh"
fmt.Println(s)

// 若给父类同名字段赋值,如下
s.Person.name = "枯藤"
fmt.Println(s)
}

输出结果:

1
2
{{  0} 0  5lmh}
{{枯藤 0} 0 5lmh}

所有的内置类型和自定义类型都是可以作为匿名字段去使用

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"

//人
type Person struct {
name string
sex string
age int
}

// 自定义类型
type mystr string

// 学生
type Student struct {
Person
int
mystr
}

func main() {
s1 := Student{Person{"5lmh", "man", 18}, 1, "bj"}
fmt.Println(s1)
}

输出结果:

1
{{5lmh man 18} 1 bj}

指针类型匿名字段

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

import "fmt"

//人
type Person struct {
name string
sex string
age int
}

// 学生
type Student struct {
*Person
id int
addr string
}

func main() {
s1 := Student{&Person{"5lmh", "man", 18}, 1, "bj"}
fmt.Println(s1)
fmt.Println(s1.name)
fmt.Println(s1.Person.name)
}

输出结果:

1
2
3
{0x140001220c0 1 bj}
5lmh
5lmh

接口

接口(interface)是 Go 语言中的一种类型,用于定义行为的集合,它通过描述类型必须实现的方法,规定了类型的行为契约。

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

Go 的接口设计简单却功能强大,是实现多态和解耦的重要工具。

接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计。

实现

  • Go 中没有关键字显式声明某个类型实现了某个接口。
  • 只要一个类型实现了接口要求的所有方法,该类型就自动被认为实现了该接口。

接口类型变量

  • 接口变量可以存储实现该接口的任意值。
  • 接口变量实际上包含了两个部分:
    • 动态类型:存储实际的值类型。
    • 动态值:存储具体的值。

零值接口

  • 接口的零值是 nil
  • 一个未初始化的接口变量其值为 nil,且不包含任何动态类型或值。

空接口

  • 定义为 interface{},可以表示任何类型。

接口的常见用法

  1. 多态:不同类型实现同一接口,实现多态行为。
  2. 解耦:通过接口定义依赖关系,降低模块之间的耦合。
  3. 泛化:使用空接口 interface{} 表示任意类型。

定义接口

1
2
3
4
type Shape interface {
Area() float64
Perimeter() float64
}
  • Shape 是一个接口,定义了两个方法:AreaPerimeter
  • 任意类型只要实现了这两个方法,就被认为实现了 Shape 接口。

实现接口: 类型通过实现接口要求的所有方法来实现接口。

1
2
3
4
5
6
7
8
9
10
11
12
//定义一个圆
type Circle struct {
Radius float64
}

// Circle类型实现Shape接口(实现了Area和Perimeter方法)
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}

初始化接口

1
var s Shape = Circle{Radius: 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
package main

import (
"fmt"
"math"
)

//定义接口
type Shape interface {
Area() float64
Perimeter() float64
}

//定义一个圆
type Circle struct {
Radius float64
}

// Circle类型实现Shape接口(实现了Area和Perimeter方法)
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}

func main() {
var s Shape = Circle{Radius: 1} //接口变量可以存储实现了接口的类型的对象
fmt.Println("Area: ", s.Area())
fmt.Println("Perimeter: ", s.Perimeter())
}

文件操作

打开文件

常见的两种打开文件的方式是使用os包提供的两个函数,Open函数返回值一个文件指针和一个错误,常见的两种打开文件的方式是使用os包提供的两个函数,Open函数返回值一个文件指针和一个错误,文件的查找路径默认为项目go.mod文件所在的路径。

1
2
3
4
5
6
7
8
9
10
// 读取文件 Open仅仅只是只读的,无法被修改
file, err := os.Open("1.txt")
if os.IsNotExist(err) {
fmt.Println("File not found")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Println("File found:", file)
defer file.Close()
}

通过OpenFile函数可以控制更多细节,例如修改文件描述符和文件权限,关于文件描述符,os包下提供了以下常量以供使用。

1
2
3
4
5
6
7
8
9
10
11
12
const (
// 只读,只写,读写 三种必须指定一个
O_RDONLY int = syscall.O_RDONLY // 以只读的模式打开文件
O_WRONLY int = syscall.O_WRONLY // 以只写的模式打开文件
O_RDWR int = syscall.O_RDWR // 以读写的模式打开文件
// 剩余的值用于控制行为
O_APPEND int = syscall.O_APPEND // 当写入文件时,将数据添加到文件末尾
O_CREATE int = syscall.O_CREAT // 如果文件不存在则创建文件
O_EXCL int = syscall.O_EXCL // 与O_CREATE一起使用, 文件必须不存在
O_SYNC int = syscall.O_SYNC // 以同步IO的方式打开文件
O_TRUNC int = syscall.O_TRUNC // 当打开的时候截断可写的文件
)

关于文件权限的则提供了以下常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const (
ModeDir = fs.ModeDir // d: 目录
ModeAppend = fs.ModeAppend // a: 只能添加
ModeExclusive = fs.ModeExclusive // l: 专用
ModeTemporary = fs.ModeTemporary // T: 临时文件
ModeSymlink = fs.ModeSymlink // L: 符号链接
ModeDevice = fs.ModeDevice // D: 设备文件
ModeNamedPipe = fs.ModeNamedPipe // p: 具名管道 (FIFO)
ModeSocket = fs.ModeSocket // S: Unix 域套接字
ModeSetuid = fs.ModeSetuid // u: setuid
ModeSetgid = fs.ModeSetgid // g: setgid
ModeCharDevice = fs.ModeCharDevice // c: Unix 字符设备, 前提是设置了 ModeDevice
ModeSticky = fs.ModeSticky // t: 黏滞位
ModeIrregular = fs.ModeIrregular // ?: 非常规文件

// 类型位的掩码. 对于常规文件而言,什么都不会设置.
ModeType = fs.ModeType

ModePerm = fs.ModePerm // Unix 权限位, 0o777
)

下面是一个以读写模式打开一个文件的代码例子,权限为0666,表示为所有人都可以对该文件进行读写,且不存在时会自动创建。

1
2
3
4
5
6
7
8
file2, err := os.OpenFile("README.md", os.O_RDWR|os.O_CREATE, 0666)
if os.IsNotExist(err) {
fmt.Println("File not found")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Println("File found:", file2.Name())
}

倘若只是想获取该文件的一些信息,并不想读取该文件,可以使用os.Stat()函数进行操作,代码示例如下

1
2
3
4
5
6
info, err := os.Stat("README.md")
if err != nil {
log.Fatal(err)
} else {
fmt.Println(fmt.Sprintf("File found:%+v", info))
}

注意

打开一个文件后永远要记得关闭该文件,通常关闭操作会放在defer语句里

1
defer file.Close()

读取文件

比较简单的两种 os.ReadFileio.ReadAll

1
2
3
4
5
6
bytes, err := os.ReadFile("1.txt")
if err != nil {
log.Fatal(err)
} else {
fmt.Println(string(bytes))
}

io.ReadAllos.ReadFile 不同的是它接受的参数不是文件路径,而是 io.Reader 接口的实现,恰好os.OpenFile 打开一个文件,它返回的 *os.File 类型就实现了 io.Reader 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
f, err := os.Open("1.txt")
if os.IsNotExist(err) {
fmt.Println("File not found")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Println("File found:", f.Name())
all, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
} else {
fmt.Println(string(all))
}
}

文件写入

os.File结构体提供了以下几种方法以供写入数据

1
2
3
4
5
6
7
8
// 写入字节切片
func (f *File) Write(b []byte) (n int, err error)

// 写入字符串
func (f *File) WriteString(s string) (n int, err error)

// 从指定位置开始写,当以os.O_APPEND模式打开时,会返回错误
func (f *File) WriteAt(b []byte, off int64) (n int, err error)

如果想要对一个文件写入数据,则必须以O_WRONLYO_RDWR的模式打开,否则无法成功写入文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f1, err := os.OpenFile("1.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if os.IsNotExist(err) {
fmt.Println("File not found")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Println("File found:", f1.Name())
n, err := f1.WriteString("MakkaPakka\n")
if err != nil {
log.Fatal(err)
} else {
fmt.Println(n)
}
f1.Close()
}

用 gocsv 库对 csv 文件进行操作

gocsv包的最基本的作用就是能够方便的将csv内容转换到对应的结构体上,或者将结构体的内容快速的转换成csv格式(包括写入文件)。

image-20250103231417649

1
2
3
4
client_id,client_name,client_age
1,Jose,42
2,Daniel,26
3,Vincent,32

gocsv.UnmarshalFile函数:csv内容转成结构体。

我们可以从文件中读取出内容,并直接转换到结构体Client上,如下:

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

import (
"fmt"
"os"
"github.com/gocarina/gocsv"
)

type NotUsed struct {
Name string
}

type Client struct { // Our example struct, you can use "-" to ignore a field
Id string `csv:"client_id"`
Name string `csv:"client_name"`
Age string `csv:"client_age"`
NotUsedString string `csv:"-"`
NotUsedStruct NotUsed `csv:"-"`
}

func main() {
clientsFile, err := os.OpenFile("1.csv", os.O_RDWR|os.O_CREATE, os.ModePerm)
if err != nil {
panic(err)
}
defer clientsFile.Close()

clients := []*Client{}

if err := gocsv.UnmarshalFile(clientsFile, &clients); err != nil { // Load clients from file
panic(err)
}
for _, client := range clients {
fmt.Println("Hello", client.Name)
}
}

gocsv.MarshalFile函数:结构体转成csv文件

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

import (
"fmt"
"os"

"github.com/gocarina/gocsv"
)

type NotUsed struct {
Name string
}

type Client struct { // Our example struct, you can use "-" to ignore a field
Id string `csv:"client_id"`
Name string `csv:"client_name"`
Age string `csv:"client_age"`
NotUsedString string `csv:"-"`
NotUsedStruct NotUsed `csv:"-"`
}

func main() {
clientsFile, err := os.OpenFile("clients.csv", os.O_RDWR|os.O_CREATE, os.ModePerm)
if err != nil {
panic(err)
}
defer clientsFile.Close()

clients := []*Client{}

clients = append(clients, &Client{Id: "12", Name: "John", Age: "21"}) // Add clients
clients = append(clients, &Client{Id: "13", Name: "Fred"})
clients = append(clients, &Client{Id: "14", Name: "James", Age: "32"})
clients = append(clients, &Client{Id: "15", Name: "Danny"})

err = gocsv.MarshalFile(&clients, clientsFile) // Use this to save the CSV back to the file
if err != nil {
panic(err)
}

}

gocsv包还可以给自定义的结构体类型定义csv和结构体的互转函数。只要自定义的类型实现如下接口即可:

1
2
3
4
5
6
7
8
9
type TypeMarshaller interface {
MarshalCSV() (string, error)
}

// TypeUnmarshaller is implemented by any value that has an UnmarshalCSV method
// This converter is used to convert a string to your value representation of that string
type TypeUnmarshaller interface {
UnmarshalCSV(string) error
}

或者将结构体转换成csv字符串时,需要实现如下接口:

1
2
3
4
5
6
7
8
// MarshalText encodes the receiver into UTF-8-encoded text and returns the result.
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}

type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}

例如,我们定义了一个结构体DateTime,里面有一个time.Time类型的属性。并且DateTime类型实现了TypeMarshaller接口的MarshalCSV函数和TypeUnmarshaller接口的UnmarshalCSV函数。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"github.com/gocarina/gocsv"
"time"
)

type DateTime struct {
time.Time
}
type Client struct { // Our example struct, you can use "-" to ignore a field
Id string `csv:"id"`
Name string `csv:"name"`
Employed DateTime `csv:"employed"`
}

func (date *DateTime) MarshalCSV() (string, error) {
return date.Time.Format("2006年01月02日"), nil
}
func main() {
client := []Client{
{
Id: "1",
Name: "Qin",
Employed: DateTime{time.Now()},
}}
csvContent, _ := gocsv.MarshalString(client)
fmt.Println("csv:", csvContent)
}

Json 操作

jsonRestful风格的接口通信中经常会用到,其相较于xml更轻便的大小,低廉的学习成本使其在web领域称为了主流的数据交换格式。

在 go 中,encoding/json包下提供对应的函数来进行 json 的序列化与反序列化,主要使用的有如下函数。

1
2
3
func Marshal(v any) ([]byte, error) //将go对象序列化为json字符串

func Unmarshal(data []byte, v any) error //将json字符串反序列化为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
// json 操作
package main

import (
"encoding/json"
"fmt"
)

type Person struct {
UserId string
Username string
Age int
Address string
}

func main() {
person := Person{
UserId: "120",
Username: "jack",
Age: 18,
Address: "usa",
}
marshal, err := json.Marshal(person)

if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(marshal))
}
}

输出

1
{"UserId":"120","Username":"jack","Age":18,"Address":"usa"}

我们可以通过结构体标签来达到重命名的效果。

1
2
3
4
5
6
type Person struct {
UserId string `json:"id"`
Username string `json:"user"`
Age int `json:"age"`
Address string `json:"adr"`
}

输出

1
{"id":"120","user":"jack","age":18,"adr":"usa"}

缩进

序列化时默认是没有任何缩进的,这是为了减少传输过程的空间损耗,但是这并不利于人为观察,在一些情况下我们需要将其序列化成人类能够观察的形式。为此,只需要换一个函数。

1
func MarshalIndent(v any, prefix, indent string) ([]byte, error)
1
marshal, err := json.MarshalIndent(person, "", "  ")

输出

1
2
3
4
5
6
{
"UserId": "120",
"Username": "jack",
"Age": 18,
"Address": "usa"
}