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 ./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 ./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) }
原因: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 ./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 ./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
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 ) fmt.Println(p2 == nil ) fmt.Println(*p1) fmt.Println(*p2) }
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 a = append (a, 1 , 2 ) 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 () { 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.Mutexfunc 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() for i := 0 ; i < n; i++ { runtime.Gosched() 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 () // 子线程结束,计数器减1func (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 )