Go
函数传参是值类型还是引用类型?
无论如何都是值传递。
基本类型(int, string等):拷贝值
结构体:拷贝值
指针类型:拷贝值,但由于指针指向变量的地址,所以能实现修改值
slice/map/channel:依旧拷贝值,能实现修改值是因为内部有指针指向同一个数组。
比如 slice 的本质是:
1
2
3
4
5type slice struct {
ptr *T // 指针变量共享一个数组
len int
cap int
}
数组和切片的区别?
长度角度:
- 数组长度不可变,在定义时就确定了,
[3]int和[4]int不是一个类型 - 切片长度可变。可使用
append()来扩容切片
- 数组长度不可变,在定义时就确定了,
是否为值类型角度:
数组是值类型,值拷贝
1
2
3
4a := [3]int{1,2,3}
b := a
b[0] = 0
fmt.Println(a) // [1 2 3]切片是引用类型,底层共享数组
1
2
3
4s1 := []int{1,2,3}
s2 := s1
s2[0] = 100
fmt.Println(s1) // [100 2 3]
底层实现角度:
- slice本质是一个结构体,三个属性:数组指针、长度len、容量cap,指针指向了底层数组。切片可以视作数组的窗口(view)。
说说空结构体 struct{}
空结构体的大小:0,也就是说他不占内存空间
1
fmt.Println(unsafe.Sizeof(struct{}{})) // 0我们可以用
struct{}作为 map 的值来模拟一个 set。1
2
3
4
5
6
7
8
9
10
11type Set map[string]struct{} // typedef
set := make(Set)
// 添加数据
for _, v := range []string{"a", "a", "b", "c"} {
set[v] = struct{}{}
}
// 检查
if _, ok := set["a"] {
fmt.Println("a exists")
}给管道发送空结构体,代表一种信号,可以节省空间,代替 bool
1
ch <- struct{}{}
init() 函数是什么时候执行的?
- init() 函数是 golang 中用来初始化包的函数,在 main() 函数之前自动执行,可以放一些字段的初始化逻辑。
- 一个 go pkg的初始化流程:依赖包 import -> 常量 -> 变量 -> init() -> main()(如果有)
- 一个包可以有多个init(),执行时不保证先后顺序。
两个 interface 可以比较吗?
可以。interface可以用 == 比较。一看interface 的动态类型,二看interface 的实际值。
- 一个 interface 内部包含两部分:
(type, value) - 比较时,一看 动态type:
- type一致,往下继续看值
- type不一致,返回 false
- type一致,但不是“可比较类型”,返回 false
- 不可比较类型有:slice, map, func 等,channel 可比较
- 二看 实际value,一致则返回 true:
- 两个
nil,一致 - 一个
nil一个非nil,不一致 - 两个值类型(int, string)或结构体内部字段值 相等,一致
- 两个指针类型,要看是不是指向同一个内存地址,地址相同则一致
- 两个
几个典型示例:
1 | |
Slice
切片源码见 runtime/slice.go
Slice底层结构
底层三个属性
1 | |
slice的扩容机制
源码见 growslice() 和 nextslicecap() 方法,后者计算新容量。
计算新容量的代码很直接:
如果
期望长度 > 2 x 旧容量,取期望长度以阈值
256分两种情况如果
旧容量 < 256,容量翻倍,即2 x 旧容量如果
旧容量 > 256,新容量的计算公式为(旧容量 + 3 * 256) >> 2,右移2代表除以4。意思是对于大切片的扩容,每次*近似 1.25,避免内存浪费
slice扩容后还共享底层数组吗?
看如下示例:
1 | |
回答:
- s1, s2 初始化多少?
- 最后程序输出什么?
slice作为参数传递,可能改变原来的slice吗?
分情况,这要看你有没有更改slice指向的数组值。因为slice本身会拷贝一份副本当成参数传入,底层三个属性:指向数组的指针、len、cap都是副本,但是如果在方法中修改底层数组的话,原有的slice是会变的。
Channel
小徐先生:https://mp.weixin.qq.com/s/QgNndPgN1kqxWh-ijSofkw
引入
Golang有个著名的思想:
不要通过共享内存来通信,而是通过通信来共享内存
Java/C++ 中多线程的写法常常是:多个线程中共享某些变量,再通过信号量/锁机制来解决同步问题。本质上是先共享内存,以此来解决通信问题。
1 | |
而 Golang 引入了 Channel 管道来解决 Goroutine之间的通信问题,并以此实现数据共享。我们可以明显看到好处:
- Goroutine之间不需要共享内存变量,它们除了将数据传给ch以外 对内存是无感知的。避免了因为共享内存而不得不引入同步,加锁/信号这些繁杂的操作,减少错误。
- 内存变量如果是敏感数据,共享给不同的线程/Goroutine增加风险;而基于Channel通信别的Goroutine根本访问不到这些敏感变量,提高安全性。
这里有一个银行账户的例子:
- 传统的共享内存的实现:
1 | |
基于Channel实现:
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
35type Withdraw struct {
amount int
}
reqCh := make(chan Withdraw)
// 取钱实现:不断读管道数据,每次都更新账户
go func() {
account := &Account{1000}
for req := range reqCh {
account.balance -= req.amount
}
}()
// 调用取钱的时候:往管道里塞数据就行
reqCh <- Withdraw{100}
## 核心数据结构
Channel 底层是一个 `hchan` 的结构体,它的缓冲区是一个环形数组的概念。
```go
// runtime/chan.go
type hchan struct {
qcount int // 对应len(ch)
dataqsiz int // 对应cap(ch)
buf unsafe.Point // 指向缓冲区
elemsize int // 数据类型
isClosed bool // 是否关闭
// ...
lock sync.Mutex // 互斥锁确保同一时刻只有一个goroutine正在访问channel
}
构造器
写流程
读流程
阻塞与非阻塞模式
两种读协议
sync.Map
小徐先生:https://mp.weixin.qq.com/s/nMuCMA8ONnhs1lsTVMcNgA
前言
核心数据结构
读流程
写流程
删流程
遍历
GMP
这部分是跟着 刘丹冰aceId 学的,b站有视频,也有他自己出版的书。
原版文档可以看 github
下面是一个思路概括
四个维度:
- Golang调度器和 GMP思想 的由来
- 早期单进程os
- 多进程/多线程os
- 进入协程co-routine
- Go语言的协程goroutine
- 旧的Golang调度器
- Golang调度器 和 GMP 的设计思想
- GMP模型组成部分
- P、M的数量?
- P、M何时创建?
- 调度器的设计策略(四个特点)
- 执行
go func()背后发生了什么? - 调度器的生命周期(M0、G0)
- 常见 Golang调度场景过程
- G1创建G2(嵌套G)
- G1执行完毕
- G2创建过多的G