go知识点梳理
map [[2023-02-20 Mon]] #
最主要的数据结构有两种:
哈希查找表(Hash table)
、搜索树(Search tree) avl或rb树
。 Go 语言采用的是链表解决哈希冲突
。 1.9.2
map 内存模型 #
- 【引申1】slice 和 map 分别作为函数参数时有什么区别? – slice返回的是结构体 map是指针
bmap
就是我们常说的“桶”,桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,第九个就会新加通过overflow连起来- 每个bucket内部key 和value是分开放的,不是一组一组放
- map在扩容的时候,要搬迁部分bucket,可能存在中间状态, 什么时候扩容? 壮哉因子超过 阈值6.5, overflow的bucket过多
- key为什么是无序的? 扩容后会搬,key位置会变动很大; 还一个每次for 遍历时go也是从一个随机序号开始的
- map不是线程安全的、遍历过程删除加都可能有问题 sync.RWMutex
- 无法对map的key value取地址,即时通过unsafe.pointer拿到,也可能会变了
interface#
-
go的接口有什么特点: 引入了
不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可
-
iface
和eface
都是 Go 中描述接口的底层结构体,区别在于iface
描述的接口包含方法,而eface
则是不包含任何方法的空接口:interface{}
-
iface
内部维护两个指针,tab
指向一个itab
实体 data指向具体的值 -
tab
是接口表指针,指向类型信息;data
是数据指针,则指向具体的数据。它们分别被称为动态类型
和动态值
。而接口值包括动态类型
和动态值
# -
eface
就比较简单了。只维护了一个_type
字段,表示空接口所承载的具体的实体类型。data
描述了具体的值 -
编译期自动检测是否实现了接口 #
-
1 2
var _ io.Writer = (*myWriter)(nil) var _ io.Writer = myWriter{} // 检查 myWriter 类型是否实现了 io.Writer 接口
类型转换
、类型断言
本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作
-
C++ 通过虚函数表来实现基类调用派生类的函数;而 Go 通过
itab
中的fun
字段来实现接口变量调用实体类型的函数
channel #
-
不要通过共享内存来通信,而要通过
通信来实现内存共享
。 这就是 Go 的并发哲学,它依赖 CSP 模型,基于 channel 实现 # -
数据结构 #
-
buf
指向底层循环数组(缓冲channel),sendq recvq
表示被阻塞的goroutine,sendx recvx
发送接收msg相对位置
-
a.检查接收的channel是否合法、比如nil直接返回fasle,goroutine 挂起 b.对于不阻塞的发送操作,如果 channel 未关闭并且没有多余的缓冲空间
-
如果channel已close,panic
-
如果能从等待接收队列 recvq 里出队一个 sudog(代表一个 goroutine),说明此时 channel 是空的,没有元素,所以才会有等待接收者。这时会调用 send 函数将元素
直接从发送者的栈拷贝到接收者的栈
,关键操作由sendDirect
函数完成 -
如果没有命中以上条件的,说明 channel 已经满了,阻塞起来。
-
这里有一些绑定操作,sudog 通过 g 字段绑定 goroutine,而 goroutine 通过 waiting 绑定 sudog,sudog 还通过
elem
字段绑定待发送元素的地址,以及c
字段绑定被“坑”在此处的 channel。 -
所以,待发送的元素地址其实是存储在 sudog 结构体里,也就是当前 goroutine 里。
- 如果 channel 是一个空值(nil),在非阻塞模式下,会直接返回
- 和发送函数一样,接下来搞了一个在非阻塞模式下,不用获取锁,快速检测到失败并且返回的操作。 (没准备好: 非缓冲的发送队列里没goroutine,缓冲性buffer是空的 — > 都说明没数据可操作)
- 接下来的操作,首先会上一把锁,粒度比较大。
- 接下来,如果有等待发送的队列,说明 channel 已经满了,要么是非缓冲型的 channel,要么是缓冲型的 channel,但 buf 满了。这两种情况下都可以正常接收数据。
- 最后,取出 sudog 里的 goroutine,调用 goready 将其状态改成 “runnable”,待发送者被唤醒,等待调度器的调度。
- close 逻辑比较简单,对于一个 channel,recvq 和 sendq 中分别保存了阻塞的发送者和接收者。关闭 channel 后,对于等待接收者而言,会收到一个相应类型的零值。对于等待发送者,会直接 panic。所以,在不了解 channel 还有没有接收者的情况下,不能贸然关闭 channel。
- 关闭的chan能读取buffer里的数据,因为读取时判定过程是有buffer的无数据并close的才会立即返回 [[2023-02-28 Tue]]
- 总结一下,发生 panic 的情况有三种:向一个关闭的 channel 进行写操作;关闭一个 nil 的 channel;重复关闭一个 channel。
读、写一个 nil channel 都会被阻塞
。
-
有两个不那么优雅地关闭 channel 的方法:
-
使用 defer-recover 机制,放心大胆地关闭 channel 或者向 channel 发送数据。即使发生了 panic,有 defer-recover 在兜底。
-
使用 sync.Once 来保证只关闭一次。
-
引入一个中间者: channel 通知goroutine关闭,最终有一个主close
- 泄漏的原因是 goroutine 操作 channel 后,处于发送或接收阻塞状态,而 channel 处于满或空的状态,一直得不到改变。同时,垃圾回收器也不会回收此类资源,进而导致 gouroutine 会一直处于等待队列中,不见天日。
- 另外,程序运行过程中,对于一个 channel,如果没有任何 goroutine 引用了,gc 会对其进行回收操作,不会引起内存泄漏
channel引用有哪些? #
context #
GC
1.基础
2. 根对象到底是什么?#
-
根对象
在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括: -
全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
-
执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含
指向分配的堆内存区块的指针
。 -
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
3. 常见的 GC 实现方式有哪些?Go 语言的 GC 使用的是什么?#
-
算法
其存在形式可以归结为追踪(Tracing)和引用计数(Reference Counting)这两种形式的混合运用, -
[[追踪式GC]]
-
特点
Go 的 GC 目前使用的是无分代
(对象没有代际之分)、不整理
(回收过程中不对对象进行移动与整理)、并发
(与用户代码并发执行)的三色标记清扫算法、 -
从根对象出发,根据对象之间的引用信息,一步步推进直到扫描完毕
整个堆
并确定需要保留的对象,从而回收所有可回收的对象。Go、 Java、V8 对 JavaScript 的实现等均为追踪式 GC -
引用计数式GC
-
每个对象自身包含一个被引用的计数器,当计数器归零时自动得到回收。因为此方法缺陷较多,在追求高性能时通常不被应用。Python、Objective-C 等均为引用计数式 GC,
快简单、但每次要计算引用数性能差,需要额外内存记录引用数
4. 三色标记法是什么?#
-
关键
理解对象的三色抽象以及波面(wavefront)推进这两个概念。三色抽象只是一种描述追踪式回收器的方法, -
白色:: 要被回收的对象,不可达的
-
灰色:: 中间带的,可能被回收,也可能可达对象
-
黑色:: 就是可达的,不可回收的
5. STW 是什么意思? #
- 定义:: stop the world,开始gc的时候会停止所有用户代码,导致停止
6. 如何观察 Go GC?#
-
方式1:
GODEBUG=gctrace=1
# -
方式2..4: go tool、 runtime.memstate、 gcstate信息
-
wall clock
是指开始执行到完成所经历的实际时间,包括其他程序和本程序所消耗的时间;cpu time
是指特定程序使用 CPU 的时间 -
wall clock < cpu time: {{cloze 充分利用多核}}
-
wall clock ≈ cpu time:
未并行执行
-
wall clock > cpu time:
多核优势不明显
7. 有了 GC,为什么还会发生内存泄露?#
-
严谨的话来说应该
预期的能很快被释放的内存由于附着在了长期存活的内存上、或生命期意外地被延长
,导致预计能够立即回收的内存而长时间得不到回收 -
a. 比如把局部变量附着到了全局的变量cache上去了,无法释放
-
b. goroutine leak
-
例:: 一个 goroutine 尝试向一个没有接收方的无缓冲 channel 发送消息,则该 goroutine 会被永久的休眠,整个 goroutine 及其执行栈都得不到释放
8. 并发标记清除法的难点是什么? #
简
如何保证标记与清除的准确性
9. 什么是写屏障、混合写屏障,如何实现?#
2.GC 的实现细节 #
10. Go 语言中 GC 的流程是什么? #
11. 触发 GC 的时机是什么? #
-
主动触发
-
被动触发,分为两种方式:
-
使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC。
-
使用步调(Pacing)算法,其核心思想是控制内存增长的比例。
12. 如果内存分配速度超过了标记清除的速度怎么办? #
-
简单说就是当gc发生时,会标记,让内存分配慢点,达到回收和分配平衡
- 当 GC 触发后,会首先进入并发标记的阶段。并发标记会设置一个标志,并在 mallocgc 调用时进行检查。当存在新的内存分配时,会暂停分配内存过快的那些 goroutine
3.GC 的优化问题 #
13. GC 关注的指标有哪些?#
- cpu利用率:: gc会影响多大 GC停顿时间: stw gc停顿频率:: 造成程序停顿频率
14. Go 的 GC 如何调优? #
-
Go 的 GC 被设计为极致简洁,与较为成熟的 Java GC 的数十个可控参数相比,严格意义上来讲,Go 可供用户调整的参数只有
GOGC 环境变量
-
一般其实不需要care这些问题:: 只有对执行延迟非常敏感才需要,gc的开销影响了性能;无非:尽量少申请内存,复用
-
((63f859a9-3219-4740-897a-774254c56dd9))