Go常见问题与回答(上)

文章目录

1、GPM模型,三个含义,三者关系

  • G:轻量级用户线程,初始内存仅2KB,通过go关键字创建,包含执行上下文PC、栈等。数量可达百万
  • P:虚拟处理器,负责管理G的本地队列(默认容量256个Goroutine),p数量由GOMAXPROCESS控制
  • M:对应操作系统内核级线程,负责执行G,M与P通常一一绑定,当G阻塞时,M会释放P并新建线程继续执行其他G

总结:

  • G由P调度:P从本地队列或全局队列获取G分配给M执行

  • M执行G:M通过绑定P获取特定执行G,若阻塞则释放P

  • P协调资源:通过工作窃取机制平衡各G的负载

2、Schedule循环调度是什么

调度循环通过持续查找---执行---唤醒的机制,使G在M和P的协作下高效运行,形成了非阻塞并发模型

3、调度器的设计策略

资源高效利用:

线程复用:通过复用线程M 减少创建/销毁开销,结合Work Stealing(任务窃取)机制平衡,当M本地队列无任务时,优先从其他P的队列偷取任务,而非销毁线程.

内存优化: 引入逻辑处理器§集中管理G的调度信息(如本地队列),减少每个M维护的上下文开销,降低内存占用
抢占式调度

通过限制Goroutine单次执行时间(如10ms),强制让出CPU,防止协程长时间占用导致饥饿,同时兼容协作式调度的部分特性
队列分级管理

  • 本地队列优先:新建的G优先加入P的本地队列(容量256个),减少全局锁竞争;本地队列满时,部分G溢出到全局队列
  • 动态平衡:M 通过"窃取"策略从其他P或全局队列获取任务,提升整体任务利用率

动态扩缩容机制

  • M的弹性创建:当阻塞或任务不足时,动态创建新M,空闲时回收M以节省资源
  • P的固定数量: P数量由GOMAXPROCS控制,限制并行度以平衡资源消耗与性能.
    阻塞与唤醒策略
  • 当M因系统调用或I/O阻塞时,解绑P并休眠,G就绪后,M被唤醒重新绑定P执行,避免资源闲置
  • 通过Spinning自旋状态优化唤醒,当仅有新任务或阻塞发生时,确保至少有一个M处于活跃窃取状态,减少不必要的线程切换。

4、Go func() 经历的过程

1、编译阶段: go func() 会被编译器转换为调用runtime.newrproc 并生成唯一函数地址

运行时创建runtime.newproc函数被调用后,会创建一个新的Goroutine结构体,初始化其栈空间(默认初始栈大小为2KB)。若栈空间不足,会触发栈增长机制,分配更大内存并拷贝数据。

2、调度入队: 新创建的Goroutine会被放入当前Processor(P)的本地队列或全局队列中。若本地队列已满(默认256个Goroutine),则通过工作窃取机制分配到其他P的队列。

3、执行执行: 当Machine(M)空闲时,从绑定的P队列中取出Goroutine执行。执行时通过CALL指令跳转至函数入口,栈帧基址(BP)和栈指针(SP)按约定布局,参数和返回值通过栈相对寻址访问。执行完毕后通过RET指令返回,并回收栈帧资源。

5、调度器的生命周期

初始化阶段: 程序启动时,调度器创建初始线程m0和调度协程g0,初始化栈、垃圾回收系统及GOMAXPROCS个逻辑处理器,随后将runtime.main生成的main协程
调度执行阶段: m0绑定P后从本地队列获取main协程执行。执行过程中,新创建的协程(G)优先加入P的本地队列,满则溢出到全局队列。M通过工作窃取机制平衡负载,从本地队列、全局队列或其他P队列中获取G执行。当G阻塞(如I/O)时,M解绑P并休眠,待G就绪后重新调度。
结束阶段: 当main协程退出后,调度器执行收尾工作(如处理Defer、Panic),调用runtime.exit终止程序。所有协程执行完毕后,P和M资源被回收,调度器生命周期结束。

6、Golang垃圾回收(GC)介绍

引用计数法

通过为每个对象维护引用计数器,当引用增加时计数加1,引用消失时减1,计数为0时立即回收内存。优点是回收速度快,但无法处理循环引用(如a.b = b; b.a = a),且频繁更新计数器会降低性能。
标记清除法

标记阶段:从根对象(全局变量、栈变量)出发,递归标记所有可达对象。

清除阶段:回收未标记的垃圾对象,内存碎片较多。

该算法需暂停程序(STW),无法并发执行,但实现简单。Go的早期版本曾使用此方法
复制法

将内存划分为两块(From和To),仅使用其中一块。GC时将存活对象复制到另一块,回收原区域。优点是避免碎片,但内存利用率低(仅50%可用),且复制成本高。Go未直接采用此方法,但三色标记中的并发复制优化有类似思想
标记整理法

在标记阶段后,将存活对象压缩至内存一端,清理边界外空间。解决了碎片问题,适合老年代(存活率高),但整理过程复杂,STW时间较长。Go的标记清除阶段会局部整理内存以提高效率
分代式

按对象生命周期分为新生代(短命对象)和老年代(长命对象),对不同代采用不同策略(如新生代高频回收)。Java的GC采用此方法,但Go语言实际采用非分代回收:

7、三色标记法原理

状态分类

所有对象初始为白色(未访问),根对象(全局变量、栈指针等)标记为灰色(待扫描),已完全扫描的存活对象标记为黑色(不可回收)。
标记阶段

初始标记(STW):暂停所有Goroutine(STW),扫描根对象,将直接引用的对象标记为灰色。

并发标记:恢复Goroutine,从灰色对象出发遍历其引用的白色对象,标记为灰色并加入队列,原灰色对象转为黑色。此阶段通过混合写屏障记录堆上对象引用的变化,避免漏标或错标。

重新标记(STW):短暂暂停Goroutine,处理剩余灰色对象及新分配的堆对象,确保标记完整性。
清理阶段

所有白色对象被回收,黑色对象保留。此阶段可并发执行,仅通过标记状态区分存活对象。
关键优化

混合写屏障:栈对象直接标记为黑色(无需重复扫描),堆对象修改时通过写屏障动态调整标记状态,减少STW时间。

分阶段STW:初始标记和重新标记阶段短暂暂停,并发标记阶段与程序运行并行,降低延迟。

8、屏障机制(STW,No Stw存在的问题)

作用:

1、 防止对象丢失:在并发标记过程中,用户程序可能修改指针引用的关系,屏障机制通过记录这些变化。确保被修改对象被正确标记为存活

2、 维护三色不变式:

强三色不变式:禁止黑色对象引用白色对象

弱三色不变式:允许黑色对象引用白色对象,但需通过灰色对象保护其路径
1、插入屏障(Insert Barrier)

作用:在指针赋值时,将被引用对象标记为灰色,确保黑色对象不会直接指向白色对象。

实现:仅对堆对象操作添加屏障,栈对象因性能原因不处理。

缺点:标记结束后需STW重新扫描栈,导致延迟。
2. 删除屏障(Delete Barrier)

作用:在指针删除时,将被删除对象标记为灰色,保护灰色对象到白色对象的路径。

实现:基于快照机制,GC启动时STW扫描整个栈,记录初始存活对象。

缺点:回收精度低,对象可能存活至下一轮GC。
3. 混合写屏障(Hybrid Write Barrier)

作用:结合插入和删除屏障的优点,减少STW时间。

规则:

新对象默认标记为黑色。

栈对象不触发屏障,堆对象修改时:

若目标对象为白色,标记为灰色。

若源对象为灰色,标记目标对象为灰色。

优势:

标记阶段无需STW,直接并发执行。

栈对象通过渐进式扫描(逐个Goroutine暂停)避免整机STW,提升吞吐

9、Golang GC过程(标记清理、Marking、Mark终止、Sweep清理、GC百分比)

初始标记(STW)

暂停所有Goroutine(Stop-The-World),扫描根对象(全局变量、栈指针等),将直接引用的对象标记为灰色,其余对象初始化为白色。

同步清理未释放的堆内存(如残留的span)。
并发标记

恢复Goroutine执行,从灰色对象出发递归标记其引用的白色对象为灰色,并加入队列。此阶段通过混合写屏障动态记录堆指针变化,避免漏标。

标记过程与程序运行并行,通过分阶段增量执行减少停顿。
重新标记(STW)

短暂暂停Goroutine,处理剩余灰色对象及新分配的堆对象,确保标记完整性。

关闭辅助标记的Goroutine,清理处理器缓存。
清除(并发)

将所有白色对象回收,黑色对象保留。此阶段可完全并发执行,通过gcmarkBits位图快速识别存活内存块。

重置内存管理单元(如span的分配状态)为下一次GC准备

10、Goroutine调度策略

从调度时机、调度策略、切换机制来谈
调度时机:

阻塞操作

当Goroutine发生I/O、锁等待、通道操作等阻塞行为时,调度器会将其挂起并切换其他Goroutine执行。例如,系统调用(如read)会触发entersyscall和exitsyscall,期间Goroutine状态变为_Gsyscall,允许调度器介入。

主动让出

通过runtime.Gosched()显式请求让出CPU,或函数调用链过长时自动触发栈保护检查(stackguard0标志),强制抢占。

时间片耗尽

每个Goroutine默认分配10ms时间片,超时后由sysmon监控线程标记为抢占点,触发重新调度。

垃圾回收(GC)

GC期间所有Goroutine被暂停(STW),回收完成后重新调度
调度策略

M:N模型

多个Goroutine(M)映射到少量操作系统线程(N),通过逻辑处理器(P)管理执行上下文,实现高效资源复用。

工作窃取算法

本地队列优先:P优先执行本地队列中的Goroutine,空闲时尝试从全局队列或其他P的队列"窃取"一半任务。

全局平衡:通过globrunqget定期补充全局队列,避免局部饥饿。

混合调度算法

结合FIFO(先进先出)和循环调度(Round-Robin),默认策略为循环调度,可通过GODEBUG参数调整。

非分代回收优化

弱化传统分代假设,通过三色标记和并发写屏障实现高效回收,减少STW时间
切换机制

抢占式调度

协作式抢占:通过stackguard0标志和preemptone函数实现,函数调用时检查是否需要抢占。

信号抢占:Go 1.14引入基于SIGURG信号的抢占,通过asyncPreempt函数插入抢占点。

上下文切换流程

触发:阻塞、主动让出、时间片耗尽或GC时,当前Goroutine状态更新为_Gwaiting。

执行:g0协程通过mcall切换栈至g0,调用schedule()选择新Goroutine,再通过gogo()切换执行上下文。

恢复:被唤醒的Goroutine状态变为_Grunnable,重新加入队列。

sysmon监控

专用监控线程定期检查P的调度情况,若某P持续执行同一Goroutine超过10ms,则通过栈标记强制抢占

11、什么是面向对象

j基于对象概念的编程范式、可以包含数据和代码,数据以字段的形式存在(通常称为属性或函数),代码以程序的形式存在(通常称为方法)

再次谈到Go如何实现:

封装:首字母大写,代表是公共的,可被外部访问的;首字母小写,代表是私有的,不可以被外部访问

继承:结构体之间的组合

多态:interface

12、Go语言和C++有什么区别

并发模型、内存回收、c++适合计算密集性任务、go在高并发/IO密集型场景

13、golang的缺点

1、不允许换行

2、支持多继承

3、不允许有未使用的包或变量

14、for range 时候他的地址会发生变化吗?

答:内存地址不变,会一直覆盖

15、go defer,多个defer的顺序,defer在什么时机会修改返回值?

答:defer在return语句执行后,函数返回前触发,此时返回值已复制但未传递给调用者②参数预计算:defer的参数在声明时求值,闭包若需动态值需显式传递;多个defer语句按LIFO顺序执行

16、介绍一下rune类型和uint类型

答:rune类型是用于表示unicode字符的类型,等价于int32,用于处理UTF-8编码的多字节字符(如中文、表情符号等);uint类型是go语言的无符号整数类型,适用于非负数值场景

17、golang中解析tag是怎么实现的,反射原理是什么?

答:tag是结构体字段的元数据,以及反引号包裹的字符串形式存在,键值对用空格分割。其本质是结构体字段的structTag类型(等价于字符串)存储在编译后的结构体描述中。

编译器解析:编译器在编译阶段会遍历结构体字段,将tag字符串解析为键值对,并存储到私有缓存中以提高运行时访问效率。例如,json

运行时访问:通过reflect包的Field包的Field.Tag.Get(key)方法可获取指定键的Tag值,

反射通过reflect.Type和reflect.Value两个核心类型实现:

reflect.Type:封装类型信息(如名称、大小、方法集)。

reflect.Value:封装值信息(如数值、指针、接口),支持动态修改(需满足Settable条件)。

动态类型与值的操作
类型检查: 通过TypeOf获取变量类型,Kind方法判断基本类型(如reflect.Int)。
值访问与修改: ValueOf获取值的反射对象,Field/Index访问字段或元素,SetInt/SetString修改值(需类型匹配)。
方法调用: MethodByName动态调用方法,支持接口的动态分派。

18、调用函数传入结构体时,应该是传值还是传指针?

传值,默认传值不改变结构体字段时,如果大结构体传指针

19、goroutine什么情况下会阻塞

1、 原子、互斥量或通道操作调用导致goroutine阻塞

2、 网络请求和IO操作导致Goroutine阻塞

3、 调用一些系统方法

4、 在goroutine执行一个sleep操作

20、Go的select底层数据结构和一些特性

1)select 操作至少要有一个 case 语句,出现读写 nil 的 channel 该分支会忽略,在 nil 的 channel 上操作则会报错。

2)select 仅支持管道,而且是单协程操作。

3)每个 case 语句仅能处理一个管道,要么读要么写。

4)多个 case 语句的执行顺序是随机的。

5)存在 default 语句,select 将不会阻塞,但是存在 default 会影响性能。

21、defer规则总结

延迟函数的参数是 defer 语句出现的时候就已经确定了的。

延迟函数执行按照后进先出的顺序执行,即先出现的 defer 最后执行。

延迟函数可能操作主函数的返回值。

申请资源后立即使用 defer 关闭资源是个好习惯。

22、Go出现panic场景

  •  数组/切片越界

  •  空指针调用。比如访问一个 nil 结构体指针的成员

  •  过早关闭 HTTP 响应体

  •  除以 0

  •  向已经关闭的 channel 发送消息

  •  重复关闭 channel

  •  关闭未初始化的 channel

  •  未初始化 map。注意访问 map 不存在的 key 不会 panic,而是返回 map 类型对应的零值,但是不能直接赋值

  •  跨协程的 panic 处理

  •  sync 计数为负数。

  •  类型断言不匹配。var a interface{} = 1; fmt.Println(a.(string)) 会 panic,建议用 s,ok := a.(string)

23、go里面如何实现set?

可以用type set struct{m map[interface{}] struct{}}

24、深浅拷贝的本质区别

深拷贝 :拷贝的是数据本身,创造一个新的对象,并在内存中开辟一个新的内存地址,与原对象是完全独立的,不共享内存,修改新对象时不会影响原对象的值。释放内存时,也没有任何关联。
浅拷贝 :拷贝的是数据地址,只复制指向的对象的指针,新旧对象的内存地址是一样的,修改一个另一个也会变。释放内存时,同时释放

25、Go多返回值是怎么实现的?

答:Go 传参和返回值是通过 FP+offset 实现,并且存储在调用函数的栈帧中。FP 栈底寄存器,指向一个函数栈的顶部;PC 程序计数器,指向下一条执行指令;SB 指向静态数据的基指针,全局符号;SP 栈顶寄存器。

26、Go中init函数的特征

Init函数既不接受参数,也不返回任何值,仅用于执行初始化操作;init函数在包被导入时自动触发,且在main函数执行前运行,无需显式调用;一个包内可定义多个init函数,按代码中出现的顺序依次执行;不同文件中的init函数按文件名字母顺序加载并执行;若当前包导入了其他包,被导入包的init函数会先于当前包的init执行。

27、uintptr和unsafe.Pointer的区别

unsafe.Pointer 是通用指针类型,它不能参与计算,任何类型的指针都可以转化成 unsafe.Pointer,unsafe.Pointer 可以转化成任何类型的指针,uuintptr 是指针运算的工具,但是它不能持有指针对象(意思就是它跟指针对象不能互相转换),unsafe.Pointer 是指针对象进行运算(也就是 uintptr)的桥梁。

28、数组和切片的区别

1)定义方式不一样

2)初始化方式不一样,数组需要指定大小,大小不改变

3)在函数传递中,数组切片都是值传递。

29、go的slice底层数据结构和一些特性

Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。

扩容机制:当切片比较小时(容量小于 1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍)

30、golang中数组和slice作为参数的区别?slice作为参数传递有什么问题

  1. 当使用数组作为参数和返回值的时候,传进去的是值,在函数内部对数组进行修改并不会影响原数据
  2. 当切片作为参数的时候穿进去的是值,也就是值传递,但是当我在函数里面修改切片的时候,我们发现源数据也会被修改,这是因为我们在切片的底层维护这一个匿名的数组,当我们把切片当成参数的时候,会重现创建一个切片,但是创建的这个切片和我们原来的数据是共享数据源的,所以在函数内被修改,源数据也会被修改

31、从数组中取一个相同大小的slice有成本吗?

没成本

32、golang切片扩容原理

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 当原 slice 容量 < threshold(阈值默认 256) 的时候,新 slice 容量变成原来的 2 倍;
  3. 当原 slice 容量 > threshold(阈值默认 256),进入一个循环,每次容量增加(旧容量+3*threshold)/4。
    就是控制让小的切片容量增长速度快一点,减少内存分配次数,而让大切片容量增长率小一点,更好地节省内存。

33、什么类型可以作为map的key

map的key可以是任何可以比较的类型。这包括所有的基本类型,如整数、浮点数、字符串和布尔值,以及结构体和数组,

34、map是否并发安全

不安全,数据竞态。

i. 使用互斥锁(sync.Mutex):

ii. 使用读写互斥锁(sync.RWMutex):

iii. 使用并发安全的map(sync.Map)

34、nil map和空 map有何不同

nil map和空map的主要区别在于它们的初始状态和对增删查操作的影响。nil map未初始化且不能用于存储键值对,而空map已初始化且可以安全地用于增删查操作。

35、map的链地址法

1、当两个不同的键通过哈希函数计算得到相同的哈希值时,Go的map并不直接覆盖旧的值,而是将这些具有相同哈希值的键值对存储在同一个桶(bucket)中的链表中。这样,即使哈希值相同,也可以通过遍历链表来找到对应的键值对。

2、当桶中的链表长度超过一定阈值时(通常是8个元素),Go的map会进行扩容和重新哈希,以减少哈希冲突,并优化查找、插入和删除操作的性能。

36、map的负载因子

负载因子 = 键数量/bucket数量

例如,对于一个bucket数量为4,包含4个键值对的哈希表来说,这个哈希表的负载因子为1.

• 哈希因子过小,说明空间利用率低

• 哈希因子过大,说明冲突严重,存取效率低

37、map的等量扩容

重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取。

38、map的查找过程

  1. 根据key值算出哈希值
  2. 取哈希值低位与hmap.B取模确定bucket位置
  3. 取哈希值高位在tophash数组中查询
  4. 如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较
  5. 当前bucket没有找到,则继续从下个overflow的bucket中查找。
  6. 如果当前处于搬迁过程,则优先从oldbuckets查找

39、map的插入过程

  1. 根据key值算出哈希值
  2. 取哈希值低位与hmap.B取模确定bucket位置
  3. 查找该key是否已经存在,如果存在则直接更新值
  4. 如果没找到将key,将key插入

40、go接口与C++有何异同

接口定义了一种规范,描述了类的行为和功能,而不做具体实现。

C++ 的接口是使用抽象类来实现的,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的

设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。

派生类需要明确地声明它继承自基类,并且需要实现基类中所有的纯虚函数。

41、channel是否线程安全,锁用在什么地方

  1. Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。
  2. 而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。
  3. 也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的

42、nil、关闭的channel、有数据的channel,再进行读、写、关闭会怎么样?

• 给一个 nil channel 发送数据,造成永远阻塞

• 从一个 nil channel 接收数据,造成永远阻塞

• 给一个已经关闭的 channel 发送数据,引起 panic

• 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值

• 无缓冲的channel是同步的,而有缓冲的channel是非同步的

43、进程、线程、协程有什么区别?

进程:是应用程序的启动实例,每个进程都有独立的内存空间,不同的进程通过进程间的通信方式来通信。

线程:从属于进程,每个进程至少包含一个线程,线程是 CPU 调度的基本单位,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。

协程:为轻量级线程,与线程相比,协程不受操作系统的调度,协程的调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行

44、go如何实现原子操作

Go 语言的标准库代码包 sync/atomic 提供了原子的读取(Load 为前缀的函数)或写入(Store 为前缀的函数)某个值

四十、go如何实现一个自旋锁

go 复制代码
type SpinLock struct {
    flag int32 // 0: 未锁定, 1: 已锁定
}

// Lock 方法尝试获取锁,失败则自旋等待
func (sl *SpinLock) Lock() {
    for !atomic.CompareAndSwapInt32(&sl.flag, 0, 1) {
        runtime.Gosched() // 让出时间片
    }
}

// Unlock 方法释放锁
func (sl *SpinLock) Unlock() {
    atomic.StoreInt32(&sl.flag, 0)
}

45、如何优雅的实现一个goroutine池

go 复制代码
type Pool struct {
    taskQueue      chan func() // 任务队列
    idleWorkers    chan *Worker // 空闲Goroutine队列
    maxWorkers     int        // 最大并发数
    workerTimeout  time.Duration
    lock           sync.Mutex
    wg             sync.WaitGroup
}
func NewPool(maxWorkers int, timeout time.Duration) *Pool {
    p := &Pool{
        taskQueue:    make(chan func(), 1000),
        idleWorkers:  make(chan *Worker, maxWorkers),
        maxWorkers:   maxWorkers,
        workerTimeout: timeout,
    }
    p.startWorkers()
    return p
}
func (p *Pool) startWorkers() {
    for i := 0; i < p.maxWorkers; i++ {
        w := &Worker{pool: p}
        p.idleWorkers <- w
        go w.run()
    }
}
func (p *Pool) Submit(task func()) {
    select {
    case p.taskQueue <- task:
        // 任务成功入队
    default:
        // 队列满时尝试复用空闲Worker
        p.lock.Lock()
        if len(p.idleWorkers) > 0 {
            w := <-p.idleWorkers
            w.task <- task
            p.lock.Unlock()
        } else {
            p.lock.Unlock()
            // 超过最大并发数,阻塞等待
            p.taskQueue <- task
        }
    }
}
type Worker struct {
    pool  *Pool
    task  chan func()
    quit  chan struct{}
}

func (w *Worker) run() {
    defer func() {
        w.pool.wg.Done()
        w.pool.lock.Lock()
        w.pool.idleWorkers <- w
        w.pool.lock.Unlock()
    }()
    for {
        select {
        case task := <-w.task:
            task()
        case <-time.After(w.pool.workerTimeout):
            return // 超时自动回收
        case <-w.quit:
            return
        }
    }
}

46、GC算法引用计数

引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象。

• 优点:对象可以很快地被回收,不会出现内存耗尽或达到某个阀值时才回收。

• 缺点:不能很好地处理循环引用,而且实时维护引用计数,也有一定的代价。

47、GC算法,标记清除

从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。

• 优点:解决了引用计数的缺点。

• 缺点:需要STW,即要暂时停掉程序运行。

• 代表语言:Golang(其采用三色标记法)

48、三色标记的组成

  1. 黑色 Black:表示对象是可达的,即使用中的对象,黑色是已经被扫描的对象。
  2. 灰色 Gary:表示被黑色对象直接引用的对象,但还没对它进行扫描。
  3. 白色 White:白色是对象的初始颜色,如果扫描完成后,对象依然还是白色的,说明此对象是垃圾对象。

49、三色标记的目的

  1. 主要是利用Tracing GC(Tracing GC 是垃圾回收的一个大类,另外一个大类是引用计数) 做增量式垃圾回收,降低最大暂停时间。
  2. 原生Tracing GC只有黑色和白色,没有中间的状态,这就要求GC扫描过程必须一次性完成,得到最后的黑色和白色对象。在前面增量式GC中介绍到了,这种方式会存在较大的暂停时间。
  3. 三色标记增加了中间状态灰色,增量式GC运行过程中,应用线程的运行可能改变了对象引用树,只要让黑色对象直接引用白色对象,GC就可以增量式的运行,减少停顿时间。

50、三色标记规则

黑色不能指向白色对象。即黑色可以指向灰色,灰色可以指向白色。

51、三色标记原理

起初所有对象都是白色。

从根出发扫描所有可达对象,标记为灰色,放入待处理队列。

从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。

重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

52、go混合写屏障

Go V1.8 引入的混合写屏障(Hybrid Write Barrier)是一种优化垃圾回收(GC)性能的机制,它结合了插入写屏障(Insert Write Barrier)和删除写屏障(Delete Write Barrier)的优点,以减少垃圾回收过程中的停顿时间(STW,Stop The World)。

53、go的插入写屏障

插入写屏障在对象A新增一个指向对象B的指针时触发。具体规则如下:

• 标记阶段:当对象A新增一个指向对象B的指针时,如果对象B是白色(未被标记),则将其标记为灰色(表示其需要被进一步扫描)。这样做可以确保在标记过程中不会遗漏任何可达对象。

• 目的:防止在并发标记过程中,由于新增的指针导致原本应该被回收的对象(白色对象)被错误地保留下来。

54、删除写屏障规则

删除写屏障在对象A删除一个指向对象B的指针时触发。具体规则如下:

• 标记阶段:当对象A删除一个指向对象B的指针时,如果对象B是灰色或白色,则将其重新标记为灰色(如果是白色,则直接标记为灰色;如果是灰色,则保持灰色状态)。这样做可以确保在后续扫描中,对象B仍然会被访问到,从而防止其被错误地回收。

• 清除阶段:在清除阶段开始时,所有在堆上的灰色对象都会被视为可达对象,因此不会被回收。删除写屏障确保了在并发修改指针的情况下,对象的可达性状态能够正确地被维护。

55、GC触发时机

1) gcTriggerHeap:当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发。

2)gcTriggerTime:当距离上一个 GC 周期的时间超过一定时间时,将会触发。时间周期以runtime.forcegcperiod 变量为准,默认 2 分钟。

3)gcTriggerCycle:如果没有开启 GC,则启动 GC。

4)手动触发的 runtime.GC 方法。

56、go内存泄漏情况

1) 如果 goroutine 在执行时被阻塞而无法退出,就会导致 goroutine 的内存泄漏,一个 goroutine 的最低栈大小为 2KB,在高并发的场景下,对内存的消耗也是非常恐怖的。

2)互斥锁未释放或者造成死锁会造成内存泄漏

3)time.Ticker 是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用 stop 方法才会停止,从而被 GC 掉,否则会一直占用内存空间。

4)字符串的截取引发临时性的内存泄漏

(5)切片截取引起子切片内存泄漏

57、内存逃逸的情况

1) 方法内返回局部变量指针。

2)向 channel 发送指针数据。

3)在闭包中引用包外的值。

4)在 slice 或 map 中存储指针。

5)切片(扩容后)长度太大。

6)在 interface 类型上调用方法。

58、go如何分配内存

  1. Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域
  2. arena区域按页划分成一个个小块
  3. span管理一个或多个页
  4. mcentral管理多个span供线程申请使用
  5. mcache作为线程私有资源,资源来源于mcentral

59、go是用什么数据类型

Method Boolean Numeric String Array Slice Struct Pointer Function Interface Map Channel

60、go struct 能不能比较

同struct类型的可以⽐较 不同struct类型的不可以⽐较,编译都不过,类型不匹配

61、log包线程安全吗?

Golang的标准库提供了log的机制,但是该模块的功能较为简单(看似简单,其实他有他的设计思路)。在输出的 位置做了线程安全的保护。

62、go除了mutex锁以外还有哪些方式安全读写共享变量?

Channel

63、go中常用的并发模型

1、 通过channel实现并发控制

2、 通过sync包的waitgrup实现并发控制

3、 引入context上下文、实现并发控制

64、死锁产生的四个必要条件

互斥条件:⼀个资源每次只能被⼀个进程使⽤

  1. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。

  2. 不剥夺条件:进程已获得的资源,在末使⽤完之前,不能强⾏剥夺。

  3. 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。 这四个条件是死锁的必要条件,只要系 统发⽣死锁,这些条件必然成⽴,⽽只要上述条件之⼀不满⾜,就不会发⽣死锁。

65、如何预防死锁

可以把资源⼀次性分配:(破坏请求和保持条件) 然后剥夺资源:即当某进程新的资源未满⾜时,释放已占有的资源(破坏不可剥夺条件) 资源有序分配法:系统给每类资源赋予⼀个编号,每⼀个进程按编号递增的顺序请求资源,释放则相反(破坏环路 等待条件)

66、如何避免死锁

预防死锁的⼏种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从⽽获得 较满意的系统性 能。由于在避免死锁的策略中,允许进程动态地申请资源。因⽽,系统在进⾏资源分配之前预先计算资源分配的安 全性。若此次分配不会导致系统进⼊不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避 免死锁算法是银⾏家算法。

67、如何检测死锁

⾸先为每个进程和每个资源指定⼀个唯⼀的号码,然后建⽴资源分配表和进程等待表.

68、如何解除死锁

当发现有进程死锁后,便应⽴即把它从死锁状态中解脱出来,常采⽤的⽅法有

69、如何剥夺资源

从其它进程剥夺⾜够数量的资源给死锁进程,以解除死锁状态

70、go的锁有哪些,三种锁,读写锁,互斥锁,还有map安全锁

互斥锁:sync.Mutex包中的类型只有两个公开的指针⽅法Lock和Unlock。

读写锁:读写锁是针对读写操作的互斥锁,可以分别针对读操作与写操作进⾏锁定和解锁操作 。 读写锁的访问控制规则如下: ① 多个写操作之间是互斥的 ② 写操作与读操作之间也是互斥的 ③ 多个读操作之间不是互斥的 在这样的控制规则下,读写锁可以⼤⼤降低性能损耗。

71、什么是channel,为什么它可以做到线程安全?

Channel是Go中的⼀个核⼼类型,可以把它看成⼀个管道,通过它并发核⼼单元就可以发送或者接收数据进⾏通讯 (communication),Channel也可以理解是⼀个先进先出的队列,通过管道进⾏通信。 Golang的Channel,发送⼀个数据到Channel 和 从Channel接收⼀个数据 都是 原⼦性的。

72、DataRace问题怎么解决

可以使⽤互斥锁sync.Mutex,解决数据竞争(Data race),也可以使⽤管道解决,使⽤管道的效 率要⽐互斥锁⾼

73、如何在运行时检查变量类型

类型开关是在运⾏时检查变量类型的最佳⽅式。类型开关按类型⽽不是值来评估变量。每个 Switch ⾄少包含⼀个 case,⽤作条件语句,和⼀个 defaultcase,如果没有⼀个 case 为真,则执⾏

74、waitgroup用法

⼀个 WaitGroup 对象可以等待⼀组协程结束。使⽤⽅法是: 1.main 协程通过调⽤ wg.Add(delta int)设置 worker 协程的个数,然后创建 worker 协程; 2.worker 协程执⾏结束以后,都要调⽤ wg.Done(); 3.main 协程调⽤ wg.Wait()且被 block,直到所有 worker 协程全部执⾏结束后返回。

75、WaitGroup 实现原理

aitGroup 主要维护了2 个计数器,⼀个是请求计数器 v,⼀个是等待计数器 w,⼆者组成⼀个64bit 的值, 请求计数器占⾼32bit,等待计数器占低32bit。 每次 Add执⾏,请求计数器 v 加1,Done⽅法执⾏,请求计数器减1,v 为0 时通过信号量唤醒 Wait()。

76、CAS

CAS的全称为 Compare And Swap,直译就是⽐较交换。是⼀条 CPU的原⼦指令,其作⽤是让 CPU先进⾏⽐较两 个值是否相等,然后原⼦地更新某个位置的值,其实现⽅式是给予硬件平台的汇编指令,在 intel 的 CPU中,使⽤ 的 cmpxchg指令,就是说 CAS是靠硬件实现的,从⽽在硬件层⾯提升效率

77、sync.Pool的作用

,⽽ sync.Pool 可以将暂时不⽤的对象缓存起来,待下次需要的时候直 接使⽤,不⽤再次经过内存分配,复⽤对象的内存,减轻 GC 的压⼒,提升系统的性能。

78、new与make的区别

New只能初始化并返回指针,而make不仅仅要做初始化,还需要设置一些数组的长度、容量等

79、解释以下命令的作用

Go env:用于查看go的环境变量

Go run:用于编译并运行go源码文件

Go build 用于编译源码文件、代码包、依赖包

Go set:用于动态获取远程代码包

Go install:用于编译go文件,并将编译结构安装到bin、pkg目录

Go clean:用于清理工作目录,删除编译和安装遗留的目标文件

Go version:用于查看go版本信息

80、说说go语言中的协程

协程和线程都可以实现程序的并发执行,通过channel来进行协程间的通信

只需要在函数调用前添加go关键字即可实现go的协程,创建并发任务,关键字go并非执行并发任务,而是创建一个并发任务单元。

81、引用类型包括哪些

切片、字典、通道、指针、接口

82、go语言的同步锁

当一个goroutine获得了mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个mutex

Rwmutex在读锁占用的情况下,会组织写,但不阻止读

RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占

83、go语言的channel特性

通信同步机制:安全的数据传输、同步等待

类型安全、阻塞特性:发送阻塞、接收阻塞

缓冲功能:缓冲机制、非阻塞发送与接收

方向限制

可关闭性

支持多路复用

84、go的select机制

多路复用、随机选择、阻塞行为、超时处理

使用场景:同时等待多个通道操作、实现超时功能、非阻塞通信、处理多个goroutine的结果

85、字符串转成byte数组,会产生内存拷贝吗?

字符串转成切片,会产生拷贝,严格来说,只要是发生类型强转都会发生内存拷贝,

86、字符串连接

Bytes.buffer、strings.builder、join

87、不同类型如何比较是否相等?

答:像 string,int,float interface 等可以通过 reflect.DeepEqual 和等于号进行比较,像 slice,struct,map 则一般使用 reflect.DeepEqual 来检测是否相等。

相关推荐
故事与他6453 小时前
Thinkphp(TP)框架漏洞攻略
android·服务器·网络·中间件·tomcat
郑州吴彦祖7724 小时前
【Java】UDP网络编程:无连接通信到Socket实战
java·网络·udp
kfepiza5 小时前
netplan是如何操控systemd-networkd的? 笔记250324
linux·网络·笔记·ubuntu
九转苍翎6 小时前
Java EE(12)——初始网络
网络·java-ee
Honeysea_707 小时前
网络编程和计算机网络五层模型的关系
网络·计算机网络
冯渺岚7 小时前
Lua语言的配置管理
开发语言·后端·golang
独行soc8 小时前
2025年渗透测试面试题总结- shopee-安全工程师(题目+回答)
java·网络·python·科技·面试·职场和发展·红蓝攻防
小码本码8 小时前
TCP/IP协议的三次握手和四次挥手
网络·网络协议·tcp/ip
慢德9 小时前
HTTP长连接与短连接的前世今生
网络·https
Python数据分析与机器学习9 小时前
《基于Python+web的家具消费数据的数据分析与应用》开题报告
开发语言·网络·分布式·python·web安全·数据分析·flask