总 :https://www.bookstack.cn/read/qcrao-Go-Questions/interface.md
不要通过共享内存来通信,要通过通信来共享内存
- 降低共享内存的使用,本来就是解耦和的重要手段之一
- 理解时go使用主动的channel通信以最小限度使用这些存在channel里的内存空间,与其他通信的goroutine共享这个channel,范围可以控制在必要的最小规模;而不是先设定好共享内存,再其他开发过程中通过互斥锁、条件变量等方式提供给不同线程去共享内容,导致
阐述golang并发机制
- goroutine
- channel
- waitgroup管理goroutine
为什么小对象多了会造成gc压力
- 内存碎片
- gc时会移堆,将对象从一个堆移动到另一个堆(内存拷贝)
- 标记的内存块也变多了,遍历的时间变长了
gc的触发条件
- 内存使用量超阈值,这个阈值可以用debug.ReadGCStats的包来看,
- 使用runtime.GC手动触发
gc的栈空间管理机制是什么
- runtime负责
- 每个goroutine分配一个固定栈空间,大小大概在2kb到4kb左右
- 栈空间不够时,runtime自动扩展栈的大小,回收时runtime回收栈空间变量
defer原理
defer的原理是先进后出的,遇到defer时,将defer后的函数用语句进行压栈处理。
- 底层实现
每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单连表,保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。
select原理,多路复用机制
监听多个channel,与linux多路复用的select区别是linux的select是轮训一个数组,golang是基于事件驱动,有通信操作是才执行时才会进行操作
是在编译过程中的静态分析机制,优化内存分配用来决定各个变量分配在堆上还是栈上,如果一个变量在函数内部初始化,但是传递到外部了,就说发生逃逸,分配到heap上
线程模型有哪些?为什么go scheduler需要实现M:N方案?scheduler 由哪些元素组成
- M:N 线程模型:
Go 语言采用了 M: N 线程模型。在这个模型中,多个用户线程会映射到少量的操作系统线程上,这些操作系统线程被称为 M(Machine)。同时,Go 语言的调度器(Scheduler)负责在这些 M 之间分发工作。
- 灵活,轻量级的用户态goroutine可以避免系统级别的上下文切换开销
- 通过runtime去调度,
- 组成元素:GPM,本地队列全局队列
解释hand off,work stealing
当一个任务队列满,没有空闲的P时,调度器会选择一个空闲的p,直接分配给该处理器执行。 - 当一个P执行完自己的任务后,它可以尝试从其他处理器的队列中窃取(steal)一个任务来执行。这样做的目的是使得各个处理器的负载尽量均衡。
mutex 有几种模式【正常和饥饿】
正常模式保证了公平竞争,适用于大多数情况,而饥饿模式则优先保证了长时间等待的协程能够获得锁。
- mutex没有提供接口,要引入一个计数器来实现饥饿模式
defer和return的先后顺序
return先执行获取返回值,然后暂停函数的执行,接下来就按defer的压栈顺序执行defer语句,顺序是后进先出的顺序go recover的执行时机
需要进行defer func捕获上级的panic:
recover 必须在 defer 函数中运行。recover 捕获的是祖父级调用时的异常,直接调用时无效。
闭包错误引用同一个变量问题怎么处理 ?
- 将闭包需要引用的变量作为参数传递给闭包函数,而不是直接在闭包内部引用外部变量。
- 在闭包函数里创建一个新的临时变量
负载因子为什么是6.5
https://blog.csdn.net/eddycjy/article/details/120359475golang中的大端序和小端序
大端序是低地址存高字节,高地址存低字节,同时也是网络字节序【大端就是顺序从左到右存放】,解析之后就是字符顺序
小端时低地址存低字节,高地址存高字节,是主机序【golang默认小端序,主机x86和arm64都是小端】,小端主机虚,符合电路的读取逻辑
syncOnce是什么【确保一个代码块只执行一次,一般用来实现一个线程安全的单例模式】
routine为什么比thread轻量
- routine是纯用户态调度,非抢占,由runtime管理,创建,切换的开销不需要内核态参与
- 协程在同一个地址空间共享堆栈,每个线程都有自己独立的堆栈
为什么要用协程,好处是什么
go的协程是为了解决多核CPU利用率问题,go语言层面并不支持多进程或多线程,但是协程更好用,协程被称为用户态线程,CPU上下文切换效率非常高。几乎所有IO密集型的应用,都可以利用协程提高速度原子操作和锁的区别
- 原子操作是对共享变量的单一操作,要么执行完药么全不执行;锁对一段临界区代码,操作的变量可以有一堆
- 原子操作开销小,锁开销较大,涉及到上下文切换等
go的多返回值如何实现
uintptr和unsafe.Pointer的区别
- uintptr:将指针转换整数表示,不包含指针的类型信息。
- unsafe.Pointer:包含任意类型指针,将任意类型的指针转换为通用指针类型,很灵活
uintptr 是一个整数类型,它被用于存储指针的整数表示形式。
使用 uintptr 可以将指针转换为整数,也可以将整数转换为指针,但这种转换是不安全的,可能会导致未定义的行为。
因为 uintptr 只是整数,不包含指针的类型信息,因此在转换后需要谨慎使用,可能会导致类型不匹配或内存安全问题。
1 | var p *int |
- unsafe.Pointer
unsafe.Pointer 是一个特殊的指针类型,它可以包含任意类型的指针,并允许在不进行类型检查的情况下进行指针操作。
使用 unsafe.Pointer 可以将任意类型的指针转换为通用的指针类型,也可以将通用指针转换为具体类型的指针。这种转换也是不安全的,可能会导致未定义的行为。
示例:1
2var p *int
pointerValue := unsafe.Pointer(p)
switch中如何强制执行下一个case块【fallthrough关键字,忽略后续条件的判断,直接执行下一个 case 的代码】
如何关闭http响应体
在defer里close,或者用完就close
解析json时,默认将数值当作哪种类型【数值默认为float64】
如何从panic中恢复
defer func(){ recover() }
解释一下静态类型声明
golang生命变量时是在编译阶段确定类型
Golang的可变参数是什么,怎么用,要注意什么
1 | func sum(nums ...int) int { |
注意:
- 可变参数必须是函数参数列表的最后一个参数:如果函数有多个参数,可变参数必须放在参数列表的最后。
- 可变参数可以不传递:如果调用者不传递任何参数,可变参数会被初始化为空切片。
- 可变参数可以传递多个值:可以传递任意数量的参数,甚至可以传递零个。
- 调用时可以传递切片:如果已经有一个切片,可以在调用函数时使用 … 操作符将其展开为可变参数。
golang支持接口的多继承(C extends A and C)吗【不支持,依靠组合实现】
go多返回值
go多返回值是通过栈传递的。将多个返回值先传回参数上,函数栈帧销毁后并不会销毁参数部分(这里用作返回值),再将参数部分进行拷贝然后再参与运算
简述scheduler函数
runtime.Gosched():
Gosched() 函数手动触发一次调度,它会将当前 Goroutine 放回队列并让其他等待执行的 Goroutine 有机会运行。这个函数主要用于释放一些处理器资源给其他 Goroutines 使用。
简述全局运行队列中获取goroutine的时机【其他本地队列中没有可stealing的】
简述如何从工作线程的本底运行队列中获取routine【运行队列为空时】
init是什么时候执行的【包初始化阶段,程序开始执行前】
Map
1. map的key为什么无序,如何处理冲突的【链地址法】
底层用hash实现的,维护了一个hmap和bmap,bmap是bucket存实际的key,
2. map可以边遍历边删元素吗【不能】为什么
线程不安全,删除的时候会导致存储结构发生变化,
3. float类型可以作为key吗,哪些不可以作为map的key
从语法上看,是可以的。Go 语言中只要是可比较的类型都可以作为 key。除开 slice,map,functions 这几种类型,其他类型都是 OK 的。具体包括:布尔值、整型、字符串、指针、通道、接口类型、结构体、只包含上述类型的数组。
channel 也可以当key?
4. 非接口的任意类型都能调用 *T方法吗
不能吧,至少引用类型不能
5. map的赋值过程 底层用了mapassign函数
对 key 计算 hash 值,根据 hash 值按照之前的流程,找到要赋值的位置(可能是插入新 key,也可能是更新老 key),对相应位置进行赋值。
6. 如何实现两种get操作
map重载了两个函数一个带comma的一个不带comma的
7. map删除一个key,内存会释放吗【不会,要等gc扫描过来】
8. 解析tag怎么实现的
反射实现的,用Field(i).Tag
map可以取地址吗【不能】
本身就是一个指向其他地址的指针,会导致编译错误
9. rune是int32
10. 值receiver 和 指针receiver的区别
值receiver 是创建结构体的一个副本,不修改原始字段的value
指针rcver是在原结构体实例上操作
概念
类型
1 | 数值类型: |
1.什么是协程
是Golang提供的线程调度的基本单位。
- 一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会自动伸缩,因此可以轻易实现成千上万个goroutine同时启动。
- 每个goroutine(Go程序并发执行的基本单元)都会分配一块独立的栈内存,用于保存函数的局部变量、参数等信息。
- 和线程的对比:一个是切换管理用runtime,没有内核态参与,资源协程共享地址空间;通信手段 使用通信共享内存,thread使用共享内存通信
2.介绍一下channel
- channel时go提供的用于并发编程的特殊类型,使 goroutine 之间的进行数据传递和共享,避免了显式的锁机制,比较安全和高效
Go以通信的手段来共享内存
- 包括有缓冲和无缓冲channel,其中无缓冲是同步的,有缓冲异步的
- 底层数据结构是hchan的结构体,内部是一个循环数组
new和make
new分配内存,返回一个指向某类型指针,make创建slice map channel的实例,初始化
- 不能用 new() 来创建 slice、map、chan 这样的引用类型。如果用 new() 来创建 slice,那么创建的 header 中的 pointer 做0值处理,就会被初始化为 nil,而 length 和 capacity 也会被初始化为0,这样显然是不正确的。
【问题】有缓冲channel和无缓冲channel的区别【阻塞情况,同步异步】
- 对于无缓冲区channel:
发送的数据如果没有被接收方接收,那么发送方阻塞;如果一直接收不到发送方的数据,接收方阻塞; - 有缓冲的channel:
发送方在缓冲区满的时候阻塞,接收方不阻塞; 接收方在缓冲区为空的时候阻塞,发送方不阻塞。
【问题】channel的等待队列如果写满了,内存占用很高,怎么解决
- 读协程可能出现问题,去修改
- 限制写操作的并发数量,避免大量写
- 使用select和超时机制
1
2
3
4
5
6select {
case ch <- value:
// 写入成功
case <-time.After(time.Second):
// 超时处理
}
【问题】协程泄漏是什么,什么引发的,怎么解决
- 程序中创建的某些协程没有被正确地释放或终止(或者发生死锁),从而导致这些协程持续存在并占用资源(阻塞,死锁,无限循环)
- 解决方法
- 内存分配器:维护一块大的全局内存,每个线程(Golang中为P)维护一块小的私有内存,私有内存不足再从全局申请。
- 预申请的内存划分为span(512MB),bitmap(16G),arena(512G堆区域),span和bitmap是管理堆区域,每个页的大小为8KB。
4.介绍一下Go的GC机制:以防止内存泄漏
- Go使用的是三色标记法,已被引用的被mark表示不可回收,未引用的被回收掉。
- 这里的标记由一个管理内存分配的数据结构mspan管理,按内存块维护资源
- mspan这个结构体中,使用allocBits位图表示每个内存块的分配情况,使用gcmarkBits标记内存块被引用的情况
- 这里的标记是从根对象进行递归扫描记录的,因为存在指针变量和记录的逻辑地址
- 标记队列存放待标记的对象,灰色表示等待,白色未被标记,黑色被标记,把标记值记录在gcmarkBits中,标记的表示正在被引用
- 白色对象:尚未被访问,处于初始状态。
- 灰色对象:已被访问,但其引用还未被访问。
- 黑色对象:已被访问,且其引用也已被访问。
- STW机制:停掉所有的goroutine,专心做垃圾回收,回收白色对象,结束后恢复goroutine
【问题】对STW的优化是什么?混合写,并发垃圾回收
Go 通过在后台运行一个专用的垃圾回收线程,与程序的其他部分并发地进行垃圾回收。 - 并发标记
- 混合写,将并发标记和 STW 结合起来的阶段。在这个阶段,部分垃圾回收工作会在并发进行,同时也会暂停所有 Goroutine 进行一些必要的 STW 操作。
- 并发清理
混合写导致的问题,为了减少停顿时间
- 内存和CPU开销,因为要引入额外元信息
- 在某些情况下,混合写屏障可能会导致一些额外的延迟,尤其是对于极短寿命的对象,因为它们在逃逸到堆之前可能会留在栈上
【问题】根对象是什么
在Go语言中,全局变量、栈上的变量以及程序计数器指向的对象等都被认为是根对象。
垃圾回收器会从这些根对象出发,逐步遍历所有可以访问到的对象,并标记它们。
【问题】goroutine 可以无限创建吗,如果创建过多会有什么后果?从GC的角度来考虑
理论上可以无限创建,取决于操作系统的限制,比如内存大小
- goroutine执行完会产生垃圾,增大gc压力
- 标记阶段时会遍历对象,goroutine多了会导致标记的压力增加
- 停顿时间变长:需要回收大量的内存,可能会导致垃圾回收器的停顿时间变长
【TIPs】在程序中要避免在短时间内产生大量临时对象,以减小垃圾回收的压力。
如何优化STW机制
- 混合写屏障:类似于一种开关,在GC的特定时机开启,开启后指针传递时会把指针标记,即 本轮不回收,下次GC时再确定
- 辅助GC:新分配的goroutine如果要分配内存,那就去辅助完成一部分gc工作,也就是自己的资源自己挣的感觉
4.介绍一下GMP
- G : 协程 goroutine
- P : 处理器 processor : 和M一对一,runtime.Gomaxprocs配置
- M : 线程 thread :runtime.setMaxThreads最大10000个,
- 有一个M阻塞,会创建一个新M
- 有M空闲,就回收或睡眠M
- 线程是运行 goroutine 的实体,调度器的功能是把可运行的 G 分配到工作线程
M 上 - 全局队列:存放正在等待运行的G
- 本地队列:不超过256个G
【问题】调度器P的workstealing机制和handoff机制
- work stealing
- 当本线程M没有可运行的G时,尝试从其他线程绑定的P中偷G
- 当从其他线程偷不到时,从全局队列偷取(为什么?因为全局队列有锁)
- hand off
- 当本线程M因为有G阻塞时,会释放自己的P给另一个唤醒/新建的M执行(runtime调度器来做detach)
【问题】go func(){} 的执行流程
- 创建一个G,优先加入到func所在线程M对应P的本底队列中,满了的话,放在全局队列中
- G运行在M中,如果本地G队列为空,就去其他M P组合去偷
- 【问题】当M系统调用结束时,所属的G会尝试获取一个空闲P去执行,并加入到这个P的G队列,如果找不到,就休眠这个M,并将这个G加入到全局队列
【问题】Go的生命周期 M0,G0是什么
M0
- M0指程序启动时,编号为0主线程,runtime的M0
- M0负责初始化和启动第一个G
- 启动G之后,和其他M地位一样了
G0
- 每次启动一个M,都会有一个G0
- G0仅负责调度其他的G1,G2
- G0本身不执行任何func
- G1执行完,先执行G0,G0再切换其他的G2
【场景1】G1嵌套创建G3
保证局部性,G3优先加入G1所在的本底队列,满了的话看场景3
【场景2】G执行完毕
执行完毕后,切换G0,G0调度切换下一个G
【场景3】连续创建多个G导致本地队列满
- 什么时候唤醒?调度器自动唤醒,当某个 Goroutine 可以被执行时,当一个被阻塞的 Channel 操作可以继续执行时。
新M所在的G队列如果为空,称为自旋线程,不断寻找G
- 由于自旋线程拥有P,handoff机制不会把P给自旋线程
【场景5】自旋线程从哪里获取G
- 首先从全局队列获取
- 全局队列如果为空,触发workstealing,从其他队列队尾偷一半
基本操作
字符串拼接
strings.Join ≈ strings.Builder > bytes.Buffer > “+” > fmt.Sprintf
slice中删除具体的值
移位法最快。
原地删除,扫描到具体值后,使用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
## b = b[:len(a)] 作用
- 优化边界检查 Bounds Check ELimination
- 在运行时,Go 语言每次都会对 b[i] 做边界检查,看看是否越界了,如果越界了,就 panic。
- 如果加上这一句,Go语言在编译时,能够做一些简单的静态分析,发现 b[i] 是不可能越界的
## error
【面试问题】如果一个函数的返回值是error,里面执行了多个defer,并且这些defer里面调用了不同的方法,也会返回error,但是这些error的格式是不一样的(比如有一些方法返回的是官方的errors,有一些是业务定义的错误,比如错误码和错误信息)。怎么样能统一处理这些defer的错误并且返回?
- go泛型接收不同error类型,由特殊需求的话,使用断言判断后返回特定信息
## 【问题】控制goroutine超时退出
- 使用 context 包
```go
func main() {
// 创建一个上下文,设置超时时间为 2 秒
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 在完成任务后取消上下文以释放资源
// 在另一个 goroutine 中执行任务
go func() {
// 模拟一个耗时的任务
time.Sleep(3 * time.Second)
// 判断上下文是否被取消
if ctx.Err() == context.Canceled {
fmt.Println("Task canceled due to timeout")
}
}()
// 等待一段时间,以确保上下文超时
time.Sleep(4 * time.Second)
}
【问题】Go语言闭包【让闭包函数拥有一个初始状态,记住了创建时所在的环境,各种变量的值,减少全局变量的使用,在栈区方便回收】
简单来说,闭包允许一个函数记住并访问了它创建时所在的环境,即使在这个函数在其他地方被调用时仍然可以使用这个环境中的变量
- 闭包用来减少全局变量,在函数调用过程中隐式传递共享变量
- 编译器检测到闭包,将外部变量分配到堆上
- 下面的程序中,a分配在堆上
1 | func fn(a int) func(i int) int{ |
panic和recover
函数签名
1 | panic(i interface{}) |
主动调用/抛出panic
- 主动调用panic结束程序运行
- 调试时用panic快速退出,并打印出来堆栈信息
- 需要主动在程序分支流程上调用recover拦截错误
标准库
IO操作
1 | os.Stdin:标准输入的文件实例,类型为*File |
实现一个cat命令
1 | // cat命令实现 |
net包
context包
当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
1 | var wg sync.WaitGroup |
Zinx
ziface
接口包括
1 | - IService 基础服务的启动 |
1 | package main |
Job结构体:id int: 用于表示一个任务的唯一标识。
Worker结构体:id int: 表示工作者的唯一标识。jobChannel chan Job: 是一个任务通道,用于接收工作者执行的任务。quit chan bool: 是一个退出通道,用于通知工作者停止运行。
Pool结构体:workerCount int: 表示协程池中的工作者数量。jobChannel chan Job: 是一个任务通道,用于向协程池中添加任务。workers []Worker: 存储了所有的工作者。
方法解析:
NewJob(id int) Job:- 返回一个新的任务
Job对象,带有指定的任务ID。
- 返回一个新的任务
NewWorker(id int, jobChannel chan Job) Worker:- 返回一个新的工作者
Worker对象,使用指定的工作者ID和任务通道。
- 返回一个新的工作者
NewPool(workerCount, jobCount int) Pool:- 创建一个新的协程池,初始化了工作者和任务通道。
- 参数
workerCount表示协程池中的工作者数量。 - 参数
jobCount表示任务通道的缓冲区大小。
Worker.Start():- 启动了一个工作者协程,该协程会不断地监听任务通道和退出通道。
- 当从任务通道收到任务时,工作者会执行任务;当从退出通道收到信号时,工作者会停止运行。
Worker.Stop():- 启动了一个协程,向退出通道发送信号,通知工作者停止运行。
Pool.Start():- 启动了协程池中所有工作者。
Pool.Stop():- 停止协程池中所有工作者。
Pool.AddJob(job Job):- 向任务通道中添加一个任务。
主函数 main 解析:
创建一个协程池
pool,包括了 3 个工作者和 10 个任务的通道缓冲区。调用
pool.Start()启动所有工作者。循环创建了 5 个任务,每个任务被添加到协程池的任务通道中。
由于在主函数结束后,主协程也会结束,所以在这里使用了
select{}语句使主协程保持活跃状态。
运行流程:
在主函数中创建了一个协程池
pool,初始化了 3 个工作者和一个任务通道。每个工作者通过
Worker.Start()方法启动了一个独立的协程,开始监听任务通道和退出通道。主函数循环创建了 5 个任务,并通过
pool.AddJob(job)方法将它们添加到协程池的任务通道中。每个工作者从任务通道中接收到任务后,会执行相应的任务。
当主函数结束后,通过
select{}语句使主协程保持活跃状态,保证所有工作者有足够的时间来处理任务。
请注意,实际应用中,你可能需要使用合适的同步机制(例如 sync.WaitGroup)来确保所有任务完成后再关闭协程池,以及处理一些错误和异常情况。
3.下面赋值正确的是()
A. var x = nil
B. var x interface{} = nil
C. var x string = nil
D. var x error = nil
参考答案及解析:BD。知识点:nil 值。nil 只能赋值给指针、chan、func、interface、map 或 slice 类型的变量。强调下 D 选项的 error 类型,它是一种内置接口类型,看下方贴出的源码就知道,所以 D 是对的。
1 | type error interface { |