6、内存溢出,内存泄漏,内存逃逸
内存溢出:超出给定内存。
递归没有结束(申请太多对象)
一次性加载超大文件或数据集到内存(如未分页读取)。
大量请求导致内存分配超出系统限制(如未合理设置容器内存上限)。
如无限增长的全局缓存
排查:
-
容器环境查看 Docker/K8s 内存指标(
docker stats
或kubectl top pods
)。 -
使用
go tool pprof -http=:8080 heap.pprof
分析内存分配热点。
内存泄漏:有些资源没有释放。 文件没有关闭
处理文件,一次性加载过多在内存中
缓存数据无过期策略(如 map
只增不减)。
创建更多的goroutin
排查:
-
检查全局变量、缓存、第三方库(如 CGO 资源管理)。
-
使用
defer
确保资源释放(如defer file.Close()
)。 -
检查 Goroutine 数量:
http://localhost:6060/debug/pprof/goroutine?debug=1
查看 Goroutine 堆栈。
内存逃逸:本来应该分配在栈上时,由于某些原因,它们被分配到了堆上。变量分配到堆上时可能增加 GC 压力,导致内存消耗增大。
func escapeExample() *int {x := 10 // x 是局部变量return &x // x 的地址被返回,逃逸到堆
}
怎么避免
不要直接返回局部变量的指针。如果需要返回数据,可以直接返回值。
排查
用 pprof
查看堆内存分配
1.2、什么是MVC
1. 用户通过界面(View)进行操作(比如点击按钮或输入 URL)。
2. Controller 接收用户输入,调用 Model 处理业务逻辑或数据库操作。
3. Model 返回结果,Controller 再将结果传递给 View。
4. View 根据数据渲染页面并展示给用户。
5、context
- Context 的数据结构包含 Deadline,Done,Err,Value,
- Deadline 方法返回一个 time.Time,表示当前 Context 应该结束的时间,ok 则表示有结束时间,
- Done 方法当 Context 被某一个操作进行了取消或者超时时候返回的一个 close 的 channel,告诉给 context 相关的函数要停止当前工作然后返回了,
- Err 表示 context 被取消的原因,
- Value 方法表示 context 实现共享数据存储的地方,
- 协程安全的。
9.3、defer关闭资源时,程序中断会被会被关闭吗?
-
defer
在以下情况会执行:-
正常退出。
-
panic
崩溃(当前函数的defer
)。 -
捕获中断信号(如
Ctrl+C
)。
-
-
defer
在以下情况不会执行:-
os.Exit
退出。 -
强制终止(如
kill -9
)。
-
11、Go 多返回值怎么实现的?
-
Go 的多返回值是通过 栈 来实现的
-
函数的多返回值会被依次压入调用栈中。
18、闭包
package mainimport "fmt"func main() {f := test()fmt.Println(f(1))//11fmt.Println(f(2))//13
}
func test() func(x int) int {var n int = 10return func(x int)int {n = n + xreturn n}
}
20、切片
20.1、slice
- copy:相当于覆盖
20.5、切片是否线程安全,为什么不安全,如何保证安全
1. append() 可能导致底层数组扩容,从而导致多个 Goroutine 访问不同的底层数组,产生数据错乱。
2. 多个 Goroutine 可能同时修改 slice 的长度,导致数据竞争(Data Race)。
21.3、为什么不安全,怎么解决安全问题?
Map 线程不安全的原因
-
底层实现:Go 的 Map 是一个哈希表,包含多个桶(bucket),每个桶存储键值对。并发读写时,多个 Goroutine 可能同时修改桶或触发扩容,导致数据竞争。。
sync.RWMutex读写锁
• 多个 Goroutine 可以同时读取数据(RLock()),不影响彼此。
• 写操作需要独占锁(Lock()),确保数据一致性。
21.4、sync.Map
-
读多写少:适合并发读操作远多于写操作的场景。
-
通过 无锁读,写 和 延迟删除 等机制减少了锁竞争。
1. 读操作(无锁)
-
优先从
read
中读取数据,若read
中不存在且amended
(标记是否有数据在 dirty 中但不在 read 中)为true
,则加锁后从dirty
中读取。 -
无锁实现:通过原子操作直接访问
read
,避免锁竞争。
2. 写操作(加锁)
-
Store
流程:-
先尝试无锁更新
read
中的entry
(通过原子操作)。 -
若
read
中不存在该键,则加锁后操作dirty
:-
若
dirty
未初始化,从read
中复制未删除的键值对到dirty
(延迟初始化)。 -
更新
dirty
中的值,并标记amended
为true
。
-
-
3. 删除操作(延迟删除)
-
将
entry
中的指针标记为nil
(逻辑删除),实际数据在dirty
提升为read
时才会物理删除。
4. dirty
提升为 read
-
当
dirty
中的键值对数量超过read
时,触发dirty
到read
的升级(加锁操作)。 -
升级后,
dirty
置为nil
,amended
标记为false
。
21.8、扩容
- 等量扩容:并不是扩大容量,buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次。桶内溢出桶数量大于等于2^hash数组长度,长度最大取15,达到长度等量扩容
- 增量扩容:桶内key-v总数/hash数组长度>6.5触发扩容,负载因子(总数/桶数组长度) > 6.5时,桶数组两倍增长。
- 增量扩容为什么是6.5
通过开发团队大量测试得出来的,低了占用内存大空间效率会变低,高了插入查找性能变低
21.10、map底层增删改查
插入
- 判断是否初始化,没有panic
- 其他线程是否在写入
- 产生一个标识标识正在写操作
- 根据key值算出哈希值
- 取哈希值低位与hmap.B取模确定bucket位置
- 判断是否需要进行扩容,分担扩容压力
- 查找该key是否已经存在,如果存在则直接更新值
- 如果没找到将key,将key插入
- 清除之前的正在写标记
查看
- 判断map是否初始化,如果没有或数量为0返回0值(为空)
- 是否有其他线程并发写入map,是报错
- key算处hash值
- 对桶取模找到桶位置
- 遍历桶链表
- 有返v无返回零
22、设计模式
22.1、单例模式
类只会创建一次
1.饿汉模式,线程安全
饿汉模式下的单例写法是最简单的,但它是线程不安全的!
开始就创建,创建好后序继续使用
2.懒汉模式,线程不安全
使用时创建
为什么不安全同时进入类,都是nil所以线程不安全
怎么解决安全问题
2个if。同时进入 强锁,1抢到new。2抢到里面if判断不为nil
可加同步锁解决线程安全问题:
24、多线程
24.1
、进程、线程、协程有什么区别?
资源 | 调度方式 | 通信 | 备注 | |
进程 | 独立的虚拟内存空间,互不干扰 | 进程是操作系统分配资源(CPU、内存、文件等)的最小单位。 | 通过管道、消息队列、共享内存等机制 | 一个进程崩溃不会直接影响其他进程 |
线程 | 共享进程 | 操作系统内核 | 共享内存 | 线程间可直接读写同一进程的变量,但需通过锁(如互斥锁)避免竞争。 一个线程崩溃可能导致整个进程崩溃。 程的创建、销毁和调度由操作系统内核管理。 1mb 1000-1500ns |
协程 | 共享线程 | 程序自行调度 (非操作系统) | 共享内存 | 协程由程序自身控制调度,不依赖操作系统内核。 2kb 切换200ns(纳秒)协程调度器按照调度策略把协程调度到线程中运行 |
24.5、channel数据结构
总结hchan结构体的主要组成部分有四个:
- - 用来保存goroutine之间传递数据的循环链表。=====> buf。 qcount(环形数组个数)
- - 用来记录此循环链表当前发送或接收数据的下标值。=====> sendx和recvx。
- - 保存向该chan发送和从改chan接收数据的goroutine的队列。==>(写)sendq 和(读) recvq
- - 保证channel写入和读取数据时线程安全的锁。 =====> lock
发送数据
接受数据
关闭channel
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。
24.6、为什么用环形链表
环形链表有头指针和尾指针,可以通过移动指针方便的找到头和尾
24.7、channel 是否线程安全?锁用在什么地方?
channel
可以在多个 goroutine 之间传递数据,所以必须确保当一个 goroutine 向 channel
发送数据或从 channel
接收数据时, 他 goroutine 不会同时修改 channel
的内部状态。
对循环数组buf进行入队出队操作必须获取互斥锁才能操作数据,锁在这里起到了保护的作用,确保了操作的原子性和一致性。
24.8、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?
nil
Channel
-
读取:阻塞
-
写入:阻塞
-
关闭:恐慌(panic)。
关闭的 Channel
-
读取:完成返回零值和一个
false
值。 -
写入:恐慌(panic)。
-
关闭:重复关闭(panic)。
有数据的 Channel
-
读取:正常读取,不会阻塞。
-
写入:有其他 goroutine 从该 channel 中读取数据或该 channel 被关闭。
-
关闭:取完毕后继续读取该 channel 会读取到零值,并且不会阻塞。
24.12、什么是GMP?
组件 | 角色说明 | 数量限制 |
---|---|---|
G (Goroutine) | 轻量级协程,用户态线程,初始栈仅 2KB,动态扩展(最大 GB 级) | 理论上无限制(百万级) |
M (Machine) | 操作系统线程(内核线程),负责执行 G 的代码,与 CPU 核心绑定 | 默认最多 10000(可调整) |
P (Processor) | 逻辑处理器,管理 G 的队列和调度策略,连接 G 和 M 的桥梁 | 默认等于 CPU 核心数(GOMAXPROCS ) |
24.13、生命周期
24.14、mutex模式
正常模式
- 通过自旋获取锁,如果仍未获取锁,通过排队等待,所有等待着按照先入先出顺序排队,当持有锁的goroutine被失败,队伍中第一个等待的锁不会直接拥有锁,需要和后来者竞争,(还在自选阶段的后来者更有优势,因为他们正在cpu运行)。第一个为获取的锁会被插入到队列的头部,(goroutine等待加锁的时间超过1ms后会转为饥饿模式)
饥饿模式
- 饥饿模式下,Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine 不会尝试获取锁,即使看起来锁没有被持有,它也不会去抢,也不会 spin(自旋),它会乖乖地加入到等待队列的尾部。
返回正常模式
- 此 waiter 已经是队列中的最后一个 waiter 了,没有其它的等待锁的 goroutine 了;
- 此 waiter 的等待时间小于 1 毫秒。
25、GC相关
go 1.8混合写屏障机制
GC 开始将栈上的可达对象全部扫描并标记为黑色,栈对象中新建的对象都会被标记成黑色。
栈的垃圾回收
2. 栈的回收策略
-
无写屏障:栈上的写操作不触发写屏障。
-
最终标记阶段的 STW:
-
在并发标记完成后,执行一次短暂的 STW,扫描所有栈的根对象。
-
确保栈上的对象引用被正确标记,未被标记的白色对象将被回收。
-
堆的垃圾回收
1. 并发标记流程
-
初始标记(STW):
-
暂停所有 Goroutine,扫描根对象(栈、全局变量等)。
-
标记直接可达的对象为灰色。
-
-
并发标记:
-
恢复 Goroutine 执行,后台线程并发标记灰色对象。
-
混合写屏障监控堆对象的写操作,更新标记状态。
-
-
最终标记(STW):
-
短暂暂停,扫描残留的栈引用和全局变量。
-
确保所有存活对象被标记为黑色。
-
2. 清除阶段(并发)
-
回收所有白色对象的内存,将其加入空闲链表。
-
清除阶段无需 STW,与用户程序并发执行。
在次基础上会出现伪命题,栈上想引用一个白色,这个白色不可能是凭空产生的