GO语言学习笔记

go语言学习笔记

资料

《Go学习路线图》

go语言圣经

Go语言101

https://www.topgoer.com/

位运算符

运算符 描述 实例
& 按位与运算符”&”是双目运算符。 其功能是参与运算的两数各对应的二进位相与。如果对应的位都为1,那么结果就是1, 如果任意一个位是0 则结果就是0 (A & B) 结果为 12, 二进制为 0000 1100
| 按位或运算符”|”是双目运算符。 其功能是参与运算的两数各对应的二进位相或。如果对应的位中任一个操作数为1 那么结果就是1 (A | B) 结果为 61, 二进制为 0011 1101
^ 按位异或运算符”^”是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (A ^ B) 结果为 49, 二进制为 0011 0001
<< 左移运算符”<<”是双目运算符。左移n位就是乘以2的n次方。 其功能把”<<”左边的运算数的各二进位全部左移若干位,由”<<”右边的数指定移动的位数,高位丢弃,低位补0。 A << 2 结果为 240 ,二进制为 1111 0000
>> 右移运算符”>>”是双目运算符。右移n位就是除以2的n次方。 其功能是把”>>”左边的运算数的各二进位全部右移若干位,”>>”右边的数指定移动的位数。 A >> 2 结果为 15 ,二进制为 0000 1111

匿名结构体

这个例子展示了简单的cache,其使用两个包级别的变量来实现,一个mutex互斥量(§9.2)和它所操作的cache:

1
2
3
4
5
6
7
8
9
10
11
var (
mu sync.Mutex // guards mapping
mapping = make(map[string]string)
)

func Lookup(key string) string {
mu.Lock()
v := mapping[key]
mu.Unlock()
return v
}

下面这个版本在功能上是一致的,但将两个包级别的变量放在了cache这个struct一组内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//依次进行了定义、初始化、赋值
var cache = struct {
sync.Mutex
mapping map[string]string
}{
mapping: make(map[string]string),
}


func Lookup(key string) string {
cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}

我们给新的变量起了一个更具表达性的名字:cache。因为sync.Mutex字段也被嵌入到了这个struct里,其Lock和Unlock方法也就都被引入到了这个匿名结构中了,这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。

一个应用的例子:

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

import (
"encoding/json"
"fmt"
)
//定义手机屏幕
type Screen01 struct {
Size float64 //屏幕尺寸
ResX, ResY int //屏幕分辨率 水平 垂直
}
//定义电池容量
type Battery struct {
Capacity string
}

//返回json数据
func getJsonData() []byte {
//tempData 接收匿名结构体(匿名结构体使得数据的结构更加灵活)
tempData := struct {
Screen01
Battery
HashTouchId bool // 是否有指纹识别
}{
Screen01: Screen01{Size: 12, ResX: 36, ResY: 36},
Battery: Battery{"6000毫安"},
HashTouchId: true,
}
jsonData, _ := json.Marshal(tempData) //将数据转换为json
return jsonData
}
func main() {
jsonData := getJsonData() //获取json数据
fmt.Println(jsonData)
fmt.Println("=========解析(分离)出的数据是===========")
//自定义匿名结构体,解析(分离)全部数据
allData := struct {
Screen01
Battery
HashTouchId bool
}{}
json.Unmarshal(jsonData, &allData)
fmt.Println("解析(分离)全部结构为:", allData)
//自定义匿名结构体,通过json数据,解析(分离)对应的结构(可以是部分结构)
screenBattery := struct {
Screen01
Battery
}{}
json.Unmarshal(jsonData, &screenBattery) //注意:此处只能为结构体指针(一般参数为interface{},都采用地址引用(即地址传递))
fmt.Println("解析(分离)部分结构:", screenBattery)
//自定义匿名结构体,解析(分离)部分结构
batteryTouch := struct {
Battery
isTouch bool
}{}
json.Unmarshal(jsonData, &batteryTouch)
fmt.Println("解析(分离)部分结构:", batteryTouch)
//自定义匿名结构体,解析(分离)部分不存在的结构
temp1 := struct {
Battery
Detail struct {
Name string
Price uint16
}
}{}
json.Unmarshal(jsonData, &temp1)
fmt.Println("解析(分离)部分不存在的结构", temp1)
//自定义匿名结构体,解析(分离)完全不存在的结构
temp2 := struct {
User string
Price uint16
}{}
json.Unmarshal(jsonData, &temp2)
fmt.Println("解析(分离)完全不存在的结构:", temp2)
}

封装

在Go语言中,我们可以对结构体的字段进行封装,并通过结构体中的方法来操作内部的字段。如果结构体中字段名的首字母是小写字母,那么这样的字段是私有的,相当于private字段。外部包裹能直接访问,如果是在名的首字母是大写字母,那么这样的字段对外暴露的,相当于public字段。能够起的方法也是一样的,如果方法名首字母是大写字母,那么这样的方法对外暴露的。

封装的好处:

  • 隐藏实现细节;
  • 可以对数据进行验证,保证数据安全合理。

如何体现封装:

  • 对结构体中的属性进行封装;
  • 通过方法,包,实现封装。

封装的实现步骤:

  1. 将结构体、字段的首字母小写;
  2. 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似一个构造函数;
  3. 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值;
  4. 提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值。

例如:

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

import "fmt"

type person struct {
Name string
age int //其它包不能直接访问..
sal float64
}

//写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {
return &person{
Name : name,
}
}

//为了访问age 和 sal 我们编写一对SetXxx的方法和GetXxx的方法
func (p *person) SetAge(age int) {
if age >0 && age <150 {
p.age = age
} else {
fmt.Println("年龄范围不正确..")
//给程序员给一个默认值
}
}
func (p *person) GetAge() int {
return p.age
}

func (p *person) SetSal(sal float64) {
if sal >= 3000 && sal <= 30000 {
p.sal = sal
} else {
fmt.Println("薪水范围不正确..")
}
}

func (p *person) GetSal() float64 {
return p.sal
}

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

import (
"fmt"
"mytest/encapsulation/model"
)

func main() {

p := model.NewPerson("smith")
p.SetAge(18)
p.SetSal(5000)
fmt.Println(p)
fmt.Println(p.Name, " age =", p.GetAge(), " sal = ", p.GetSal())
}

切片

slice的结构体

1
2
3
4
5
type slice struct {
array unsafe.Pointer // 指针,指向底层数组
len int // slice长度,即当前slice可以访问的范围
cap int // slice容量,当前slice可访问底层数组的最大范围,如果cap不够,则会执行扩容操作
}

切片和数组的关系

  1. 切片的本质是操作数组,只是数组是固定长度的,而切片的长度可变的
  2. 切片是引用类型,可以理解为引用数组的一个片段;而数组是值类型,把数组A赋值给数组B,会为数组B开辟新的内存空间,修改数组B的值并不会影响数组A。而切片作为引用类型,指向同一个内存地址,是会互相影响的。

切片的长度:元素的个数

切片的容量:在切片引用的底层数组中从切片的第一个元素到数组最后一个元素的长度(元素数量)

所以判断一个切片是否为空,使用len(s) == 0 判断,不能使用 s==nil 判断

1
2
3
4
5
6
7
8
9
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}

s5 := a1[:4] //[1 2 3 4]
s6 := a1[2:] //[3 4 5 6 7 8 9]
s7 := a1[:] //[1 2 3 4 5 6 7 8 9]

fmt.Printf("len(s5):%d cap(s5):%d\\n", len(s5), cap(s5)) //4 9
fmt.Printf("len(s6):%d cap(s6):%d\\n", len(s6), cap(s6)) //7 7
fmt.Printf("len(s7):%d cap(s7):%d\\n", len(s7), cap(s7)) //9 9

注意:slice是引用类型

当底层数组改变时,不管是切片,还是切片再切片,值都会改变。因为他们使用的是一个内存块,引用的一个内存地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义数组
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
//有数组切割成切片s6
s6 := a1[2:] //[3 4 5 6 7 8 9]
//切片再次切片,赋值给s8
s8 :=s6[3:] //[6 7 8 9]
//修改原始数组,把下标为2的值由3改为333
a1[2] = 333
//打印s6,发现s6中的3也变成了333
fmt.Println("s6:", s6) //[333 4 5 6 7 8 9]
//因为s8基于s6切片而成,我们测试一下切片再切片的引用传的
fmt.Println("s8:", s8) //[6 7 8 9]
//我们把原始数组下标为5的值由6改为666
a1[5] = 666
//打印s8切片,得到结果6也变成了666
fmt.Println("s8:", s8) //[666 7 8 9]

生成切片

初始化一个 slice 有两种方式:

  • 直接声明: 比如 var s []int
  • 使用 make 关键字,比如: s := make([]int, 0)

区别:

  • 直接声明 slice 的方式内部是不申请内存空间的,slice 内部 array 指针指向 null。
  • 使用 make 关键字会申请包含 0 个元素的内存空间,底层 array 指针指向申请的内存。

使用json.Marshal序列化的结果是有区别的。

  • json.Marshal(直接声明): 返回 null
  • json.Marshal(make关键字初始化): 返回 []

make()函数的第一个参数指定切片的数组类型,第二个参数指定切片的长度,第三个参数指定切片的容量。

1
2
s1 := make([]int,5,10)
fmt.Printf("s1:%v len(s1):%d cap(s1):%d\\n", s1, len(s1), cap(s1))

扩容机制

只有 append 操作可以触发 slice 的扩容

slice 在初始化时只会申请有限的内存空间,而随着 append 元素的增多,当元素超过当前 slice 的 cap ,就会重新申请一段新内存,把原数据 copy 到这个新内存上,然后 slice 把内部的指针指向这段新内存。

  1. 如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
  2. 如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
  3. 如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
  4. 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
newcap := old.cap
doublecap := newcap + newcap // 两倍扩容
if cap > doublecap { // 待扩容大小大于原切片的两倍,则按照待扩容大小处理
newcap = cap
} else {
if old.len < 1024 { // 当原切片长度小于1024时,新切片的容量会直接翻倍。
newcap = doublecap
} else { // 当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}

demo

如果在函数内部发生了扩容,这时再修改 slice 中的值是不起作用的,因为修改发生在新的 array 内存中,对老的 array 内存不起作用

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 main() {
s := make([]int, 3,8)
s[0] = 0
s[1] = 1
s[2] = 2
fmt.Println("s =",s, cap(s)) //s = [0 1 2] 8
s1 := s;
// 指向同一数组(s还有容量可以扩容,所以其实s和s1其实是都指向同一数组的,可以理解为浅拷贝)
s1 = append(s,3,4)
s1[1] = 123
fmt.Println("s =",s, cap(s)) //s = [0 123 2] 8
fmt.Println("s1=",s1, cap(s1)) //s1= [0 123 2 3 4] 8

// s没有容量扩容了,新slice和s指向不同数组(可以理解为执行了slice的深拷贝)
s2 := append(s1,1,2,3,4,5)
fmt.Println("s2=",s2, cap(s2)) //s2= [0 123 2 3 4 1 2 3 4 5] 16
s2[2] = 123
fmt.Println("s =",s, cap(s)) //s = [0 123 2] 8
fmt.Println("s2=",s2, cap(s2)) //s2= [0 123 123 3 4 1 2 3 4 5] 16
}

复制

copy方法是复制了一份,开辟了新的内存空间,不再引用s1的内存地址

1
2
3
4
5
6
7
8
9
10
11
12
13
//定义切片s1
s1 := []int{1, 2, 3}

//第一种方式:直接声明变量 用=赋值
//s2切片和s1引用同一个内存地址
var s2 = s1

//第二种方式:copy
var s3 = make([]int, 3)
copy(s3, s1) //使用copy函数将 参数2的元素复制到参数1

s1[0] = 11
fmt.Printf("s1:%v s2:%v s3:%v",s1, s2, s3) //s1和s2是[11 2 3] s3是[1 2 3]

删除

删除切片中的元素 不能直接删除 可以组合使用分割+append的方式删除切片中的元素

1
2
3
s3 := []int{1, 2, 3}
s3 = append(s3[:1], s3[2:]...) //第一个不用拆开 原因是一个作为被接受的一方 是把后面的元素追加到第一个
fmt.Println(s3)

rune

rune它是int32的别名(-2147483648~2147483647),相比于byte(-128~127),可表示的字符更多。由于rune可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用rune。

字符串修改是不能直接修改的,需要转成rune切片后再修改

1
2
3
4
s2 := "小白兔"
s3 := []rune(s2) //把字符串强制转成rune切片
s3[0] = '大' //注意 这里需要使用单引号的字符,而不是双引号的字符串
fmt.Println(string(s3)) //把rune类型的s3强转成字符串

只要是双引号包裹的类型就是string,只要是单引号包裹的类型就是int32,也就是rune。和中英文无关。

1
2
3
4
5
6
c1 := "红"
c2 := '红'
fmt.Printf("c1的类型:%T c2的类型:%T \\n", c1, c2) //c1的类型:string c2的类型:int32
c3 := "H"
c4 := 'H'
fmt.Printf("c3的类型:%T c4的类型:%T \\n", c3, c4) //c3的类型:string c4的类型:int32

interface

Interface 是一个定义了方法签名的集合,用来指定对象的行为,如果对象做到了 Interface 中方法集定义的行为,那就可以说实现了 Interface;

这些方法可以在不同的地方被不同的对象实现,这些实现可以具有不同的行为;

interface 的主要工作仅是提供方法名称签名,输入参数,返回类型。最终由具体的对象来实现方法,比如 struct;

interface 初始化值为 nil;

golang接口定义不能包含变量,但是允许不带任何方法,这种类型的接口叫 empty interface

使用 type 关键字来申明,interface 代表类型,大括号里面定义接口的方法签名集合。

1
2
3
4
type Animal interface {
Bark() string
Walk() string
}

如下,Dog 实现了 Animal 接口,所以可以用 Animal 的实例去接收 Dog的实例,必须是同时实现 Bark() 和Walk() 方法,否则都不能算实现了Animal接口。

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
type Dog struct {
name string
}

func (dog Dog) Bark() string {
fmt.Println(dog.name + ":wan wan wan!")
return "wan wan wan"
}

func (dog Dog) Walk() string {
fmt.Println(dog.name + ":walk to park!")
return "walk to park"
}

func main() {
var animal Animal

fmt.Println("animal value is:", animal) //animal value is: <nil>
fmt.Printf("animal type is: %T\\n", animal) //animal type is: <nil>

animal = Dog{"旺财"}
animal.Bark() //旺财:wan wan wan!
animal.Walk() //旺财:walk to park!

fmt.Println("animal value is:", animal) //animal value is: {旺财}
fmt.Printf("animal type is: %T\\n", animal) //animal type is: main.Dog
}

nil interface

官方定义:Interface values with nil underlying values:

  • 只声明没赋值的interface 是nil interface,value和 type 都是 nil
  • 只要赋值了,即使赋了一个值为nil类型,也不再是nil interface
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
type I interface {
Hello()
}

type S []int

func (i S) Hello() {
fmt.Println("hello")
}
func main() {
var i I
fmt.Printf("1:i Type:%T\\n", i)
fmt.Printf("2:i Value:%v\\n", i)

var s S
if s == nil {
fmt.Printf("3:s Value%v\\n", s)
fmt.Printf("4:s Type is %T\\n", s)
}

i = s
if i == nil {
fmt.Println("5:i is nil")
} else {
fmt.Printf("6:i Type:%T\\n", i)
fmt.Printf("7:i Value:%v\\n", i)
}
}

output:

其中把值为 nil 的变量 s 赋值i后,i 不再为nil interface

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
	1:i Type:<nil>
2:i Value:<nil>
3:s Value[]
4:s Type is main.S
6:i Type:main.S
7:i Value:[]
package main

import (
"fmt"
"reflect"
)

type State struct{}

func testnil1(a, b interface{}) bool {
return a == b
}

func testnil2(a *State, b interface{}) bool {
return a == b
}

func testnil3(a interface{}) bool {
return a == nil
}

func testnil4(a *State) bool {
return a == nil
}

func testnil5(a interface{}) bool {
v := reflect.ValueOf(a)
return !v.IsValid() || v.IsNil()
}

func main() {
var a *State
fmt.Println(testnil1(a, nil)) //false
fmt.Println(testnil2(a, nil)) //false
fmt.Println(testnil3(a)) //false
fmt.Println(testnil4(a)) //true
fmt.Println(testnil5(a)) //true
}

反射

反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:

1
(value, type)

基本功能

reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型

reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 “Allen.Wu” 25} 这样的结构体struct的值

也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种

获取接口interface信息

当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。

已知原有类型

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

func main() {
var num float64 = 1.2345

pointer := reflect.ValueOf(&num)
value := reflect.ValueOf(num)

// 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
// Golang 对类型要求非常严格,类型一定要完全符合
// 如下两个,一个是*float64,一个是float64,如果弄混,则会panic
convertPointer := pointer.Interface().(*float64)
convertValue := value.Interface().(float64)

fmt.Println(convertPointer)
fmt.Println(convertValue)
}

//运行结果:
//0xc42000e238
//1.2345

未知原有类型

进行遍历探测其Filed

通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumField进行遍历
  2. 再通过reflect.Type的Field获取其Field
  3. 最后通过Field的Interface()得到对应的value

通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumMethod进行遍历
  2. 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
  3. 最后对结果取其Name和Type得知具体的方法名
  4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是一样的判断处理方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func (u User) ReflectCallFunc() {
fmt.Println("Allen.Wu ReflectCallFunc")
}

func main() {

user := User{1, "Allen.Wu", 25}

DoFiledAndMethod(user)

}

// 通过接口来获取任意参数,然后一一揭晓
func DoFiledAndMethod(input interface{}) {

getType := reflect.TypeOf(input)
fmt.Println("get Type is :", getType.Name())

getValue := reflect.ValueOf(input)
fmt.Println("get all Fields is:", getValue)

// 获取方法字段
// 1. 先获取interface的reflect.Type,然后通过NumField进行遍历
// 2. 再通过reflect.Type的Field获取其Field
// 3. 最后通过Field的Interface()得到对应的value
for i := 0; i < getType.NumField(); i++ {
field := getType.Field(i)
value := getValue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}

// 获取方法
// 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历
for i := 0; i < getType.NumMethod(); i++ {
m := getType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}

//运行结果:
//get Type is : User
//get all Fields is: {1 Allen.Wu 25}
//Id: int = 1
//Name: string = Allen.Wu
//Age: int = 25
//ReflectCallFunc: func(main.User)

设置变量值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

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

import (
"fmt"
"reflect"
)

func main() {

var num float64 = 1.2345
fmt.Println("old value of pointer:", num)

// 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
pointer := reflect.ValueOf(&num)
newValue := pointer.Elem()

fmt.Println("type of pointer:", newValue.Type())
fmt.Println("settability of pointer:", newValue.CanSet())

// 重新赋值
newValue.SetFloat(77)
fmt.Println("new value of pointer:", num)

////////////////////
// 如果reflect.ValueOf的参数不是指针,会如何?
pointer = reflect.ValueOf(num)
//newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}

//运行结果:
//old value of pointer: 1.2345
//type of pointer: float64
//settability of pointer: true
//new value of pointer: 77

调用方法

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

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func (u User) ReflectCallFuncHasArgs(name string, age int) {
fmt.Println("ReflectCallFuncHasArgs name: ", name, ", age:", age, "and origal User.Name:", u.Name)
}

func (u User) ReflectCallFuncNoArgs() {
fmt.Println("ReflectCallFuncNoArgs")
}

// 如何通过反射来进行方法的调用?
// 本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call

func main() {
user := User{1, "Allen.Wu", 25}

// 1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
getValue := reflect.ValueOf(user)

// 一定要指定参数为正确的方法名
// 2. 先看看带有参数的调用方法
methodValue := getValue.MethodByName("ReflectCallFuncHasArgs")
args := []reflect.Value{reflect.ValueOf("wudebao"), reflect.ValueOf(30)}
methodValue.Call(args)

// 一定要指定参数为正确的方法名
// 3. 再看看无参数的调用方法
methodValue = getValue.MethodByName("ReflectCallFuncNoArgs")
args = make([]reflect.Value, 0)
methodValue.Call(args)
}


//运行结果:
//ReflectCallFuncHasArgs name: wudebao , age: 30 and origal User.Name: Allen.Wu
//ReflectCallFuncNoArgs

defer

defer关键字传入的函数会在函数返回之前运行。

存入的内容以先进后出的方式输出

1
2
3
4
5
6
7
8
9
func main() {
{
defer fmt.Println("defer1 runs")
fmt.Println("block ends")
defer fmt.Println("defer2 runs")
}

fmt.Println("main ends")
}

输出:

1
2
3
4
block ends
main ends
defer2 runs
defer1 runs

调用 defer关键字会立刻拷贝函数中引用的外部参数

1
2
3
4
5
6
7
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt)) //0s
defer func() { fmt.Println(time.Since(startedAt)) }() //1s

time.Sleep(time.Second)
}

Goroutines和Channels

并发和协程

并发协程相关知识

通道

不要让计算通过共享内存来通讯,而应该让它们通过通讯来共享内存。通道机制就是这种哲学的一个设计结果

我们可以把一个通道看作是在一个程序内部的一个先进先出(FIFO:first in first out)数据队列。 一些协程可以向此通道发送数据,另外一些协程可以从此通道接收数据。

通道可以是双向的,也可以是单向的。

  • 字面形式chan T表示一个元素类型为T的双向通道类型。 编译器允许从此类型的值中接收和向此类型的值中发送数据。
  • 字面形式chan<- T表示一个元素类型为T的单向发送通道类型。 编译器不允许从此类型的值中接收数据。
  • 字面形式<-chan T表示一个元素类型为T的单向接收通道类型。 编译器不允许向此类型的值中发送数据。

一个容量为0的通道值称为一个非缓冲通道(unbuffered channel),一个容量不为0的通道值称为一个缓冲通道(buffered channel)。

1
2
3
ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

当一个通道值被赋给另一个通道值后,这两个通道值将共享相同的底层部分。 换句话说,这两个通道引用着同一个底层的内部通道对象。 比较这两个通道的结果为true

通道的操作

  1. 调用内置函数close来关闭一个通道:

    1
    close(ch)

    传给close函数调用的实参必须为一个通道值,并且此通道值不能为单向接收的。

  2. 使用下面的语法向通道ch发送一个值v

    1
    ch <- v

    v必须能够赋值给通道ch的元素类型。 ch不能为单向接收通道。 <-称为数据发送操作符。

  3. 使用下面的语法从通道ch接收一个值:如果一个通道操作不永久阻塞,它总会返回至少一个值,此值的类型为通道ch的元素类型。 ch不能为单向发送通道。 <-称为数据接收操作符,是的它和数据发送操作符的表示形式是一样的。在大多数场合下,一个数据接收操作可以被认为是一个单值表达式。 但是,当一个数据接收操作被用做一个赋值语句中的唯一的源值的时候,它可以返回第二个可选的类型不确定的布尔值返回值从而成为一个多值表达式。 此类型不确定的布尔值表示第一个接收到的值是否是在通道被关闭前发送的。 (从后面的章节,我们将得知我们可以从一个已关闭的通道中接收到无穷个值。)数据接收操作在赋值中被用做源值的例子:

    1
    <-ch
    1
    2
    v = <-ch
    v, sentBeforeClosed = <-ch
  4. 查询一个通道的容量:

    1
    cap(ch)

    cap的返回值的类型为内置类型int

  5. 查询一个通道的长度:

    1
    len(ch)

len的返回值的类型也为内置类型int。 一个通道的长度是指当前有多少个已被发送到此通道但还未被接收出去的元素值。

  • 如果一个通道已经关闭了,则它的发送数据协程队列和接收数据协程队列肯定都为空,但是它的缓冲队列可能不为空。
  • 在任何时刻,如果缓冲队列不为空,则接收数据协程队列必为空。
  • 在任何时刻,如果缓冲队列未满,则发送数据协程队列必为空。
  • 如果一个通道是缓冲的,则在任何时刻,它的发送数据协程队列和接收数据协程队列之一必为空。
  • 如果一个通道是非缓冲的,则在任何时刻,一般说来,它的发送数据协程队列和接收数据协程队列之一必为空, 但是有一个例外:一个协程可能在一个select流程控制中同时被推入到此通道的发送数据协程队列和接收数据协程队列中。

一个简单的通过一个非缓冲通道实现的请求/响应的例子:

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

import (
"fmt"
"time"
)

func main() {
c := make(chan int) // 一个非缓冲通道
go func(ch chan<- int, x int) {
time.Sleep(time.Second)
// <-ch // 此操作编译不通过
ch <- x*x // 阻塞在此,直到发送的值被接收
}(c, 3)
done := make(chan struct{})
go func(ch <-chan int) {
n := <-ch // 阻塞在此,直到有值发送到c
fmt.Println(n) // 9
// ch <- 123 // 此操作编译不通过
time.Sleep(time.Second)
done <- struct{}{}
}(c)
<-done // 阻塞在此,直到有值发送到done
fmt.Println("bye")
}

//输出:
//9
//bye

缓冲通道的例子

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() {
c := make(chan int, 2) // 一个容量为2的缓冲通道
c <- 3
c <- 5
close(c)
fmt.Println(len(c), cap(c)) // 2 2
x, ok := <-c
fmt.Println(x, ok) // 3 true
fmt.Println(len(c), cap(c)) // 1 2
x, ok = <-c
fmt.Println(x, ok) // 5 true
fmt.Println(len(c), cap(c)) // 0 2
x, ok = <-c
fmt.Println(x, ok) // 0 false
x, ok = <-c
fmt.Println(x, ok) // 0 false
fmt.Println(len(c), cap(c)) // 0 2
close(c) // 此行将产生一个恐慌
c <- 7 // 如果上一行不存在,此行也将产生一个恐慌。

select流程控制

多路复用可以简单地理解为,N 个 channel 中,任意一个 channel 有数据产生,select 都可以监听到,然后执行相应的分支,接收数据并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func main() {
//声明三个存放结果的channel

firstCh := make(chan string)
secondCh := make(chan string)
threeCh := make(chan string)

//同时开启3个goroutine下载

go func() {
firstCh <- downloadFile("firstCh")
}()

go func() {
secondCh <- downloadFile("secondCh")
}()

go func() {
threeCh <- downloadFile("threeCh")
}()

//开始select多路复用,哪个channel能获取到值,就说明哪个最先下载好,就用哪个。

select {

case filePath := <-firstCh:
fmt.Println(filePath)

case filePath := <-secondCh:
fmt.Println(filePath)

case filePath := <-threeCh:
fmt.Println(filePath)
}
}

func downloadFile(chanName string) string {
//模拟下载文件,可以自己随机time.Sleep点时间试试

time.Sleep(time.Second)
return chanName+":filePath"
}

共享变量的并发

深入源码

出现的问题

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

import "fmt"

var a, b int

func f() {
a = 1
b = 2
}

func g() {
fmt.Println("b=",b)
fmt.Println("a=",a)
}

func main() {
go f()
g()
}

image-20220813225331713

以上代码有四种可能的输出

1
2
3
4
b=2,a=1  //1-2-3-4
b=0,a=0 //3-4-1-2
b=0,a=1 //3-1-2-4
b=2,a=0 //2-3-4-1

无论任何时候,只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。并且,在不影响语言规范对 goroutine 的行为定义的时候,编译器和 CPU 会对读取和写入的顺序进行重新排序。

所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。

sync.Mutex互斥锁

可以使用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var (
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)

func Deposit(amount int) {
sema <- struct{}{} // acquire token
balance = balance + amount
<-sema // release token
}

func Balance() int {
sema <- struct{}{} // acquire token
b := balance
<-sema // release token
return b
}

sync包里的Mutex类型可以直接支持。它的Lock方法能够获取到token(这里叫锁),并且Unlock方法会释放这个token。

如果其它的goroutine已经获得了这个锁的话,这个操作会被阻塞直到其它goroutine调用了Unlock使该锁变回可用状态。mutex会保护共享变量。

尽量使用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
import "sync"

var (
mu sync.Mutex // guards balance
balance int
)

func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
deposit(-amount)
if balance < 0 {
deposit(amount)
return false // insufficient funds
}
return true
}

func Deposit(amount int) {
mu.Lock()
defer mu.Unlock()
deposit(amount)
}

func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}

// This function requires that the lock be held.
func deposit(amount int) { balance += amount }

没法对一个已经锁上的mutex来再次上锁–这会导致程序死锁,没法继续执行下去,Withdraw会永远阻塞下去。

这样的写法是错误的

1
2
3
4
5
6
7
8
9
10
11
// NOTE: incorrect!
func Withdraw(amount int) bool {
mu.Lock()
defer mu.Unlock()
Deposit(-amount)
if Balance() < 0 {
Deposit(amount)
return false // insufficient funds
}
return true
}

sync.RWMutex读写锁

允许多个只读操作并行执行,但写操作会完全互斥

RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。

sync.WaitGroup

一个 WaitGroup 对象可以等待一组协程结束 简单使用就是在创建一个任务的时候wg.Add(1), 任务完成的时候使用wg.Done()来将任务减一。使用wg.Wait()来阻塞等待所有任务完成。

例子

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

type httpPkg struct{}

func (httpPkg) Get(url string) {}

var http httpPkg

func main() {
var wg sync.WaitGroup
var urls = []string{
"<http://www.golang.org/>",
"<http://www.google.com/>",
"<http://www.somestupidname.com/>",
}
for _, url := range urls {
// Increment the WaitGroup counter.
wg.Add(1)
// Launch a goroutine to fetch the URL.
go func(url string) {
// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)
}(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()
}

注意:golang里如果方法传递的不是地址,那么就会做一个拷贝,这里调用的wg根本就不是一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
var wg sync.WaitGroup
ch := make(chan int, 1000)
for i := 0; i < 1000; i++ {
wg.Add(1)
go doSomething(i, wg, ch)
}
wg.Wait()
fmt.Println("all done")
for i := 0; i < 1000; i++ {
dd := <-ch
fmt.Println("from ch:"+strconv.Itoa(dd))
}
}

func doSomething(index int, wg sync.WaitGroup, ch chan int) {
defer wg.Done()
fmt.Println("start done:" + strconv.Itoa(index))
//time.Sleep(20 * time.Millisecond)
ch <- index

应该改为

1
2
3
4
5
go doSomething(i, &wg, ch)

func doSomething(index int, wg *sync.WaitGroup, ch chan int) {
...
}

sync.Once初始化

sync.Once可以保证go程序在运行期间的某段代码只会执行一次,作用与init类似,但是也有所不同:

  • init函数是在文件包首次被加载的时候执行,且只执行一次。
  • sync.Once是在代码运行中需要的时候执行,且只执行一次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
var (
o sync.Once
wg sync.WaitGroup
)

for i := 0; i < 10; i++ {
wg.Add(1)

go func(i int) {
defer wg.Done()
o.Do(func() {
fmt.Println("once", i)
})
}(i)
}

wg.Wait()
}

//输出:once 9

看看源码

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
// Once is an object that will perform exactly one action.
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/x86),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}

func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}

func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

在Do()中首先原子性的读取done字段的值是否改变,没有改变则执行doSlow()方法.

一进入doslow()方法就开始执行加锁操作,这样在并发情况下可以保证只有一个线程会执行,再判断一次当前done字段是否发生改变(为什么这里还要在判断一次flag?这里目的其实就是保证并发的情况下,代码块也只会执行一次,毕竟加锁是在doslow()方法内,不加这个判断的在并发情况下就会出现其他goroutine也能执行f()),如果未发生改变,则开始执行代码块,代码块运行结束后会对done字段做原子操作,标识该代码块已经被执行过了.

竞争条件检测

Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)。

只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。

由于需要额外的记录,因此构建时加了竞争检测的程序跑起来会慢一些,且需要更大的内存

race使用指南


GO语言学习笔记
http://sissice.github.io/2022/08/13/GO语言学习笔记/
作者
Sissice
发布于
2022年8月13日
许可协议