Golang笔记和技巧记录

Go

基础

变量

定义

1
2
//变量的声明
var a int = 10

简短声明

1
2
3
//简短声明
b := 20
fmt.Println(a, b)

多重赋值

1
2
3
//多重赋值
c, d, e := 30, 40, 50
fmt.Println(c, d, e)

交换变量值

1
2
3
4
5
//交换变量值
f, g := 99, 88
fmt.Println(f, g)//99 88
f, g = g, f
fmt.Println(f, g)//88 99

下划线忽略值

1
2
3
//忽略值/返回值
h, _ := 10, 20
fmt.Println(h)

格式化输出

1
2
3
4
5
6
7
8
9
//格式输出
fmt.Printf("%d\n", 1) //%d 1 以整数输出
fmt.Printf("%f\n", 1.1) //%f 1.100000 以浮点型输出
fmt.Printf("%.2f\n", 1.11111) //%.2f 1.11 以浮点型输出,保留2位小数
fmt.Printf("%t\n", false) //%t false 以布尔型数据输出
fmt.Printf("%s\n", "false") //%s false 以字符串型数据输出
fmt.Printf("%c\n", 'a') //%c 'a' 以字符型数据输出
fmt.Printf("%p\n", &a) //%p 0x1400012c008 以指针数据输出地址
fmt.Printf("%T\n", a) //%T 输出该变量的类型

获取输入

1
2
3
//获取用户输入
fmt.Scan(&a) //输入88
fmt.Println(a) //输出88

字符和字节

1
2
3
var n byte = 'a' //byte只能存单个字符
var m rune = '树' //rune相当于其他语言里的char,存储单个Unicode字符/中文等等
fmt.Println(n,m)

常量

定义

常量存储一直不会发生变化的数据

1
2
//常量的定义
const a = false

iota枚举

常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。注意:在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。

1
2
3
4
5
6
7
8
9
10
11
12
13
const (
b = iota
c = iota
d = iota
)
fmt.Println(b, c, d) //0 1 2

const (
f = iota
g
h
)
fmt.Println(f, g, h) //0 1 2

如果定义枚举时,常量写在同一行值是相同的,换一行就加一

1
2
3
4
5
const (
i = iota
j, k = iota, iota
)
fmt.Println(i, j, k) //0 1 1

运算符

算数运算符

运算符 描述
+ 相加
- 相减
* 相乘
/ 相除
% 求余

注意: ++(自增)和–(自减)在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= 按位或后赋值
^= 按位异或后赋值

类型转换

1
2
3
4
//类型转换
c := 3
d := float64(c)
fmt.Println(c, d)

流程控制

If语句(Go不支持三目)

1
2
3
4
5
6
7
• 可省略条件表达式括号。
• 持初始化语句,可定义代码块局部变量。
• 代码块左 括号必须在条件表达式尾部。

if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}

可以在表达式中声明变量

1
2
3
4
5
6
7
8
x := 0
if n := "abc"; x > 0 { // 初始化语句未必就是定义变量, 如 println("init") 也是可以的。
println(n[2])
} else if x < 0 { // 注意 else if 和 else 左大括号位置。
println(n[1])
} else {
println(n[0])
}

Switch语句

语法

Go中默认带Break,如果不需要Break可以用fallthrough关键字

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

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。

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

import "fmt"

func main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90

switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}

switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" )
}
fmt.Printf("你的等级是 %s\n", grade )
}

以上代码执行结果为:

1
2
优秀!
你的等级是 A
Type Switch
1
2
3
4
5
6
7
8
9
switch x.(type){
case type:
statement(s)
case type:
statement(s)
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s)
}
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import "fmt"

func main() {
var x interface{}
//写法一:
switch i := x.(type) { // 带初始化语句
case nil:
fmt.Printf(" x 的类型 :%T\r\n", i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
//写法二
var j = 0
switch j {
case 0:
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("def")
}
//写法三
var k = 0
switch k {
case 0:
println("fallthrough")
fallthrough
/*
Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;
而如果switch没有表达式,它会匹配true。
Go里面switch默认相当于每个case最后带有break,
匹配成功后不会自动向下执行其他case,而是跳出整个switch,
但是可以使用fallthrough强制执行后面的case代码。
*/
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("def")
}
//写法三
var m = 0
switch m {
case 0, 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("def")
}
//写法四
var n = 0
switch { //省略条件表达式,可当 if...else if...else
case n > 0 && n < 10:
fmt.Println("i > 0 and i < 10")
case n > 10 && n < 20:
fmt.Println("i > 10 and i < 20")
default:
fmt.Println("def")
}
}

以上代码执行结果为:

1
2
3
4
5
x 的类型 :<nil>
fallthrough
1
1
def

For语句

for循环是一个循环控制结构,可以执行指定次数的循环。

语法

Go语言的For循环有3中形式,只有其中的一种使用分号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    for init; condition; post { }
for condition { }
for { }
init: 一般为赋值表达式,给控制变量赋初值;
condition: 关系表达式或逻辑表达式,循环控制条件;
post: 一般为赋值表达式,给控制变量增量或减量。
for语句执行过程如下:
①先对表达式 init 赋初值;
②判别赋值表达式 init 是否满足给定 condition 条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行 post,进入第二次循环,再判别 condition;否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。
s := "abc"

for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
println(s[i])
}

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

for { // 替代 while (true) {}
println(s) // 替代 for (;;) {}
}

不要期望编译器能理解你的想法,在初始化语句中计算出全部结果是个好主意。

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

func length(s string) int {
println("call length.")
return len(s)
}

func main() {
s := "abcd"

for i, n := 0, length(s); i < n; i++ { // 避免多次调用 length 函数。
println(i, s[i])
}
}

输出:

1
2
3
4
5
call length.
0 97
1 98
2 99
3 100
实例
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() {

var b int = 15
var a int

numbers := [6]int{1, 2, 3, 5}

/* for 循环 */
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)
}
}

以上实例运行输出结果为:

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
a 的值为: 0
a 的值为: 1
a 的值为: 2
a 的值为: 3
a 的值为: 4
a 的值为: 5
a 的值为: 6
a 的值为: 7
a 的值为: 8
a 的值为: 9
a 的值为: 1
a 的值为: 2
a 的值为: 3
a 的值为: 4
a 的值为: 5
a 的值为: 6
a 的值为: 7
a 的值为: 8
a 的值为: 9
a 的值为: 10
a 的值为: 11
a 的值为: 12
a 的值为: 13
a 的值为: 14
a 的值为: 15
第 0 位 x 的值 = 1
第 1 位 x 的值 = 2
第 2 位 x 的值 = 3
第 3 位 x 的值 = 5
第 4 位 x 的值 = 0
第 5 位 x 的值 = 0
循环嵌套

在 for 循环中嵌套一个或多个 for 循环

语法

以下为 Go 语言嵌套循环的格式:

1
2
3
4
5
6
7
8
for [condition |  ( init; condition; increment ) | Range]
{
for [condition | ( init; condition; increment ) | Range]
{
statement(s)
}
statement(s)
}
实例

以下实例使用循环嵌套来输出 2 到 100 间的素数:

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

import "fmt"

func main() {
/* 定义局部变量 */
var i, j int

for i=2; i < 100; i++ {
for j=2; j <= (i/j); j++ {
if(i%j==0) {
break // 如果发现因子,则不是素数
}
}
if(j > (i/j)) {
fmt.Printf("%d 是素数\n", i)
}
}
}

以上实例运行输出结果为:

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
2  是素数
3 是素数
5 是素数
7 是素数
11 是素数
13 是素数
17 是素数
19 是素数
23 是素数
29 是素数
31 是素数
37 是素数
41 是素数
43 是素数
47 是素数
53 是素数
59 是素数
61 是素数
67 是素数
71 是素数
73 是素数
79 是素数
83 是素数
89 是素数
97 是素数
无限循环

如过循环中条件语句永远不为 false 则会进行无限循环,我们可以通过 for 循环语句中只设置一个条件表达式来执行无限循环:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
for true {
fmt.Printf("这是无限循环。\n");
}
}

Range语句

语法

Golang range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。

for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

1
2
3
for key, value := range oldMap {
newMap[key] = value
}
1st value 2nd value
string index s[index] unicode, rune
array/slice index s[index]
map key m[key]
channel element

可忽略不想要的返回值,或 "_" 这个特殊变量。

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

func main() {
s := "abc"
// 忽略 2nd value,支持 string/array/slice/map。
for i := range s {
println(s[i])
}
// 忽略 index。
for _, c := range s {
println(c)
}
// 忽略全部返回值,仅迭代。
for range s {

}

m := map[string]int{"a": 1, "b": 2}
// 返回 (key, value)。
for k, v := range m {
println(k, v)
}
}

输出结果:

1
2
3
4
5
6
7
8
97
98
99
97
98
99
a 1
b 2
重要注意

*注意,range 会复制对象。

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

import "fmt"

func main() {
a := [3]int{0, 1, 2}

for i, v := range a { // index、value 都是从复制品中取出。

if i == 0 { // 在修改前,我们先修改原数组。
a[1], a[2] = 999, 999
fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
}

a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。

}

fmt.Println(a) // 输出 [100, 101, 102]。
}

输出结果:

1
2
[0 999 999]
[100 101 102]

建议改用引用类型,其底层数据不会被复制。

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

func main() {
s := []int{1, 2, 3, 4, 5}

for i, v := range s { // 复制 struct slice { pointer, len, cap }。

if i == 0 {
s = s[:3] // 对 slice 的修改,不会影响 range。
s[2] = 100 // 对底层数据的修改。
}

println(i, v)
}
}

输出结果:

1
2
3
4
5
0 1
1 2
2 100
3 4
4 5

另外两种引用类型 map、channel 是指针包装,而不像 slice 是 struct。

for和range区别

主要是使用场景不同

for可以 遍历array和slice || 遍历key为整型递增的map || 遍历string

for range可以完成所有for可以做的事情,却能做到for不能做的,包括

遍历key为string类型的map并同时获取key和value || 遍历channel

函数

定义

函数特点
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

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

参数

普通形参

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:

值传递

指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

1
2
3
func swap(x, y int) int {
... ...
}
引用传递

是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

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

import (
"fmt"
)

/* 定义相互交换值的函数 */
func swap(x, y *int) {
var temp int

temp = *x /* 保存 x 的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y*/

}

func main() {
var a, b int = 1, 2
/*
调用 swap() 函数
&a 指向 a 指针,a 变量的地址
&b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)

fmt.Println(a, b)
}

输出结果:

1
2 1

在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

注意1

无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

注意2

map、slice、chan、指针、interface默认以引用的方式传递。

不定参数

不定参数传值就是函数的参数不是固定的,后面的类型是固定的。(可变参数)

Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。

在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。

1
2
3
4
5
6
7
8
func main() {
//不定参函数
test(1, 2, 3, 4)//[1 2 3 4]
}

func test(args ...int) {
fmt.Println(args)
}
Slice传入不定参方法

使用 slice 对象做变参时,必须展开。(slice...)

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

import (
"fmt"
)

func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}

return fmt.Sprintf(s, x)
}

func main() {
s := []int{1, 2, 3}
res := test("sum: %d", s...) // slice... 展开slice
println(res)
}

返回值

"_"标识符,用来忽略函数的某个返回值

Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。

返回值的名称应当具有一定的意义,可以作为文档使用。

没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。

直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

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

import (
"fmt"
)

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

func calc(a, b int) (sum int, avg int) {
sum = a + b
avg = (a + b) / 2

return
}

func main() {
var a, b int = 1, 2
c := add(a, b)
sum, avg := calc(a, b)
fmt.Println(a, b, c, sum, avg)
}

输出结果:

1
1 2 3 3 1 

Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。

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

func test() (int, int) {
return 1, 2
}

func main() {
// s := make([]int, 2)
// s = test() // Error: multiple-value test() in single-value context

x, _ := test()
println(x)
}

输出结果:

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

func test() (int, int) {
return 1, 2
}

func add(x, y int) int {
return x + y
}

func sum(n ...int) int {
var x int
for _, i := range n {
x += i
}

return x
}

func main() {
println(add(test()))
println(sum(test()))
}

输出结果:

1
2
3
3

命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。

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

func add(x, y int) (z int) {
z = x + y
return
}

func main() {
println(add(1, 2))
}

输出结果:

1
3  

命名返回参数可被同名局部变量遮蔽,此时需要显式返回。

1
2
3
4
5
6
7
func add(x, y int) (z int) {
{ // 不能在一个级别,引发 "z redeclared in this block" 错误。
var z = x + y
// return // Error: z is shadowed during return
return z // 必须显式返回。
}
}

命名返回参数允许 defer 延迟调用通过闭包读取和修改。

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

func add(x, y int) (z int) {
defer func() {
z += 100
}()

z = x + y
return
}

func main() {
println(add(1, 2))
}

输出结果:

1
103

显式 return 返回前,会先修改命名返回参数。

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

func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()

z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}

func main() {
println(add(1, 2)) // 输出: 203
}

输出结果:

1
2
203
203

匿名函数

匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。

在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。

匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

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

import (
"fmt"
"math"
)

func main() {
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}

输出结果:

1
2
闭包

因为函数在调用结束后会进行销毁,所有函数内部的变量值没有进行保存

1
2
3
4
func test1(a int) {
a++
fmt.Println(a)
}
1
2
3
4
5
6
func main() {
a := 1
for i := 0; i < 10; i++ {
test1(a)
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
2
2
2
2
2
2
2
2
2
2

返回值也是函数的函数称为闭包

可以通过匿名函数和闭包 实现函数在栈区实现持久化

1
2
3
4
5
6
7
func main(){
a := 1
f := test2(a)
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
1
2
3
4
5
6
func test2(a int) func() int {
return func() int {

return a
}
}

输出结果

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

递归函数

递归,就是在运行的过程中调用自己。
一个函数调用自己,就叫做递归函数。

构成递归需具备的条件:

1
2
1.子问题须与原始问题为同样的事,且更为简单。
2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
数字阶乘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func factorial(i int) int {
if i <= 1 {
return 1
}
return i * factorial(i-1)
}

func main() {
var i int = 7
fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}

输出结果:

1
Factorial of 7 is 5040
斐波那契数列

这个数列从第3项开始,每一项都等于前两项之和。

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

import "fmt"

func fibonaci(i int) int {
if i == 0 {
return 0
}
if i == 1 {
return 1
}
return fibonaci(i-1) + fibonaci(i-2)
}

func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\n", fibonaci(i))
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
0
1
1
2
3
5
8
13
21
34

defer延迟调用

defer特性
1
2
3
4
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
defer用途
1
2
3
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放

defer是先进后出的

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

import "fmt"

func main() {
var whatever [5]struct{}

for i := range whatever {
defer fmt.Println(i)
}
}

输出结果:

1
2
3
4
5
4
3
2
1
0
defer和闭包

image-20220416190037239

defer 会延迟函数的执行,虽然立即调用了匿名函数,但是该匿名函数不会执行,等整个 main( )函数结束之前在去调用执行匿名函数

image-20220416190056827

由于匿名函数前面加上了 defer 所以,匿名函数没有立即执行。但是问题是,程序从上开始执行当执行到匿名函数时,虽然没有立即调用执行匿名函数,但是已经完成了参数的传递。

defer和异常

多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

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

func test(x int) {
defer println("a")
defer println("b")

defer func() {
println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()

defer println("c")
}

func main() {
test(0)
}

输出结果:

1
2
3
4
c
b
a
panic: runtime error: integer divide by zero
重要

*延迟调用参数在注册时求值或复制,可用指针或闭包 “延迟” 读取。

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

func test() {
x, y := 10, 20

defer func(i int) {
println("defer:", i, y) // y 闭包引用
}(x) // x 被复制 <<<<<------这里闭包将参数传递进来了

x += 10
y += 100
println("x =", x, "y =", y)
}

func main() {
test()
}

输出结果:

1
2
x = 20 y = 120
defer: 10 120
滥用defer

*滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

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

var lock sync.Mutex

func test() {
lock.Lock()
lock.Unlock()
}

func testdefer() {
lock.Lock()
defer lock.Unlock()
}

func main() {
func() {
t1 := time.Now()

for i := 0; i < 10000; i++ {
test()
}
elapsed := time.Since(t1)
fmt.Println("test elapsed: ", elapsed)
}()
func() {
t1 := time.Now()

for i := 0; i < 10000; i++ {
testdefer()
}
elapsed := time.Since(t1)
fmt.Println("testdefer elapsed: ", elapsed)
}()

}

输出结果:

1
2
test elapsed:  223.162µs
testdefer elapsed: 781.304µs
defer陷阱
defer 与 closure
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 (
"errors"
"fmt"
)

func foo(a, b int) (i int, err error) {
defer fmt.Printf("first defer err %v\n", err)
defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
defer func() { fmt.Printf("third defer err %v\n", err) }()
if b == 0 {
err = errors.New("divided by zero!")
return
}

i = a / b
return
}

func main() {
foo(2, 0)
}

输出结果:

1
2
3
third defer err divided by zero!
second defer err <nil>
first defer err <nil>

解释:如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。

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

import "fmt"

func foo() (i int) {

i = 0
defer func() {
fmt.Println(i)
}()

return 2
}

func main() {
foo()
}

输出结果:

1
2

解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。

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

import (
"fmt"
)

func test() {
var run func() = nil
defer run()
fmt.Println("runs")
}

func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
test()
}

输出结果:

1
2
runs
runtime error: invalid memory address or nil pointer dereference

解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。

数组

1
2
3
4
5
6
7
8
9
10
11
12
1. 数组:是同一种数据类型的固定长度的序列。
2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
3. 长度是数组类型的一部分,因此,var a[5] intvar a[10]int是不同的类型。
4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
for i := 0; i < len(a); i++ {
}
for index, v := range a {
}
5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
7.支持 "==""!=" 操作符,因为内存总是被初始化过的。
8.指针数组 [n]*T,数组指针 *[n]T

数组初始化

一维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
全局:
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
局部:
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}

代码:

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

var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}

func main() {
a := [3]int{1, 2} // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
d := [...]struct {
name string
age uint8
}{
{"user1", 10}, // 可省略元素类型。
{"user2", 20}, // 别忘了最后一行的逗号。
}
fmt.Println(arr0, arr1, arr2, str)
fmt.Println(a, b, c, d)
}

输出结果:

1
2
[1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [   hello world tom]
[1 2 0] [1 2 3 4] [0 0 100 0 200] [{user1 10} {user2 20}]
多维数组
1
2
3
4
5
6
全局
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
局部:
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。

代码:

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

import (
"fmt"
)

var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

func main() {
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
fmt.Println(arr0, arr1)
fmt.Println(a, b)
}

输出结果:

1
2
[[0 0 0] [0 0 0] [0 0 0] [0 0 0] [0 0 0]] [[1 2 3] [7 8 9]]
[[1 2 3] [4 5 6]] [[1 1] [2 2] [3 3]]

值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。

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

import (
"fmt"
)

func test(x [2]int) {
fmt.Printf("x: %p\n", &x)
x[1] = 1000
}

func main() {
a := [2]int{}
fmt.Printf("a: %p\n", &a)

test(a)
fmt.Println(a)
}

输出结果:

1
2
3
a: 0xc42007c010
x: 0xc42007c030
[0 0]

内置函数 len 和 cap 都返回数组长度 (元素数量)。

1
2
3
4
5
6
package main

func main() {
a := [2]int{}
println(len(a), cap(a))
}

输出结果:

1
2 2
多维数组遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

func main() {

var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

for k1, v1 := range f {
for k2, v2 := range v1 {
fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
}
fmt.Println()
}
}

输出结果:

1
2
(0,0)=1 (0,1)=2 (0,2)=3 
(1,0)=7 (1,1)=8 (1,2)=9
数组拷贝和传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func printArr(arr *[5]int) {
arr[0] = 10
for i, v := range arr {
fmt.Println(i, v)
}
}

func main() {
var arr1 [5]int
printArr(&arr1)
fmt.Println(arr1)
arr2 := [...]int{2, 4, 6, 8, 10}
printArr(&arr2)
fmt.Println(arr2)
}

切片

slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

1
2
3
4
5
6
1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
2. 切片的长度可以改变,因此,切片是一个可变的数组。
3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。
4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
5. 切片的定义:var 变量名 []类型,比如 var str []string var arr []int
6. 如果 slice == nil,那么 lencap 结果都等于 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"

func main() {
//1.声明切片
var s1 []int
if s1 == nil {
fmt.Println("是空")
} else {
fmt.Println("不是空")
}
// 2.:=
s2 := []int{}
// 3.make()
var s3 []int = make([]int, 0)
fmt.Println(s1, s2, s3)
// 4.初始化赋值
var s4 []int = make([]int, 0, 0)
fmt.Println(s4)
s5 := []int{1, 2, 3}
fmt.Println(s5)
// 5.从数组切片
arr := [5]int{1, 2, 3, 4, 5}
var s6 []int
// 前包后不包
s6 = arr[1:4]
fmt.Println(s6)
}

切片初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//全局:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end]
var slice1 []int = arr[:end]
var slice2 []int = arr[start:]
var slice3 []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
//局部:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[start:end]
slice6 := arr[:end]
slice7 := arr[start:]
slice8 := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素

img

代码:

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

import (
"fmt"
)

var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
var slice1 []int = arr[0:6] //可以简写为 var slice []int = arr[:end]
var slice2 []int = arr[5:10] //可以简写为 var slice[]int = arr[start:]
var slice3 []int = arr[0:len(arr)] //var slice []int = arr[:]
var slice4 = arr[:len(arr)-1] //去掉切片的最后一个元素
func main() {
fmt.Printf("全局变量:arr %v\n", arr)
fmt.Printf("全局变量:slice0 %v\n", slice0)
fmt.Printf("全局变量:slice1 %v\n", slice1)
fmt.Printf("全局变量:slice2 %v\n", slice2)
fmt.Printf("全局变量:slice3 %v\n", slice3)
fmt.Printf("全局变量:slice4 %v\n", slice4)
fmt.Printf("-----------------------------------\n")
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[2:8]
slice6 := arr[0:6] //可以简写为 slice := arr[:end]
slice7 := arr[5:10] //可以简写为 slice := arr[start:]
slice8 := arr[0:len(arr)] //slice := arr[:]
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
fmt.Printf("局部变量: arr2 %v\n", arr2)
fmt.Printf("局部变量: slice5 %v\n", slice5)
fmt.Printf("局部变量: slice6 %v\n", slice6)
fmt.Printf("局部变量: slice7 %v\n", slice7)
fmt.Printf("局部变量: slice8 %v\n", slice8)
fmt.Printf("局部变量: slice9 %v\n", slice9)
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
全局变量:arr [0 1 2 3 4 5 6 7 8 9]
全局变量:slice0 [2 3 4 5 6 7]
全局变量:slice1 [0 1 2 3 4 5]
全局变量:slice2 [5 6 7 8 9]
全局变量:slice3 [0 1 2 3 4 5 6 7 8 9]
全局变量:slice4 [0 1 2 3 4 5 6 7 8]
-----------------------------------
局部变量: arr2 [9 8 7 6 5 4 3 2 1 0]
局部变量: slice5 [2 3 4 5 6 7]
局部变量: slice6 [0 1 2 3 4 5]
局部变量: slice7 [5 6 7 8 9]
局部变量: slice8 [0 1 2 3 4 5 6 7 8 9]
局部变量: slice9 [0 1 2 3 4 5 6 7 8]

通过make来创建切片

1
2
3
var slice []type = make([]type, len)
slice := make([]type, len)
slice := make([]type, len, cap)

null

代码:

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

var slice0 []int = make([]int, 10)
var slice1 = make([]int, 10)
var slice2 = make([]int, 10, 10)

func main() {
fmt.Printf("make全局slice0 :%v\n", slice0)
fmt.Printf("make全局slice1 :%v\n", slice1)
fmt.Printf("make全局slice2 :%v\n", slice2)
fmt.Println("--------------------------------------")
slice3 := make([]int, 10)
slice4 := make([]int, 10)
slice5 := make([]int, 10, 10)
fmt.Printf("make局部slice3 :%v\n", slice3)
fmt.Printf("make局部slice4 :%v\n", slice4)
fmt.Printf("make局部slice5 :%v\n", slice5)
}

输出结果:

1
2
3
4
5
6
7
make全局slice0 :[0 0 0 0 0 0 0 0 0 0]
make全局slice1 :[0 0 0 0 0 0 0 0 0 0]
make全局slice2 :[0 0 0 0 0 0 0 0 0 0]
--------------------------------------
make局部slice3 :[0 0 0 0 0 0 0 0 0 0]
make局部slice4 :[0 0 0 0 0 0 0 0 0 0]
make局部slice5 :[0 0 0 0 0 0 0 0 0 0]

切片的内存布局

null

读写操作实际目标是底层数组,只需注意索引号的差别。

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

import (
"fmt"
)

func main() {
data := [...]int{0, 1, 2, 3, 4, 5}

s := data[2:4]
s[0] += 100
s[1] += 200

fmt.Println(s)
fmt.Println(data)
}

输出:

1
2
[102 203]
[0 1 102 203 4 5]

可直接创建 slice 对象,自动分配底层数组。

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

import "fmt"

func main() {
s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
fmt.Println(s1, len(s1), cap(s1))

s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
fmt.Println(s2, len(s2), cap(s2))

s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
fmt.Println(s3, len(s3), cap(s3))
}

输出结果:

1
2
3
[0 1 2 3 0 0 0 0 100] 9 9
[0 0 0 0 0 0] 6 8
[0 0 0 0 0 0] 6 6

使用 make 动态创建slice,避免了数组必须用常量做长度的麻烦。还可用指针直接访问底层数组,退化成普通数组操作。

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

import "fmt"

func main() {
s := []int{0, 1, 2, 3}
p := &s[2] // *int, 获取底层数组元素指针。
*p += 100

fmt.Println(s)
}

输出结果:

1
[0 1 102 3]

至于 [][]T,是指元素类型为 []T 。

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

import (
"fmt"
)

func main() {
data := [][]int{
[]int{1, 2, 3},
[]int{100, 200},
[]int{11, 22, 33, 44},
}
fmt.Println(data)
}

输出结果:

1
[[1 2 3] [100 200] [11 22 33 44]]

切片追加

用append内置函数操作切片(切片追加

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

import (
"fmt"
)

func main() {

var a = []int{1, 2, 3}
fmt.Printf("slice a : %v\n", a)
var b = []int{4, 5, 6}
fmt.Printf("slice b : %v\n", b)
c := append(a, b...)
fmt.Printf("slice c : %v\n", c)
d := append(c, 7)
fmt.Printf("slice d : %v\n", d)
e := append(d, 8, 9, 10)
fmt.Printf("slice e : %v\n", e)

}

输出结果:

1
2
3
4
5
slice a : [1 2 3]
slice b : [4 5 6]
slice c : [1 2 3 4 5 6]
slice d : [1 2 3 4 5 6 7]
slice e : [1 2 3 4 5 6 7 8 9 10]

append :向 slice 尾部添加数据,返回新的 slice 对象。

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

import (
"fmt"
)

func main() {

s1 := make([]int, 0, 5)
fmt.Printf("%p\n", &s1)

s2 := append(s1, 1)
fmt.Printf("%p\n", &s2)

fmt.Println(s1, s2)

}

输出结果:

1
2
3
0xc42000a060
0xc42000a080
[] [1]

超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。

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

import (
"fmt"
)

func main() {
//这里的10:0是指第10位置的索引值为0,那么其他没有赋初始值的元素也为0
data := [...]int{0, 1, 2, 3, 4, 10: 0}
s := data[:2:3]

s = append(s, 100, 200) // 一次 append 两个值,超出 s.cap 限制。

fmt.Println(s, data) // 重新分配底层数组,与原数组无关。
fmt.Println(&s[0], &data[0]) // 比对底层数组起始指针。

}

输出结果:

1
2
[0 1 100 200] [0 1 2 3 4 0 0 0 0 0 0]
0xc4200160f0 0xc420070060

从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。
通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

slice中cap重新分配规律

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

import (
"fmt"
)

func main() {

s := make([]int, 0, 1)
c := cap(s)

for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}

}

输出结果:

1
2
3
4
5
6
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64

切片删除指定元素

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

import (
"fmt"
)

//删除函数
func remove(s []string, i int) []string {
return append(s[:i], s[i+1:]...)
}

func main() {
s := []string{"a", "b", "c"}
fmt.Println(s)
s = remove(s, 1)
fmt.Println(s)
}

切片拷贝

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 main() {

s1 := []int{1, 2, 3, 4, 5}
fmt.Printf("slice s1 : %v\n", s1)
s2 := make([]int, 10)
fmt.Printf("slice s2 : %v\n", s2)
copy(s2, s1)
fmt.Printf("copied slice s1 : %v\n", s1)
fmt.Printf("copied slice s2 : %v\n", s2)
s3 := []int{1, 2, 3}
fmt.Printf("slice s3 : %v\n", s3)
s3 = append(s3, s2...)
fmt.Printf("appended slice s3 : %v\n", s3)
s3 = append(s3, 4, 5, 6)
fmt.Printf("last slice s3 : %v\n", s3)

}

输出结果:

1
2
3
4
5
6
7
slice s1 : [1 2 3 4 5]
slice s2 : [0 0 0 0 0 0 0 0 0 0]
copied slice s1 : [1 2 3 4 5]
copied slice s2 : [1 2 3 4 5 0 0 0 0 0]
slice s3 : [1 2 3]
appended slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0]
last slice s3 : [1 2 3 1 2 3 4 5 0 0 0 0 0 4 5 6]

copy :函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。

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

import (
"fmt"
)

func main() {

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println("array data : ", data)
s1 := data[8:]
s2 := data[:5]
fmt.Printf("slice s1 : %v\n", s1)
fmt.Printf("slice s2 : %v\n", s2)
copy(s2, s1)
fmt.Printf("copied slice s1 : %v\n", s1)
fmt.Printf("copied slice s2 : %v\n", s2)
fmt.Println("last array data : ", data)

}

输出结果:

1
2
3
4
5
6
array data :  [0 1 2 3 4 5 6 7 8 9]
slice s1 : [8 9]
slice s2 : [0 1 2 3 4]
copied slice s1 : [8 9]
copied slice s2 : [8 9 2 3 4]
last array data : [8 9 2 3 4 5 6 7 8 9]

应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。

slice遍历

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

import (
"fmt"
)

func main() {

data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice := data[:]
for index, value := range slice {
fmt.Printf("inde : %v , value : %v\n", index, value)
}

}

输出结果:

1
2
3
4
5
6
7
8
9
10
inde : 0 , value : 0
inde : 1 , value : 1
inde : 2 , value : 2
inde : 3 , value : 3
inde : 4 , value : 4
inde : 5 , value : 5
inde : 6 , value : 6
inde : 7 , value : 7
inde : 8 , value : 8
inde : 9 , value : 9

切片resize(调整大小)

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

import (
"fmt"
)

func main() {
var a = []int{1, 3, 4, 5}
fmt.Printf("slice a : %v , len(a) : %v\n", a, len(a))
b := a[1:2]
fmt.Printf("slice b : %v , len(b) : %v\n", b, len(b))
c := b[0:3]
fmt.Printf("slice c : %v , len(c) : %v\n", c, len(c))
}

输出结果:

1
2
3
slice a : [1 3 4 5] , len(a) : 4
slice b : [3] , len(b) : 1
slice c : [3 4 5] , len(c) : 3

数组和切片的内存布局

null

字符串和切片(string and slice)

string底层就是一个byte的数组,因此,也可以进行切片操作。

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

import (
"fmt"
)

func main() {
str := "hello world"
s1 := str[0:5]
fmt.Println(s1)

s2 := str[6:]
fmt.Println(s2)
}

输出结果:

1
2
hello
world

string本身是不可变的,因此要改变string中字符。需要如下操作:
英文字符串:

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

import (
"fmt"
)

func main() {
str := "Hello world"
s := []byte(str) //中文字符需要用[]rune(str)
s[6] = 'G'
s = s[:8]
s = append(s, '!')
str = string(s)
fmt.Println(str)
}

输出结果:

1
Hello Go!

含有中文字符串

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

import (
"fmt"
)

func main() {
str := "你好,世界!hello world!"
s := []rune(str)
s[3] = '够'
s[4] = '浪'
s[12] = 'g'
s = s[:14]
str = string(s)
fmt.Println(str)
}

输出结果:

1
你好,够浪!hello go

切片切割理解

golang slice data[:6:8] 两个冒号的理解

常规slice , data[6:8],从第6位到第8位(返回6, 7),长度len为2, 最大可扩充长度cap为4(6-9)

另一种写法: data[:6:8] 每个数字前都有个冒号, slice内容为data从0到第6位,长度len为6,最大扩充项cap设置为8

a[x:y:z] 切片内容 [x:y] 切片长度: y-x 切片容量:z-x

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

import (
"fmt"
)

func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
d1 := slice[6:8]
fmt.Println(d1, len(d1), cap(d1))
d2 := slice[:6:8]
fmt.Println(d2, len(d2), cap(d2))
}

数组or切片转字符串:

1
strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)

切片的底层实现

切片的类型

o 数组是值类型,赋值和函数传参操作都会复制整个数组数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
arrayA := [2]int{100, 200}
var arrayB [2]int

arrayB = arrayA

fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)

testArray(arrayA)
}

func testArray(x [2]int) {
fmt.Printf("func Array : %p , %v\n", &x, x)
}

打印结果:

1
2
3
arrayA : 0xc4200bebf0 , [100 200]
arrayB : 0xc4200bec00 , [100 200]
func Array : 0xc4200bec30 , [100 200]

三个内存地址都不同,Go 中数组赋值和函数传参都是值复制的

可以采用指针传值的方式

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
arrayA := []int{100, 200}
testArrayPoint(&arrayA) // 1.传数组指针
arrayB := arrayA[:]
testArrayPoint(&arrayB) // 2.传切片
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
}

func testArrayPoint(x *[]int) {
fmt.Printf("func Array : %p , %v\n", x, *x)
(*x)[1] += 100
}

打印结果:

1
2
3
func Array : 0xc4200b0140 , [100 200]
func Array : 0xc4200b0180 , [100 300]
arrayA : 0xc4200b0140 , [100 400]
切片的数据结构

切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。

给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。

Slice 的数据结构定义如下:

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片扩容

当一个切片的容量满了,就需要扩容了。怎么扩,策略是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&et))
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}

if et.size == 0 {
// 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}

// 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}

// 这里就是扩容的策略
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}

// 计算新的切片的容量,长度。
var lenmem, newlenmem, capmem uintptr
const ptrSize = unsafe.Sizeof((*byte)(nil))
switch et.size {
case 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
newcap = int(capmem)
case ptrSize:
lenmem = uintptr(old.len) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
newcap = int(capmem / et.size)
}

// 判断非法的值,保证容量是在增加,并且容量不超过最大容量
if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
panic(errorString("growslice: cap out of range"))
}

var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
// 在老的切片后面继续扩充容量
p = mallocgc(capmem, nil, false)
// 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
memmove(p, old.array, lenmem)
// 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// 重新申请新的数组给新切片
// 重新申请 capmen 这个大的内存地址,并且初始化为0值
p = mallocgc(capmem, et, true)
if !writeBarrier.enabled {
// 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
memmove(p, old.array, lenmem)
} else {
// 循环拷贝老的切片的值
for i := uintptr(0); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i))
}
}
}
// 返回最终新切片,容量更新为最新扩容之后的容量
return slice{p, old.len, newcap}
}

上述就是扩容的实现。主要需要关注的有两点,一个是扩容时候的策略,还有一个就是扩容是生成全新的内存地址还是在原来的地址后追加。

扩容策略
1
2
3
4
5
6
7
8
9
func main() {
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)
fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
newSlice[1] += 10
fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
}

输出结果:

1
2
3
4
Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8

用图表示出上述过程。

null

如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。

一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。

注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。

注意点
1
2
3
4
5
6
func main() {
slice := []int{10, 20, 30, 40}
for index, value := range slice {
fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
}
}

输出:

1
2
3
4
value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变。

null

由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过 &slice[index] 获取真实的地址。

指针

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

指针地址和指针类型

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

取变量指针的语法如下:

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

其中:

1
2
v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

举个例子:

1
2
3
4
5
6
7
func main() {
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
}

我们来看一下b := &a的图示:

null

指针取值

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

1
2
3
4
5
6
7
8
9
func main() {
//指针取值
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)
}

输出如下:

1
2
3
type of b:*int
type of c:int
value of c:10

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

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

1
2
3
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
9
10
11
12
13
14
package main

import "fmt"

func main() {
var p *string
fmt.Println(p)
fmt.Printf("p的值是%v\n", p)
if p != nil {
fmt.Println("非空")
} else {
fmt.Println("空值")
}
}

new和make

例子:

1
2
3
4
5
6
7
8
9
func main() {
var a *int
*a = 100
fmt.Println(*a)

var b map[string]int
b["测试"] = 100
fmt.Println(b)
}

执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存

new

new是一个内置的函数,它的函数签名如下:

1
func new(Type) *Type

其中,

1
2
1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:

1
2
3
4
5
6
7
8
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}

本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:

1
2
3
4
5
6
func main() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a)
}

make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

1
func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。

本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:

1
2
3
4
5
6
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["测试"] = 100
fmt.Println(b)
}

new与make的区别

1
2
3
1.二者都是用来做内存分配的。
2.make只用于slicemap以及channel的初始化,返回的还是这三个引用类型本身;
3.new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

map

map定义

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

1
map[KeyType]ValueType

其中,

1
2
3
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
6
7
func main() {
userInfo := map[string]string{
"username": "pprof.cn",
"password": "123456",
}
fmt.Println(userInfo) //
}

判断某个键是否存在

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

1
value, ok := map[key]   

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}

map的遍历

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

1
2
3
4
5
6
7
8
9
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}

但我们只想遍历key的时候,可以按下面的写法:

1
2
3
4
5
6
7
8
9
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
for k := range scoreMap {
fmt.Println(k)
}
}

注意: 遍历map时的元素顺序与添加键值对的顺序无关。

使用delete()函数删除键值对

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

1
delete(map, key)  

其中,

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

key:表示要删除的键值对的键

示例代码如下:

1
2
3
4
5
6
7
8
9
10
func main(){
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["王五"] = 60
delete(scoreMap, "小明")//将小明:100从map中删除
for k,v := range scoreMap{
fmt.Println(k, v)
}
}

元素为map类型的切片

下面的代码演示了切片中的元素为map类型时的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "王五"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "红旗大街"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}

值为切片类型的map

下面的代码演示了map中值为切片类型的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}

Map实现原理

key,value存储

最通俗的话说Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组,在存储时key不能重复,当key重复时,value进行覆盖,我们通过key进行hash运算(可以简单理解为把key转化为一个整形数字)然后对数组的长度取余,得到key存储在数组的哪个下标位置,最后将key和value组装为一个结构体,放入数组下标处,看下图:

1
2
3
4
5
length = len(array) = 4
hashkey1 = hash(xiaoming) = 4
index1 = hashkey1% length= 0
hashkey2 = hash(xiaoli) = 6
index2 = hashkey2% length= 2

null

hash冲突

如上图所示,数组一个下标处只能存储一个元素,也就是说一个数组下标只能存储一对key,value, hashkey(xiaoming)=4占用了下标0的位置,假设我们遇到另一个key,hashkey(xiaowang)也是4,这就是hash冲突(不同的key经过hash之后得到的值一样),那么key=xiaowang的怎么存储?
hash冲突的常见解决方法

开放定址法:也就是说当我们存储一个key,value时,发现hashkey(key)的下标已经被别key占用,那我们在这个数组中空间中重新找一个没被占用的存储这个冲突的key,那么没被占用的有很多,找哪个好呢?常见的有线性探测法,线性补偿探测法,随机探测法,这里我们主要说一下线性探测法

线性探测,字面意思就是按照顺序来,从冲突的下标处开始往后探测,到达数组末尾时,从数组开始处探测,直到找到一个空位置存储这个key,当数组都找不到的情况下回扩容(事实上当数组容量快满的时候就会扩容了);查找某一个key的时候,找到key对应的下标,比较key是否相等,如果相等直接取出来,否则按照顺寻探测直到碰到一个空位置,说明key不存在。如下图:首先存储key=xiaoming在下标0处,当存储key=xiaowang时,hash冲突了,按照线性探测,存储在下标1处,(红色的线是冲突或者下标已经被占用了) 再者key=xiaozhao存储在下标4处,当存储key=xiaoliu是,hash冲突了,按照线性探测,从头开始,存储在下标2处 (黄色的是冲突或者下标已经被占用了)

null

拉链法:何为拉链,简单理解为链表,当key的hash冲突时,我们在冲突位置的元素上形成一个链表,通过指针互连接,当查找时,发现key冲突,顺着链表一直往下找,直到链表的尾节点,找不到则返回空,如下图:

null

开放定址(线性探测)和拉链的优缺点

  • 由上面可以看出拉链法比线性探测处理简单
  • 线性探测查找是会被拉链法会更消耗时间
  • 线性探测会更加容易导致扩容,而拉链不会
  • 拉链存储了指针,所以空间上会比线性探测占用多一点
  • 拉链是动态申请存储空间的,所以更适合链长不确定的
Go中Map的使用

直接用代码描述,直观,简单,易理解

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
//直接创建初始化一个mao
var mapInit = map[string]string {"xiaoli":"湖南", "xiaoliu":"天津"}
//声明一个map类型变量,
//map的key的类型是string,value的类型是string
var mapTemp map[string]string
//使用make函数初始化这个变量,并指定大小(也可以不指定)
mapTemp = make(map[string]string,10)
//存储key ,value
mapTemp["xiaoming"] = "北京"
mapTemp["xiaowang"]= "河北"
//根据key获取value,
//如果key存在,则ok是true,否则是flase
//v1用来接收key对应的value,当ok是false时,v1是nil
v1,ok := mapTemp["xiaoming"]
fmt.Println(ok,v1)
//当key=xiaowang存在时打印value
if v2,ok := mapTemp["xiaowang"]; ok{
fmt.Println(v2)
}
//遍历map,打印key和value
for k,v := range mapTemp{
fmt.Println(k,v)
}
//删除map中的key
delete(mapTemp,"xiaoming")
//获取map的大小
l := len(mapTemp)
fmt.Println(l)

看了上面的map创建,初始化,增删改查等操作,我们发现go的api其实挺简单易学的

Go中Map的实现原理

知其然,更得知其所以然,会使用map了,多问问为什么,go底层map到底怎么存储呢?接下来我们一探究竟。map的源码位于 src/runtime/map.go中 map同样也是数组存储的的,每个数组下标处存储的是一个bucket,这个bucket的类型见下面代码,每个bucket中可以存储8个kv键值对,当每个bucket存储的kv对到达8个之后,会通过overflow指针指向一个新的bucket,从而形成一个链表,看bmap的结构,我想大家应该很纳闷,没看见kv的结构和overflow指针啊,事实上,这两个结构体并没有显示定义,是通过指针运算进行访问的。

1
2
3
4
5
6
7
8
9
10
11
//bucket结构体定义 b就是bucket
type bmap{
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
//翻译:top hash通常包含该bucket中每个键的hash值的高八位。
如果tophash[0]小于mintophash,则tophash[0]为桶疏散状态 //bucketCnt 的初始值是8
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt values.
// NOTE: packing all the keys together and then all the values together makes the // code a bit more complicated than alternating key/value/key/value/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8.// Followed by an overflow pointer. //翻译:接下来是bucketcnt键,然后是bucketcnt值。
注意:将所有键打包在一起,然后将所有值打包在一起, 使得代码比交替键/值//值/更复杂。但它允许//我们消除可能需要的填充, 例如map[int64]int8./后面跟一个溢出指针}

看上面代码以及注释,我们能得到bucket中存储的kv是这样的,tophash用来快速查找key值是否在该bucket中,而不同每次都通过真值进行比较;还有kv的存放,为什么不是k1v1,k2v2….. 而是k1k2…v1v2…,我们看上面的注释说的 map[int64]int8,key是int64(8个字节),value是int8(一个字节),kv的长度不同,如果按照kv格式存放,则考虑内存对齐v也会占用int64,而按照后者存储时,8个v刚好占用一个int64,从这个就可以看出go的map设计之巧妙。

null

最后我们分析一下go的整体内存结构,阅读一下map存储的源码,如下图所示,当往map中存储一个kv对时,通过k获取hash值,hash值的低八位和bucket数组长度取余,定位到在数组中的那个下标,hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储,当一个bucket满时,通过overfolw指针链接到下一个bucket。

null

go的map存储源码如下,省略了一些无关紧要的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
//获取hash算法
alg := t.key.alg
//计算hash值
hash := alg.hash(key, uintptr(h.hash0))
//如果bucket数组一开始为空,则初始化
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 定位存储在哪一个bucket中
bucket := hash & bucketMask(h.B)
//得到bucket的结构体
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) +bucket*uintptr(t.bucketsize)))
//获取高八位hash值
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var val unsafe.Pointer
bucketloop:
//死循环
for {
//循环bucket中的tophash数组
for i := uintptr(0); i < bucketCnt; i++ {
//如果hash不相等
if b.tophash[i] != top {
//判断是否为空,为空则插入
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
val = add( unsafe.Pointer(b),
dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize) )
}
//插入成功,终止最外层循环
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
//到这里说明高八位hash一样,获取已存在的key
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
//判断两个key是否相等,不相等就循环下一个
if !alg.equal(key, k) {
continue
}
// 如果相等则更新
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
//获取已存在的value
val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
goto done
}
//如果上一个bucket没能插入,则通过overflow获取链表上的下一个bucket
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}

if inserti == nil {
// all current buckets are full, allocate a new one.
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
val = add(insertk, bucketCnt*uintptr(t.keysize))
}

// store new key/value at insert position
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectvalue() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(val) = vmem
}
typedmemmove(t.key, insertk, key)
//将高八位hash值存储
*inserti = top
h.count++
return val
}

Struct结构体

自定义类型

在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

1
2
//将MyInt定义为int类型
type MyInt int

通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

1
type TypeAlias = Type 

我们之前见过的rune和byte就是类型别名,他们的定义如下:

1
2
type byte = uint8
type rune = int32

类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
var a NewInt
var b MyInt

fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}

结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

结构体的定义

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

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

}

其中:

1
2
3
1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
3.字段类型:表示结构体字段的具体类型。
1
2
3
4
5
6
7
8
9
10
type person struct {
name string
city string
age int8
}

type person1 struct {
name, city string
age int8
}

结构体实例化

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 p = person{
"名字",
"城市",
18,
}
fmt.Println(p)
}

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

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

import (
"fmt"
)

func main() {
var user struct{Name string; Age int}
user.Name = "pprof.cn"
user.Age = 18
fmt.Printf("%#v\n", user)
}

创建指针类型结构体

还可以通过使用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}

从打印的结果中我们可以看出p2是一个结构体指针。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

1
2
3
4
5
var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次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语言帮我们实现的语法糖。

结构体初始化

1
2
3
4
5
6
7
8
9
10
type person struct {
name string
city string
age int8
}

func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}

使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

1
2
3
4
5
6
p5 := person{
name: "pprof.cn",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"pprof.cn", city:"北京", age:18}

也可以对结构体指针进行键值对初始化,例如:

1
2
3
4
5
6
p6 := &person{
name: "pprof.cn",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

1
2
3
4
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}

使用值的列表初始化

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

1
2
3
4
5
6
p8 := &person{
"pprof.cn",
"北京",
18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"pprof.cn", city:"北京", age:18}

使用这种格式初始化时,需要注意:

1
2
3
1.必须初始化结构体的所有字段。
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用。

结构体内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)

输出:

1
2
3
4
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。

方法的定义格式如下:

1
2
3
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}

其中,

1
2
3
1.接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
2.接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
3.方法名、参数列表、返回参数:具体格式与函数定义相同。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Person 结构体
type Person struct {
name string
age int8
}

//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}

//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。

1
2
3
4
5
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}

调用该方法:

1
2
3
4
5
6
func main() {
p1 := NewPerson("测试", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}

值类型的接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}

func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}

什么时候应该使用指针类型接收者

1
2
3
1.需要修改接收者中的值
2.接收者是拷贝代价比较大的大对象
3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Person 结构体Person类型
type Person struct {
string
int
}

func main() {
p1 := Person{
"pprof.cn",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"pprof.cn", int:18}
fmt.Println(p1.string, p1.int) //pprof.cn 18
}

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Address 地址结构体
type Address struct {
Province string
City string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}

func main() {
user1 := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "黑龙江",
City: "哈尔滨",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

嵌套匿名结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Address 地址结构体
type Address struct {
Province string
City string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}

func main() {
var user2 User
user2.Name = "pprof"
user2.Gender = "女"
user2.Address.Province = "黑龙江" //通过匿名结构体.字段名访问
user2.City = "哈尔滨" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}

当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。

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
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}

//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}

//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}

func main() {
var user3 User
user3.Name = "pprof"
user3.Gender = "女"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}

结构体的“继承”

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
//Animal 动物
type Animal struct {
name string
}

func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}

结构体字段的可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

结构体与JSON序列化

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号””包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。

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
//Student 学生
type Student struct {
ID int
Gender string
Name string
}

//Class 班级
type Class struct {
Title string
Students []*Student
}

func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}

结构体标签(Tag)

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

1
`key1:"value1" key2:"value2"`  

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

例如我们为Student结构体的每个字段定义json序列化时使用的Tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}

func main() {
s1 := Student{
ID: 1,
Gender: "女",
name: "pprof",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
}

接口

接口类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

为什么要使用接口

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

func (c Cat) Say() string { return "喵喵喵" }

type Dog struct{}

func (d Dog) Say() string { return "汪汪汪" }

func main() {
c := Cat{}
fmt.Println("猫:", c.Say())
d := Dog{}
fmt.Println("狗:", d.Say())
}

上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?

像类似的例子在我们编程过程中会经常遇到:

比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?

比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?

比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。

接口的定义

Go语言提倡面向接口编程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
接口是一个或多个方法签名的集合。
任何类型的方法集中只要拥有该接口'对应的全部方法'签名。
就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。
这称为Structural Typing
所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
当然,该类型还可以有其他方法。

接口只有方法声明,没有实现,没有数据字段。
接口可以匿名嵌入其他接口,或嵌入到结构中。
对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
只有当接口存储的类型和对象都为nil时,接口才等于nil
接口调用不会做receiver的自动转换。
接口同样支持匿名字段方法。
接口也可实现类似OOP中的多态。
空接口可以作为任何类型数据的容器。
一个类型可实现多个接口。
接口命名习惯以 er 结尾。

每个接口由数个方法组成,接口的定义格式如下:

1
2
3
4
5
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}

其中:

1
2
3
1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
2.方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

1
2
3
type writer interface{
Write([]byte) error
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

我们来定义一个Sayer接口:

1
2
3
4
// Sayer 接口
type Sayer interface {
say()
}

定义dog和cat两个结构体:

1
2
3
type dog struct {}

type cat struct {}

因为Sayer接口里只有一个say方法,所以我们只需要给dog和cat 分别实现say方法就可以实现Sayer接口了。

1
2
3
4
5
6
7
8
9
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

接口类型变量

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。

1
2
3
4
5
6
7
8
9
func main() {
var x Sayer // 声明一个Sayer类型的变量x
a := cat{} // 实例化一个cat
b := dog{} // 实例化一个dog
x = a // 可以把cat实例直接赋值给x
x.say() // 喵喵喵
x = b // 可以把dog实例直接赋值给x
x.say() // 汪汪汪
}

值接收者和指针接收者实现接口的区别

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个Mover接口和一个dog结构体。

1
2
3
4
5
type Mover interface {
move()
}

type dog struct {}

值接收者实现接口

1
2
3
func (d dog) move() {
fmt.Println("狗会动")
}

此时实现接口的是dog类型:

1
2
3
4
5
6
7
8
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
x.move()
}

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

指针接收者实现接口

同样的代码我们再来测试一下使用指针接收者有什么区别:

1
2
3
4
5
6
7
8
9
10
func (d *dog) move() {
fmt.Println("狗会动")
}
func main() {
var x Mover
var wangcai = dog{} // 旺财是dog类型
x = wangcai // x不可以接收dog类型
var fugui = &dog{} // 富贵是*dog类型
x = fugui // x可以接收*dog类型
}

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

下面的代码是一个比较好的面试题

请问下面的代码是否能通过编译?

不能,必须 var peo People = &Student{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type People interface {
Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
if think == "sb" {
talk = "你是个大帅比"
} else {
talk = "您好"
}
return
}

func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}

类型与接口的关系

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。

1
2
3
4
5
6
7
8
9
// Sayer 接口
type Sayer interface {
say()
}

// Mover 接口
type Mover interface {
move()
}

dog既可以实现Sayer接口,也可以实现Mover接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type dog struct {
name string
}

// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}

// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}

func main() {
var x Sayer
var y Mover

var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}
多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。

1
2
3
4
// Mover 接口
type Mover interface {
move()
}

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type dog struct {
name string
}

type car struct {
brand string
}

// dog类型实现Mover接口
func (d dog) move() {
fmt.Printf("%s会跑\n", d.name)
}

// car类型实现Mover接口
func (c car) move() {
fmt.Printf("%s速度70迈\n", c.brand)
}

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

1
2
3
4
5
6
7
8
9
func main() {
var x Mover
var a = dog{name: "旺财"}
var b = car{brand: "保时捷"}
x = a
x.move()
x = b
x.move()
}

上面的代码执行结果如下:

1
2
旺财会跑
保时捷速度70迈

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Sayer 接口
type Sayer interface {
say()
}

// Mover 接口
type Mover interface {
move()
}

// 接口嵌套
type animal interface {
Sayer
Mover
}

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type cat struct {
name string
}

func (c cat) say() {
fmt.Println("喵喵喵")
}

func (c cat) move() {
fmt.Println("猫会动")
}

func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}

空接口

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 定义一个空接口x
var x interface{}
s := "pprof.cn"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
空接口的应用
空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

1
2
3
4
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值

使用空接口实现可以保存任意值的字典。

1
2
3
4
5
6
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

接口值

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

我们来看一个具体的例子:

1
2
3
4
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

请看下图分解:

null

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

1
x.(T) 

其中:

1
2
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

1
2
3
4
5
6
7
8
9
10
func main() {
var x interface{}
x = "pprof.cn"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

1
2
3
4
5
6
7
8
9
10
11
12
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

异常处理

Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic:

1
2
3
4
1、内置函数
2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
4、直到goroutine整个退出,并报告错误

recover:

1
2
3
4
5
1、内置函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议
a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error

注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main

func main() {
test()
}

func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()

panic("panic error!")
}

输出结果:

1
panic error! 

由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。

1
2
func panic(v interface{})
func recover() interface{}

向已关闭的通道发送数据会引发panic

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

import (
"fmt"
)

func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()

var ch chan int = make(chan int, 10)
close(ch)
ch <- 1
}

输出结果:

1
send on closed channel

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

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

import "fmt"

func test() {
defer func() {
fmt.Println(recover())
}()

defer func() {
panic("defer panic")
}()

panic("test panic")
}

func main() {
test()
}

输出:

1
defer panic 

捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。

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() {
defer func() {
fmt.Println(recover()) //有效
}()
defer recover() //无效!
defer fmt.Println(recover()) //无效!
defer func() {
func() {
println("defer inner")
recover() //无效!
}()
}()

panic("test panic")
}

func main() {
test()
}

输出:

1
2
3
defer inner
<nil>
test panic

使用延迟匿名函数或下面这样都是有效的。

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

import (
"fmt"
)

func except() {
fmt.Println(recover())
}

func test() {
defer except()
panic("test panic")
}

func main() {
test()
}

输出结果:

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

import "fmt"

func test(x, y int) {
var z int

func() {
defer func() {
if recover() != nil {
z = 0
}
}()
panic("test panic")
z = x / y
return
}()

fmt.Printf("x / y = %d\n", z)
}

func main() {
test(2, 1)
}

输出结果:

1
x / y = 0

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

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

标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

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

var ErrDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
if y == 0 {
return 0, ErrDivByZero
}
return x / y, nil
}

func main() {
defer func() {
fmt.Println(recover())
}()
switch z, err := div(10, 0); err {
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}

输出结果:

1
division by zero

Go实现类似 try catch 的异常处理

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

import "fmt"

func Try(fun func(), handler func(interface{})) {
defer func() {
if err := recover(); err != nil {
handler(err)
}
}()
fun()
}

func main() {
Try(func() {
panic("test panic")
}, func(err interface{}) {
fmt.Println(err)
})
}

输出结果:

1
test panic

IO流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
package main

import (
"bufio"
"fmt"
"io"
"log"
"os"
)

func main01() {
file := Create("a.txt")
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Fatal("文件关闭失败")
}
}(file)
Write(file)

fp := Create("b.txt")
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Fatal("文件关闭失败")
}
}(fp)
//b := []byte{'h', 'e', 'l', 'l', 'o'}
b := []byte("HelloWorld")
_, _ = fp.Write(b)
}

func Write(file *os.File) {
n, _ := file.WriteString("文件写入")
fmt.Println(n)
n1, _ := file.WriteString("文件hhh")
fmt.Println(n1)
}

// Create 创建文件
func Create(path string) (file *os.File) {
//创建文件 文件路径
file, err := os.Create(path)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println("文件创建成功")
return file
}

func main02() {
//打开文件
file, err := os.OpenFile("a.txt", os.O_RDWR, 6)
defer file.Close()
if err != nil {
fmt.Println(err.Error())
}
//覆盖文件原来的内容进行写入
_, err = file.WriteAt([]byte("芜湖"), 3)
if err != nil {
fmt.Errorf(err.Error())
}
}

func main03() {
//打开文件
file, err := os.OpenFile("a.txt", os.O_RDWR, 6)
defer file.Close()
if err != nil {
fmt.Println(err.Error())
}
//将偏移量移动到文件末尾
seek, err := file.Seek(0, io.SeekEnd)
if err != nil {
fmt.Errorf(err.Error())
}
_, err = file.WriteAt([]byte("芜湖"), seek)
if err != nil {
fmt.Errorf(err.Error())
}
}

func main04() {
//打开文件
file, err := os.Open("b.txt")
if err != nil {
fmt.Errorf(err.Error())
}
b := make([]byte, 1024)
for i := 0; i < len(b); i++ {
file.Read(b)
}
fmt.Println(b)
}

func main05() {
//打开文件
file, err := os.Open("b.txt")
if err != nil {
fmt.Errorf(err.Error())
}
r := bufio.NewReader(file)
b, _ := r.ReadBytes('\n')
fmt.Println(string(b))
b, _ = r.ReadBytes('\n')
}

//字节读取
func main06() {
file, err := os.Open("b.txt")
if err != nil {
fmt.Println(err.Error())
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
fmt.Println("文件关闭失败")
}
}(file)
b := make([]byte, 20)
for {
n, err := file.Read(b)
//表示读取到文件结尾了
if err == io.EOF {
break
}
fmt.Println(string(b[:n]))
}
}

//行读取
func main07() {
file, err := os.Open("b.txt")
if err != nil {
fmt.Println(err.Error())
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
fmt.Println("文件关闭失败")
}
}(file)
r := bufio.NewReader(file)
for {
bytes, err := r.ReadBytes('\n')
fmt.Print(string(bytes))
if err == io.EOF {
break
}
}
}

//文件拷贝
func main() {
file, err := os.Open("labuladong.pdf")
if err != nil {
fmt.Println("文件打开失败")
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
fmt.Println("关闭失败")
}
}(file)

fp, err := os.Create("labuladonghhhh.pdf")
if err != nil {
fmt.Println("文件创建失败")
}
defer func(fp *os.File) {
err := fp.Close()
if err != nil {
fmt.Println("关闭失败")
}
}(fp)

data := make([]byte, 1*1024*1024)
for {
n, err := file.Read(data)
if err == io.EOF {
break
}
fp.Write(data[:n])
}
}

Test测试

例如测试这个方法

1
2
3
4
5
func calcTriangle(a, b int) int {
var c int
c = int(math.Sqrt(float64(a*a + b*b)))
return c
}

可以在同目录下创建对应的 包名_test.go

image-20220418005055464

使用表格驱动测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func Test_calcTriangle(t *testing.T) {
tests := []struct {
a, b, c int
}{
{3, 4, 5},
{8, 15, 17},
{6, 8, 10},
{12, 35, 37},
{30000, 40000, 50000},
}
for _, tt := range tests {
if got := calcTriangle(tt.a, tt.b); got != tt.c {
t.Errorf("calcTriangle() = %v, want %v", got, tt.c)
}
}
}

testing.B是性能测试

1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkName(b *testing.B) {
for i := 0; i < b.N; i++ {
arr := []int{1, 9, 10, 30, 2, 5, 45, 8, 63, 234, 12}
for i := 0; i < len(arr); i++ {
for j := i + 1; j < len(arr); j++ {
if arr[i] > arr[j] {
arr[i], arr[j] = arr[j], arr[i]
}
}
}
}
}

标准库

strings

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

import (
"fmt"
"strconv"
"strings"
)

func main01() {
//判断字符串是否包含在目标字符串里
str := "hello world"
if isCont := strings.Contains(str, "hello"); isCont {
fmt.Println("存在")
} else {
fmt.Println("不存在")
}

//字符串拼接
s := []string{"1234", "3876", "9017", "1859"}
str = strings.Join(s, "-")
fmt.Println(str)

//返回目标字符串第一次出现的字符
str1 := "日照香炉生紫烟"
fmt.Println(strings.Index(str1, "生"))

//字符串重复
str2 := "日照香炉生紫烟"
repeat := strings.Repeat(str2, 2)
fmt.Println(repeat)

//字符串替换
str3 := "日照香炉生紫烟烟"
//源字符,替换字符,替换成的字符,替换次数 -1代表全部替换
replace := strings.Replace(str3, "烟", "hhh", -1)
fmt.Println(replace)

//字符串分割
str4 := "日,照,香,炉,生,紫,烟"
split := strings.Split(str4, ",")
fmt.Println(split)

//去除前后空格
str5 := " 日照香炉生紫烟 "
trim := strings.Trim(str5, " ")
fmt.Println(trim)

str6 := " 日 照 香 炉 生 紫 烟 "
fields := strings.Fields(str6)
fmt.Println(fields)
}

strconv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func main02() {
//字符串转换
formatBool := strconv.FormatBool(true)
fmt.Println(formatBool)

//目标值 进制
s := strconv.FormatInt(123, 10)
fmt.Println(s)
s = strconv.FormatInt(3, 3)
fmt.Println(s)

//数字转成10进制
itoa := strconv.Itoa(10)
fmt.Println(itoa)
//数据 'f' 小数点后几位保留 以几位处理
//会四舍五入
float := strconv.FormatFloat(3.1232224, 'f', 3, 64)
fmt.Println(float)
}

func main() {
//将字符串转成其他类型
str := "false"
parseBool, _ := strconv.ParseBool(str)
fmt.Println(parseBool)
str1 := "111"
fmt.Println(strconv.ParseInt(str1, 10, 64))

atoi, _ := strconv.Atoi(str1)
fmt.Println(atoi)

//将整数转成字符后添加到切片里
b := make([]byte, 0, 1024)
b = strconv.AppendBool(b, false)
b = strconv.AppendInt(b, 10, 10)
fmt.Println(string(b))
}

进阶

并发编程

goroutine

使用goroutine

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

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

启动单个goroutine

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

举个例子如下:

1
2
3
4
5
6
7
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
hello()
fmt.Println("main goroutine done!")
}

这个示例中hello函数和下面的语句是串行的,执行的结果是打印完Hello Goroutine!后打印main goroutine done!。

接下来我们在调用hello函数前面加上关键字go,也就是启动一个goroutine去执行hello这个函数。

1
2
3
4
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
}

这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。为什么呢?

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。

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

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

1
2
3
4
5
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}

执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。

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

启动多个goroutine

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {

for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}

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

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

import (
"fmt"
"runtime"
)

func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
// 主协程
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逻辑核心上实现并行的效果,这里举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}

func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}

func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}

两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}

func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}

func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}

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
var 变量 chan 元素类型  

举几个例子:

1
2
3
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
无缓冲的通道

null

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

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
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}

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

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

有缓冲的通道

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

null

我们可以在使用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结束")
}
优雅的从通道循环取值

当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。当通道被关闭时,往该通道发送值会引发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
// channel 练习
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中从ch2中接收值打印
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}

从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range的方式。

单向通道

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

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是一个只能接收的通道,可以接收但是不能发送。

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

总结

image-20220420210031635

例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import (
"fmt"
"time"
)

var channel = make(chan int)

func printer(s string) {
for _, ch := range s {
fmt.Printf("%c", ch)
time.Sleep(300 * time.Millisecond)
}
}

func person1() {
printer("hello")
channel <- 8
}
func person2() {
<-channel
printer("word")
}

func main01() {
go person1()
go person2()
for {

}
}

func main02() {
ch := make(chan string)
go func() {
for i := 0; i < 2; i++ {
fmt.Println("i=", i)
}
//通知主go打印完毕
ch <- "打印完毕"
}()
str := <-ch
fmt.Println(str)
}

func main03() {
//无缓冲channel
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
fmt.Println("子go程, i = ", i)
ch <- i
}
}()
for i := 0; i < 5; i++ {
num := <-ch
fmt.Println(num)
}

}

func main() {
//无缓冲channel 关闭后还能读,默认值是0
ch := make(chan int)
go func() {
for i := 0; i < 8; i++ {
ch <- i
fmt.Println("子go程 i= ", i, " len : ", len(ch), " cap ", cap(ch))
}
close(ch)
}()
for {
if num, ok := <-ch; ok {
fmt.Println("主go程 读= ", num)
} else {
fmt.Println(<-ch)
break
}
}
}

定时器

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

import (
"fmt"
"time"
)

func main01() {
fmt.Println(time.Now())
//创建定时器
myTime := time.NewTimer(time.Second * 2)
//定时满系统会自动写入时间
nowTime := <-myTime.C
fmt.Println(nowTime)
}

func main02() {
fmt.Println(time.Now())
nowTime := <-time.After(time.Second * 2)
fmt.Println(nowTime)

}
func main03() {
myTimer := time.NewTimer(1 * time.Second)
go func() {
<-myTimer.C
fmt.Println("子go程读取定时完毕")
// 每5分钟执行一次
}()
for {

}
}
func main() {
quit := make(chan bool)
myTicker := time.NewTicker(1 * time.Second)
i := 0
go func() {
for {
<-myTicker.C
i++
//定时执行此方法
fmt.Println("哈哈哈哈哈")
if i == 10 {
quit <- true
}
}
}()
<-quit
}

select

多路复用

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

1
2
3
4
5
6
7
for{
// 尝试从ch1接收值
data, ok := <-ch1
// 尝试从ch2接收值
data, ok := <-ch2

}

这种方式虽然可以实现从多个通道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go内置了select关键字,可以同时响应多个通道的操作。

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

1
2
3
4
5
6
7
8
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
  • select可以同时监听一个或多个channel,直到其中一个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
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结束")
}
  • 可以用于判断管道是否存满
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
package main

import (
"fmt"
"time"
)

// 判断管道有没有存满
func main() {
// 创建管道
output1 := make(chan string, 10)
// 子协程写数据
go write(output1)
// 取数据
for s := range output1 {
fmt.Println("res:", s)
time.Sleep(time.Second)
}
}

func write(ch chan string) {
for {
select {
// 写数据
case ch <- "hello":
fmt.Println("write hello")
default:
fmt.Println("channel full")
}
time.Sleep(time.Millisecond * 500)
}
}
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import (
"fmt"
"time"
)

func main01() {
ch := make(chan int) //数据通信
quit := make(chan bool) //判断是否退出
go func() { //写数据
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
close(ch)
quit <- true //通知主go程退出
close(quit)
}()

go func() {

}()

for {
select {
case data := <-ch:
fmt.Println(data)
case <-quit:
//break //beak跳出select
return //终止进程
}
}
fmt.Println("=============")
}

//chan 实现斐波那契数列
func main() {
ch := make(chan int)
quit := make(chan bool)
go func() {
for i := 1; i <= 20; i++ {
ch <- fib(i)
}
close(ch)
quit <- true //通知主go程退出
close(quit)
}()
for {
select {
case data := <-ch:
fmt.Println(data)
case <-quit:
return
}
}
}

func fib(n int) int {
if n < 3 {
return 1
}
res := make([]int, n+1)
res[1] = 1
res[2] = 1
for i := 3; i <= n; i++ {
res[i] = res[i-1] + res[i-2]
}
return res[n]
}

并发安全和锁

死锁

channel应该在至少2个以上的go程进行通信,否则会发生死锁

使用channel一端读(写),要保证另一端写(读)操作,同时有机会执行。否则死锁

(待)网络编程

(待)反射

(待)Web

Gin

Gorm

连接数据库

1
2
3
4
5
dsn := "用户名:密码@tcp(127.0.0.1:3306)/表名?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}

打印日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Silent, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: false, // 禁用彩色打印
},
)
_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}

迁移

定义表结构
1
2
3
4
5
type Product struct {
gorm.Model
Code string
Price uint
}
1
2
//定义一个表结构,自动生成表
err = db.AutoMigrate(&Product{}) //执行建表的语句

微服务

rpc

protobuf的使用

定义message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
syntax = "proto3";

package proto;

import "google/protobuf/empty.proto";
import "Test/grpc_test/proto/base.proto";
option go_package = "./;proto";

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply); //Greeter接口
rpc Ping (google.protobuf.Empty) returns (Pong);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}
定义方法
1
2
3
4
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply); //Greeter接口
rpc Ping (google.protobuf.Empty) returns (Pong);
}
message嵌套
1
2
3
4
5
6
7
8
9
message HelloReply {
string message = 1;
repeated Result data = 2;
}

message Result {
string name = 1;
string url = 2;
}
生成proto-go文件

shell指令

1
protoc --go_out=plugins=grpc:. helloworld.proto
定义枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
protobuf关键字
repeated

repeated 在message的类型前面加这个关键字,就是数组

1
2
3
4
5
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3; //表示字符串数组/切片string[]
}
required
1
2
3
message SearchResponse {
required Result results = 1; //表示必须传的参数
}
rpc(头部携带信息)metadata

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
//stream流模式调用
conn, _ := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer func(conn *grpc.ClientConn) {
err := conn.Close()
if err != nil {
panic(err)
}
}(conn)
client := proto.NewGreeterClient(conn)
//创建metadata
md := metadata.New(map[string]string{
"token": "666666666666666666666",
})
//将metadata写入写入到context上下文中
ctx := metadata.NewOutgoingContext(context.Background(), md)
//和客户端一起请求发过去
r, _ := client.SayHello(ctx, &proto.HelloRequest{Name: "zwj"})
fmt.Println(r.Message)
}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (s *Server) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
//如果metadata不存在
if md, ok := metadata.FromIncomingContext(ctx); !ok {
//抛异常
fmt.Println("get metadata error")
} else {
//存在并取出
if value, ok := md["token"]; ok {
fmt.Println(value)
}
}

return &proto.HelloReply{
Message: "Hello" + req.Name,
}, nil
}

实战

网络爬虫

拓展技巧

异常处理

如果遇到底层的异常,可以使用第三方异常处理包pkg/errors进行包装wrap,

image-20220512220428303

然后直接返回即可,而不是每个错误产生都去打日志,这样会导致层层日志一直打印,重复打印,在上层调用函数展示错误的堆栈信息,和上下文信息等,上层可以使用%v+,err进行展示堆栈信息

image-20220512220628645

第一条的意思是:如果当前写的这个方法或者项目被很多其他的项目引用或者使用,最好直接返回错误本身,不要进行包装,因为有可能其他的项目调用本项目会产生错误,然后进行了warp包装,导致多重wrap嵌套报错堆栈信息,造成冗余


Golang笔记和技巧记录
https://www.gravity.wang/2022/05/12/go/
Author
Gravity
Posted on
May 12, 2022
Licensed under