Eli's Blog

1. 逃逸分析

1.1 堆和栈:

  • 堆(Heap):
    • 一般情况下,手动申请、分配、释放。内存大小并不定,较大的对象。另外其分配相对慢,涉及到的指令动作也相对多
    • 堆在内存分配中类似于往一个房间里摆放各种家具,家具的尺寸有大有小。
    • 引用类型 (指针、slice、map、chan、interface)的地址对应的数据存储内存通常分配在堆上
  • 栈(Stack):
    • 由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数,局部变量等等都会存放在栈上
    • 栈是一种拥有特殊规则的线性表数据结构,只允许线性表的一端放入数据,之后再这一端取出数据,按照后进先出(lifo)的顺序
    • 值类型 (整型、浮点型、bool、string、array和struct) 的变量直接存储值,内存通常分配在栈上

1.2 逃逸分析

逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:

  • 是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否则分配到栈上
  • 即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上

1.3 需要逃逸的原因

频繁申请、分配堆内存是有一定 “代价” 的。会影响应用程序运行的效率,间接影响到整体系统。因此 “按需分配” 最大限度的灵活利用资源,才是正确的治理之道

1.4 查看逃逸分析

1.4.1 通过编译器命令,就可以看到详细的逃逸分析过程

1
2
3
4
go build -gcflags '-m -l' main.go

-m: 进行内存分配分析
-l: 禁用掉 inline 函数内联, 避免程序内联

1.4.2 通过反编译命令查看

1
go tool compile -S main.go

1.5 逃逸案例

1.5.1 指针

1) 外部引用,逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type User struct {
ID int
Name string
Age byte
}

func GetUser() *User {
return &User{
ID: 1,
Name: "jack",
Age: 12,
}
}

func main() {
_ = GetUser()
}
1
2
3
4
5
6
7
8
$ go build -gcflags "-m -l" main.go 
# command-line-arguments
./main.go:10:9: &User literal escapes to heap

$ go tool compile -S main.go | grep CALL
0x0028 00040 (main.go:10) CALL runtime.newobject(SB)
0x005f 00095 (main.go:9) CALL runtime.morestack_noctxt(SB)

2)外部未引用,不逃逸

1
2
3
4
func main() {
s := new(string)
*s = "abc"
}
1
2
3
4
5
$ go build -gcflags "-m -l" main.go 
# command-line-arguments
./main.go:4:10: new(string) does not escape

$ go tool compile -S main.go | grep CALL

1.5.2 未确定类型

1
2
3
4
5
6
7
func main() {
s := new(string)
*s = "abc"

//fmt.Println(*s) // not escape
fmt.Println(s) // escape to heap
}

原因:func Println(a ...interface{}) (n int, err error)接收任意类型,在编译时无法确定具体类型,因此产生逃逸

1.5.3 泄漏参数

1
2
3
4
5
6
7
8
9
10
11
12
13
type User struct {
ID int
Name string
Age byte
}

func GetUser(u *User) *User {
return u
}

func main() {
_ = GetUser(&User{ID: 1, Name: "jack", Age: 12})
}
1
2
3
4
$ go build -gcflags "-m -l" main.go 
# command-line-arguments
./main.go:9:14: leaking param: u to result ~r1 level=0
./main.go:14:14: &User literal does not escape

使其逃逸:被外部所引用,将分配到堆上

1
2
3
4
5
6
7
8
9
10
11
12
13
type User struct {
ID int
Name string
Age byte
}

func GetUser(u User) *User {
return &u
}

func main() {
_ = GetUser(User{ID: 1, Name: "jack", Age: 12})
}
1
2
3
$ go build -gcflags "-m -l" main.go 
# command-line-arguments
./main.go:9:14: moved to heap: u

2. new和make:

  • new:

    • 分配内存
    • 设置零值
    • 返回指针(重要)
  • make:

    • sice, map, chan
    • 分配内存
    • 返回对象 (对象本身为引用类型,不需要返回指针)

3. Go类型断言:

1) Type Assertion

1
2
t := o.(T)
t, ok := o.(T)

2) Type Switch

1
2
3
4
5
6
switch o.(type) {
case int:
case string:
case nil:
...
}

4. slice

4.1 slice扩容的内存管理

  • 翻新扩展:当前元素为 kindNoPointers,将在老 Slice cap 的地址后继续申请空间用于扩容
  • 举家搬迁:重新申请一块内存地址,整体迁移并扩容

cap < 1024: cap = cap * 2
cap >= 1024: cap = cap + cap/4

4.2 empty & nil slice

len 和 cap 均等于0

1) empty: arary -> []int{} 指向空数组

1
2
s := []int{}
s = make([]int, 0)

2) nil: array -> nil

1
var s []int

5. 指针

5.1 空指针

1
2
3
4
5
6
7
8
9
10
func main() {
var p1 *int
var p2 = new(int)

fmt.Println(p1 == nil) // true
fmt.Println(p2 == nil) // false

fmt.Println(*p1) // panic: nil pointer dereference
fmt.Println(*p2) // 0
}

6. slice 参数

  • 可通过下标修改原始slice的元素值
  • append操作,会改变slice指向的底层数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
a := make([]int, 1, 2)
fmt.Printf("a=%v, len=%d, cap=%d, ptr=%p\n", a, len(a), cap(a), &a)

foo(a)
fmt.Printf("a=%v, len=%d, cap=%d, ptr=%p\n", a, len(a), cap(a), &a)

bar(&a)
fmt.Printf("a=%v, len=%d, cap=%d, ptr=%p\n", a, len(a), cap(a), &a)
}

func foo(a []int) {
a[0] = 9 // slice副本指向的 array 未变,所以能够修改原始 a 的值
a = append(a, 1, 2) // append操作,a指向的地址改变
fmt.Printf("%p\n", &a)
}

func bar(a *[]int) {
*a = append(*a, 5, 6)
}

7. 并发读写map

fatal error: concurrent map read and map write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func mapTest() {
m := make(map[int]int)

go func() {
for {
m[0] = 1
}
}()

go func() {
for {
_ = m[0]
}
}()
}

解决方案:sync.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
func main() {
//mapTest()
syncMap()

select {
case <-time.After(time.Second):
break
}
}

func syncMap() {
m := sync.Map{}

go func() {
for {
m.Store(0, 1)
}
}()

go func() {
for {
_, _ = m.Load(0)
}
}()
}

8. Go 接口

Go 接口为非侵入式接口

  • 非侵入式接口: 接口的定义者无需知道接口被哪些类型实现了, 而接口的实现者也不需要知道实现了哪些接口, 无需指明已经实现了哪些接口, 只需要关注自己实现的是什么样的接口即可. 编译器会自己识别哪个类型实现哪些接口
  • 侵入式接口: 主要体现是实现接口的类需要很明确的声明自己实现了哪个接口

9. Go 并发

CSP: Communicating Sequential Process 通信顺序进程,消息传递模型

Goroutine: 轻量级线程, 简称协程。一个Goroutine 的栈启动很小(2k或者4k)。当Goroutine的栈空间不够的时候,会根据需要动态伸缩栈大小(甚至可到到1G)。

Go 语言线程模型:

  • M: 内核线程(物理线程)
  • P: 执行Go代码所必须的资源(上下文环境)
  • G: 待执行的Go代码,即协程本身

9.1 sync.Mutex 互斥锁

解决 goroutine 抢占公共资源问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var m = make(map[int]int)
var lock sync.Mutex

func main() {
for i := 1; i <= 10; i++ {
go factorial(i)
}

time.Sleep(time.Second * 2)

for k, v := range m {
fmt.Printf("%d!=%d\n", k, v)
}
}

func factorial(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}

lock.Lock()
m[n] = res
lock.Unlock()
}

9.2 并发数据同步

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
func add(n int) {
defer wg.Done()

// method1: Found 1 data race
for i := 0; i < n; i++ {
runtime.Gosched()
c++
}

// method2: 正常
/* for i := 0; i < n; i++ {
atomic.AddInt64(&c, 1)
runtime.Gosched()
}*/

// method3: 正常
/* defer lock.Unlock()
lock.Lock()
for i := 0; i < n; i++ {
c++
}*/
}

func main() {
wg.Add(2)

go add(3)
go add(4)

wg.Wait()
fmt.Println("c =", c)
}

检测并发竞争状态:go run --race main.go

sync.WaitGroup: 等待组。用于等待一组线程的结束。

1
2
3
func (wg *WaitGroup) Add(delta int)   // 等待个数计数器
func (wg *WaitGroup) Done() // 子线程结束,计数器减1
func (wg *WaitGroup) Wait() // 阻塞等待所有子线程结束,即计数器为0

sync/atomic: 原子操作包。以底层的加锁机制来同步访问整型变量和指针。

1
2
3
func AddInt64(addr *int64, delta int64) (new int64)
func LoadInt64(addr *int64) (val int64)
func StoreInt64(addr *int64, val int64)

 上一页