Golang面试常见问题
golang内存逃逸概念,如何解决(腾讯)
go语言内存逃逸是当一个对象的生命周期超出当前函数的作用域时,编译器将这个对象分配到堆上而不是栈上。
减少指针使用:因为只有指针才能访问堆数据。比如一个结构体很小,且只在函数内部使用,则传值比指针更好。
控制作用域:用大括号定义小的作用域创建临时变量。
预分配内存:用对象池sync Pool复用对象,减少堆分配。
GMP模型概念,作用(腾讯,美团)
goroutine是轻量级线程实现方式。
machine是实际执行代码的线程。
Processor是逻辑处理器和调度器,维护本地运行队列(存放可执行的goroutine)。当P本地队列空时,通过任务窃取从其他P获取G。
go map怎么解决并发问题(腾讯)
map是非线程安全的。
互斥锁Mutex:用sync Mutex对map读写加锁,缺点是锁竞争会造成瓶颈。
分片锁:将map划分为多个分片,每个分片独立加锁。
sync map:他是线程安全的,但不保证类型安全。
go语言interface底层如何实现(腾讯)page 164
通过eface和和iface表示空接口和非空接口。
空接口的底层结构:_type指向动态类型的元数据,data指向实际的指针。
非空接口的底层结构:itab缓存接口类型和动态类型的关系及方法表。
用接口调用方法时,go通过itab fun中的方法表找到具体实现。
go语言中Channel底层的实现(Channel底层原理)(腾讯)page 164
channel 是怎么保证线程安全?
底层数据结构:hchan结构体,包含互斥锁、缓冲区(环形队列)、队列信息(包含sendq和recvq发送接收等待队列,存储阻塞的goroutine,通过sudog封装)。
发送和接收流程:1.加锁。2.直接传递:如recvq有等待的接收者,直接拷贝给他并唤醒它。3.缓冲区操作:发送如缓冲区未满就写入。4.阻塞:缓冲区满,将当前goroutine打包为sudug加入队列。
Go语言channel关闭了,读写这个channel会发生什么(腾讯)page 164
向已关闭的Channel写数据:触发panic,Channel有缓冲以及没有缓冲都会panic。
从已关闭的Channel读数据:如Channel有缓冲数据,正常读取。如没有缓冲数据,返回Channel类型零值,且第二个返回参数ok为false。
go怎么实现线程池(美团)
任务队列:用Channel作为任务队列,接收待处理的任务。
工作协程池:启动固定数量Worker Goroutine,从任务队列消费并执行任务。
同步:用sync waitGroup等待所有任务完成。
go语言协程是对称的还是不对称的?(腾讯)page 164
go语言协程是对称的。
调度模型对称性:调度器采用协作式抢占调度,所有Goroutine在调度器中是平等的。
协程行为的对称:Goroutine之间不需要生成器如python的yield主动交出控制权。
go语言Context你用过吗(腾讯)page 164
Context是处理请求生命周期和跨api传递数据的工具。
实际使用场景:1.聚合多个服务结果:调用多个外部服务时,某个服务响应慢导致整体延迟。用WithTimeout控制统一的超时时间。2.数据库查询取消:用户取消请求后,db操作仍在进行。可以检查Context canceled回收资源。3.跨服务传递元数据:微服务调用链中要传递trace id用于日志聚合,用withValue。
go语言sync包mutex实现(字节)page 164
结构体与状态管理:Mutex内部通过state字段管理锁的状态,二进制位有不同的信息:第0位表示锁是否被持有,第1位是否处于饥饿模式,剩余位表示等待锁的Goroutine数量。
两种模式:正常模式:新请求的Goroutine通过自旋获取锁,自旋失败进入等待队列。饥饿模式:当某个Goroutine等待超过1毫秒,触发,锁直接交给等待队列队首Goroutine,新到达Goroutine直接进入队尾。
加锁流程:快速路径:用cas直接获取。慢速路径:自旋,若失败进入阻塞队列。
go语言中协程和线程的区别?(字节)page 1
资源消耗与创建开销:一个goroutine的堆栈初始只需要几KB内存,而一个线程可能需要MB级别的空间。因此,你可以在单个程序中启动成千上万甚至更多的goroutines,这在线程模型下几乎是不可能实现的。
调度机制:Goroutines由Go runtime自行调度,并且能够跨多个OS线程进行负载均衡。这意味着即使某个操作被阻塞了,也不会影响到其他正在工作的goroutines。相比之下,操作系统对线程的调度则更重、更慢,涉及用户态与内核态切换。
通信方式:Go提倡使用通道(channel)来进行goroutine之间的安全通信,这种方式比直接共享内存要简单得多也安全得多。而对于多线程应用来说,处理好同步问题往往比较复杂,容易出现竞态条件等问题。
go语言中数组和切片区别?(字节)page 1
长度固定:数组声明时需指定长度。切片长度动态,用append扩展。
传递方式:数组值传递,切片是引用类型,包含指针、长度len和容量cap。
go语言结构体作为参数传递时是传值还是传指针(字节)page 161
结构体是值传递。
go语言如何对map排序(字节)page 161
map底层无序。排序可以用切片和sort实现。
key排序:1.提取map所有key存入切片。 2.对key的切片排序。 3.遍历排序后的key切片,顺序访问map的值。
按值排序:1.定义结构体切片,存key和值。 2.填充切片。 3.用sort Slice自定义排序规则。
go语言垃圾回收机制(字节)page 161
机制:三色标记清除法。标记阶段,将对象分为白,灰黑,从根对象(栈、全局变量)出发,递归标记可达对象。清除阶段,回收未标记的白色对象。
工作流程:1.stw初始标记,扫描根对象。 2.并发标记,用写屏障记录并发修改。 3.stw重新标记。 4.并发清除。
触发条件:默认当堆内存大小达到上次gc存活对象的两倍(由gogc环境变量调整)。
讲讲golang 写屏障解决什么问题?没有写屏障会导致什么问题?(B站)page 2
写屏障维护三色标记法不变性。没有写屏障有漏标问题,并发标记中如果程序对对象指针修改,但没有对应的写屏障记录,那么gc会漏掉修改后可达的对象。
go语言中并发utils了解多少(字节)page 161
Goroutine和Channel:Goroutine是轻量级协程,Channel用于协程间通信。
sync包:1.mutex rwmutex互斥锁和读写锁。2.waitgroup:等一组Goroutine完成。once:保证代码执行一次。
atomic包:提供原子操作,add,cas。
context包:传递取消信号,控制多级Goroutine生命周期。
go语言如何两个协程实现奇偶交替打印(百度)
定义两个Channel,分别用于奇数线程和偶数线程信号传递。
奇数线程收到信号后打印一个奇数,然后向偶数协程发信号;偶数协程收到信号后打印偶数,将控制权交给奇数线程。
主线程启动后,先发送一个信号给奇数线程启动打印。
package main
import (
"fmt"
"time"
)
func main() {
oddCh := make(chan bool) // 奇数打印协程信号通道
evenCh := make(chan bool) // 偶数打印协程信号通道
// 奇数打印协程
go func() {
for i := 1; i < 10; i += 2 { // 假设打印 1,3,5,7,9
<-oddCh // 等待信号
fmt.Println(i) // 打印奇数
evenCh <- true // 通知偶数协程继续
}
}()
// 偶数打印协程
go func() {
for i := 2; i <= 10; i += 2 { // 假设打印 2,4,6,8,10
<-evenCh // 等待信号
fmt.Println(i) // 打印偶数
oddCh <- true // 通知奇数协程继续
}
}()
// 启动流程:先给奇数协程发送信号
oddCh <- true
// 为了防止主函数退出,这里用 select 阻塞
//select {}
time.Sleep(1 * time.Second)
}
go语言按照列遍历二维切片(百度)
参考。 可以先确定列数,再逐列访问每一行元素。
package main
import (
"fmt"
)
func main() {
// 定义一个二维切片(假设为规则矩阵,每行长度相同)
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
// 获取行数和列数
rows := len(matrix)
if rows == 0 {
return
}
cols := len(matrix[0])
// 按列遍历
for j := 0; j < cols; j++ { // 外层循环遍历列
for i := 0; i < rows; i++ { // 内层循环遍历行
fmt.Printf("%d ", matrix[i][j])
}
fmt.Println() // 每遍历完一列换行输出
}
}
go语言的goroutine是什么(百度)page 160
Goroutine是go轻量级线程。Goroutine的初始栈空间非常小,且由go运行时调度器管理。
go语言实现发布订阅模型(百度)
用Channel实现,发布订阅模型是发布者发布消息,订阅者接收感兴趣的消息,且订阅者异步接收。
package main
import (
"fmt"
"sync"
"time"
)
// 定义消息结构体
type Message struct {
Topic string
Content string
}
// 发布者
type Publisher struct {
subscribers map[string][]chan Message
mu sync.RWMutex
}
// 创建一个新的发布者
func NewPublisher() *Publisher {
return &Publisher{
subscribers: make(map[string][]chan Message),
}
}
// 发布消息
func (p *Publisher) Publish(topic string, msg Message) {
p.mu.RLock()
defer p.mu.RUnlock()
if chans, ok := p.subscribers[topic]; ok {
for _, ch := range chans {
ch <- msg
}
}
}
// 订阅消息
func (p *Publisher) Subscribe(topic string) chan Message {
p.mu.Lock()
defer p.mu.Unlock()
ch := make(chan Message)
p.subscribers[topic] = append(p.subscribers[topic], ch)
return ch
}
// 主程序
func main() {
publisher := NewPublisher()
// 订阅者1订阅"news"主题
subscriber1 := publisher.Subscribe("news")
// 订阅者2订阅"news"主题
subscriber2 := publisher.Subscribe("news")
// 启动订阅者1的接收消息 goroutine
go func() {
for msg := range subscriber1 {
fmt.Printf("Subscriber1 received: %s\n", msg.Content)
}
}()
// 启动订阅者2的接收消息 goroutine
go func() {
for msg := range subscriber2 {
fmt.Printf("Subscriber2 received: %s\n", msg.Content)
}
}()
// 发布消息到"news"主题
publisher.Publish("news", Message{Topic: "news", Content: "Hello, World!"})
publisher.Publish("news", Message{Topic: "news", Content: "Go is awesome!"})
// 让主程序等待一会儿
time.Sleep(1 * time.Second)
}
go语言 GMP中M对应的内核的什么,G除了系统调用会阻塞还有什么会阻塞(在 Go 的 GMP 模型中,M 在操作系统内核中对应什么?除了系统调用外,还有哪些情况会导致 G 被阻塞?)(百度)page 136
M对应操作系统线程。G是轻量级协程,由go运行时调度在多个M上执行,P是调度器。每个M绑定一个P来执行G,但一个M可以与多个P交替工作。
阻塞原因:1.等待其他Goroutine,如通过Channel。2.锁竞争。3.调度器阻塞,G没有可用的P。
go语言 P怎么调度G的(百度)page 160
每个P有一个本地队列,存储要执行的Goroutine。
当一个M绑定到P后,M会从P的本地队列中取出一个G执行。
如果P队列为空,M会从其他P上偷取Goroutine执行。
执行完一个G后,M继续从P队列中取下一个G。
讲讲go func执行的过程?page 14
创建一个Goroutine。
队列存储。有两个存储G的队列,一个是P的本地队列,一个是全局G队列,新创建的G会首先保存在P的本地队列中,如果本地队列满,那么G会存入全局队列。
G执行调度。G需要M(操作系统线程)执行。每个M必须绑定一个P,M和P是1比1关系,M从P本地队列弹出一个G执行,如果P本地队列为空,M从其他MP组合窃取G,或者从全局队列获取G。
M调度G执行。是循环机制,M会不断调度G,直到所有的G都执行。如果G发生系统调用,M也会阻塞,剩余的G会启动一个新的M处理。
讲讲go park和go ready的底层原理。page 14
go运行时调度器是m比n调度模型,m是操作系统线程,n是Goroutine。调度器中go park用于挂起Goroutine,goready用于唤醒Goroutine,他们是调度原语。
gopark。Goroutine因io或Channel、锁无法运行,会调用gopark将Goroutine状态从Runnable变为waiting或blocked。调度器将g从p本地队列移除。
goready。goready将唤醒的Goroutine加入对应的p的本地队列,如满则放入全局队列。
讲讲go协程的栈内存管理。page 14
小栈起步。每个Goroutine启动分配很小空间如2K字节。
动态栈扩展。当函数调用层次加深,运行时检测到进行栈拓展,运行时分配一块更大的内存区域,将现有栈复制,并更新指针。
讲讲go语言sync pool底层原理。page 14
按P本地缓存。sync pool为每个P分配局部缓存,每个P内部数据操作不用加锁,因为一个P同一时刻只会运行一个G。缓存分为private和shared部分。private没有加锁,shared是CAS队列,支持其他P窃取。
对象读取get过程。先从private读取,再从shared读取,如果当前P内部没有,再从其他P的shared队列窃取,用CAS保证并发安全。最后如果没有,且定义了new函数,则创建新对象。
对象写put过程。先放private,再放shared队列。
讲讲go语言常用的包。page 1
fmt包。格式化输入输出,打印日志。
errors包。构造和处理错误信息。
io和ioutil包。读取写入文件、流操作。
net http包。构造http客户端和服务器。
encoding json包。序列化和反序列化。
database sql包。与数据库交互。
sync包。并发原语。
context包。在api调用传递截止时间、取消信号。
讲讲go语言特权协程Goroutine g0。page 1
g0是每个操作系统线程M持有的受保护Goroutine。
运行时调度器。g0执行上下文切换、栈管理。
g0的栈空间不会进行动态栈扩展。
go语言结构体可以比较吗?page 14
结构体可以比较,但前提是所有字段是可比较的,如果结构体有切片,map,函数,就不能比较。
go语言 defer怎么用的(百度)page 160
defer用于延迟函数的执行,用于关闭文件,解锁互斥锁。多个defer按照后进先出顺序执行。defer在函数返回之前执行,但是在return语句执行之后。defer函数参数是在程序走到defer语句时已经确定,而不是在函数返回之前那时候才确定。
go语言 cap和len区别(百度)page 160
cap返回切片容量,len返回切片实际长度。
go语言能往已经closed的Channel写数据吗(百度)page 159
会导致panic。
从已关闭的 Channel 读取数据会发生什么?为什么?(腾讯)page 21
不会panic,不会阻塞。1.如果Channel带缓冲且关闭前存入数据,那么会依次读取缓冲区数据。2.Channel为空,则返回该数据的零值,ok是false。
因为这是支持优雅关闭机制,使得消费者在Channel关闭后退出循环。
go语言往关闭(closed)的Channel写入会发生什么?(go语言往关闭(closed)的有缓冲Channel写入会发生什么?)(go语言往关闭(closed)的无缓冲Channel写入会发生什么?)page15
panic。
go语言往关闭的无缓冲Channel读取会发生什么?page15
不阻塞不panic,返回零值,ok为false。
go语言往关闭的有缓冲Channel读取会发生什么?page15
缓存有则读取,空则返回零值,ok为false。
go语言往满了的有缓冲Channel写入会发生什么?(go语言往空的有缓冲Channel读取会发生什么?)page15
阻塞。
如何确保 Channel 只会被关闭一次?(腾讯)page 1
遵循关闭原则。只让唯一的发送方关闭Channel。
用同步原语。用sync once保证关闭只执行一次。
go语言 :=和=的区别(百度)page 159
:=声明并初始化一个新变量,=将一个已声明的变量赋值一个新值。
go语言有几种锁,乐观锁怎么实现的(百度)page 159
互斥锁mutex,读写锁rwmutex,条件变量cond。
乐观锁实现:读取当前值,计算新值,用cas更新,如atomic CompareAndSwap系列函数。
go语言 Channel有缓冲和无缓冲区别(百度)page 159
无缓冲:同步传递,他没有内部队列,发送和接收必须同时发生,当一个Goroutine向无缓冲Channel发送数据时,如没有其他Goroutine等待接收,发送会阻塞。
有缓冲:内部有队列,只有缓冲区满发送才会阻塞。适用于生产者消费者模型。
go语言值传递和引用传递区别(腾讯云智)page 159
所有参数都是值传递。数组也是值类型,直接传递会复制数组。
可以用指针实现在函数中修改原变量。
切片、map、Channel、interface:虽然也是值传递,但内部封装了指针,修改他们会影响原数据。
go语言子gouroutine的panic会不会被父g捕获(子 goroutine 中的 panic,会被启动它的“父” goroutine 捕获吗?)(子 goroutine 发生 panic,父 goroutine 能用 recover() 接住吗?)(滴滴)page 1
不会。因为panic只能在发生panic的Goroutine内部传播
给你10个goroutine,如何打印出升序数字(滴滴)
用链式Channel。
package main
import (
"fmt"
)
func main() {
// 构造10个 channel,用于传递信号
chs := make([]chan struct{}, 10)
for i := range chs {
chs[i] = make(chan struct{})
}
// 启动10个 goroutine,每个 goroutine 负责打印一个数字
for i := 0; i < 10; i++ {
go func(i int) {
// 除了第一个 goroutine,其它都需要等待前一个信号
if i > 0 {
<-chs[i-1]
}
// 打印数字(这里假设我们打印的是1~10)
fmt.Println(i + 1)
// 通知下一个 goroutine继续执行
if i < 9 {
close(chs[i])
}
}(i)
}
// 等待最后一个 goroutine 执行完(也可以用其它方式等待,比如 sync.WaitGroup)
<-chs[9]
}
go语言 waitgroup原理(滴滴)page 158
waitgroup是同步原语,等待一组Goroutine完成。内部维护计数器,wg add(n)将计数器增加n,启动n个并发任务,每个Goroutine完成后,需调用wg done,将计数器减一。
计数值的低32位是未完成任务数量,高32位是当前等待的Goroutine数量。
go语言 once原理(滴滴)page 158
状态标记:once内部有状态标记done,记录是否执行过目标函数。还维护了互斥锁,保护状态。
双重检查:1.第一次检查:调用方法时,先通过原子操作atomic loadUnit32检查done的值,不为0直接返回。2.获取互斥锁,再检查done的值。如为0,调用目标函数,用defer在目标函数执行完后,将done设为1。
goroutine可能内存泄漏吗?为什么(B站)
可能。
阻塞 :Goroutine等待永远不会受到数据的Channel操作。比如我主Goroutine有个无缓冲Channel,子Goroutine发送数据,如果没有消费者,会一直阻塞,然后主Goroutine 用select超时返回,子Goroutine发生泄漏。
对于go语言 sync.map底层如何实现并发安全?(百度)page 158
只读map:保存在原子变量中,不可变,可多个Goroutine共享。
脏map:有互斥锁。如果一个键在只读map没找到,且map状态是已修改,则获取锁去脏map查找。
miss计数:当只读map没找到去脏map查找次数达到一定阈值,将脏map整体复制到只读map。
go语言map并发访问时panic如何实现的(小米)page 158
状态:map内部有状态,一个Goroutine开始写操作,会设置状态。在每次对map读写,运行时会检查这个状态,检测到并发访问,map调用runtime throw,触发panic。
TODO : https://github.com/xiaobaiTech/golangFamily
golang切片可以作为MAP的KEY吗(腾讯)page 57
切片不能作为map的key。因为map的key必须是可比较的,切片不可比较。
设计考虑。允许比较切片内容要遍历整个数组,性能上可能不理想。
golang底层有没有自动回收对象的机制?(腾讯)page 56
垃圾回收器。golang的gc采用并发标记-清除算法。
三色标记法。
写屏障。
golang二维的map按行和按列哪个快?(腾讯)page 55
按行更快。
减少查找开销。按行读取先遍历外层map,再遍历内层map;按列读取,则要针对每一列遍历外层key,然后再从内层map找到元素。
局部性。按行读取可利用map的数据局部性,减少缓存不命中。
golang Context放map有什么不好的?(腾讯)page 55
Context设计初衷是传递取消信号、截止时间和少量只读的请求级别数据。
并发问题。map本身不是并发安全的,把map放到Context中,在多个Goroutine读写,会引发数据竞争。
C++实现golang类似的协程怎么做?(腾讯)page 55
用三方库实现栈式协程。boost coroutine,手动切换协程。
用c++20内置协程。c++20引入co_wait、co_yield和co_return,可定义协程函数。c++20的协程不保存完整调用栈,调度要自己实现。
如果在匿名函数内panic了,在匿名函数外的defer是否会触发panic-recover?反之在匿名函数外触发panic,是否会触发匿名函数内的panic-recover?(如果 panic 发生在匿名函数内部,外层函数中用 defer 调用 recover 能捕获该 panic 吗?)(如果 panic 发生在外层函数中,匿名函数内部的 defer recover 能捕获该 panic 吗?)(字节)page 55
panic发生在匿名函数内。能捕获panic,panic沿着调用栈往上传递。
panic发生在匿名函数外。不能捕获panic,因为他的函数调用栈与panic无关。
golang切片底层实现?(字节)page 54
切片底层就是一个结构体。含指向底层数组的指针、切片长度和切片容量。
底层数组和切片关系。1.共享底层数组。当对切片进行切片操作时,新切片仍指向原底层数组。2.动态扩容。用append添加元素时,若切片容量不足,go会自动分配,并复制。
defer的底层原理?(字节)page 54
编译器转换。编译器将defer转化为对一个特殊数据结构的调用。
defer栈的实现。1.每遇到defer时,将调用信息如函数指针和参数值压入栈中,函数返回时,按照先进后出调用这些延迟函数。2.defer中函数的参数在被执行时就已经求值。
调度。函数即将退出时,go遍历defer栈,逐个调用记录下来的函数。
空切片和nil切片的区别(百度)page 54
空切片底层指针不是nil,长度和容量为0。
nil切片内部指针为nil,长度和容量为0。
golang map的底层原理?(字节)page 54
数据结构。1.hmap结构体。含count、flag、B字段,flag控制map的状态,B代表桶数量,以及指向桶数组的指针。2.桶结构体。bucket维护tophash数组,存放每个key哈希值高位部分,快速排除不匹配的key。3.溢出链表。一个bucket存满,为该bucket分配溢出bucket,通过链表连接。
查找过程。map用哈希函数对key计算,得到哈希值,根据哈希值获取索引。
扩容渐进rehash。map中数量超过负载因子时触发扩容。扩容过程在后续map操作中将旧bucket数据迁移到新bucket中。
golang sync map底层原理?(字节) page 54
数据结构。1.read读映射。用atomic Value包装的只读map。2.dirty脏映射。对map写入的数据存入脏映射,会加锁。当只读map找不到key时,会去脏map找,找到则复制。
写操作。1.store。如key存在于读映射,直接更新,否则加锁,写脏映射。如果是首次写,将读映射标记为amended,不完整。2.delete。删除将key设为nil。
golang map加锁和sync.map的差别(字节)page 53
sync map更适合大量读操作且key基本稳定,写操作较少或新写入key频率低。
map+rwmutex适合写操作多,频繁新增key。
golang怎么查找性能瓶颈?(golang gc如何调优)(golang 垃圾回收怎么调优)(go语言怎么调优)(字节)page 61
用pprof工具。1.go内置net http pprof和runtime pprof。2.用go tool pprof生成火焰图,看调用栈占用时间较多的部分。
cpu profiling。在main函数开始调用pprof startcpuProfile,程序结束时调用pprof stopcpuProfile。
内存profiling。用pprof lookup(“heap”)采集堆内存。
go语言 GC 算法中怎么实现的可达性分析(得物)page 1
三色标记法。1.白色是要回收的对象集合,黑色不会被回收,灰色是还没扫描的,但可以从gc root访问到。2.三色标记算法的执行流程。初始黑色集合为空,灰色集合是gc root直接引用的对象集合,白色集合是所有其他对象。(1)从灰色集合选一个对象o。(2)将o引用的每个白色对象移动到灰色集合。(3)将o移动到黑色集合。(4)重复上面三个步骤,直到灰色集合为空。这个算法保证了黑色对象不会直接引用白色对象。三色标记法可以不发生stw进行gc。
go语言用写屏障保证并发标记正确性。另外,每个逻辑处理器P有一个gcWork工作缓冲区,存储扫描过程中的灰色对象,提高并发标记效率。
参考:https://en.wikipedia.org/wiki/Tracing_garbage_collection#/media/File:Animation_of_tri-color_garbage_collection.gif
go语言三色标记出来之前垃圾回收gc是怎么去做的(在 Go 语言采用三色标记算法之前,垃圾回收是如何实现的? )(得物)page 7
在三色标记法问世之前,用的是一种传统的停顿-标记-清除的方式。当 GC 被触发时,所有的 goroutine 会被暂停,GC 从所有 GC Roots(全局变量、goroutine 栈以及寄存器中的指针)出发,递归地遍历整个对象图,将所有可达的对象标记为‘活跃’。在这个过程中,由于程序完全暂停,因此不需要考虑并发修改问题,也就不需要写屏障或三色抽象。随后,GC 会遍历整个堆,释放那些未被标记的‘垃圾’对象。
golang中无缓冲channel满了,继续写入会怎么样(文远知行)page 43
写入会阻塞当前Goroutine。
go中如何将string转为切片[]byte,不申请额外空间(如何在 Go 中将 string 无额外内存分配地转换为 []byte字节数组?)(得物)page 71
直接用内置转换会产生新的内存拷贝。如要在O 1空间复杂度实现可借助reflect包和unsafe包。用reflect StringHeader和reflect SliceHeader,将字符串头部结构体数据指针赋值给切片头部结构体。
golang string和[]byte区别?(golang string字符串和byte字节切片区别)(得物)page 41
string字符串是不可变字节序列,内部是一个结构体,有一个指针和一个长度值。默认utf8编码。
字节切片可以修改。内部是一个结构体,有一个指针、长度和容量。
对于函数参数,struct结构体内有引用类型,想在函数内对引用类型做修改,可以修改吗?(结构体内的引用类型(如切片或 map)在函数内部修改会影响原始数据吗?)(B站)page 2
如果是切片不可以修改,因为比如append方法往切片新增一个元素,这时append是返回一个新切片,函数内只是对局部变量赋值,不改变原始结构体。
如果是map可以修改,比如用key修改值,会反映到原始map上。
package main
import "fmt"
type MyStruct struct {
Slice []int
}
// modify1 仅修改切片内的元素
func modify1(s MyStruct) {
// 修改底层数组中的第一个元素,此修改会反映到原始数据上
if len(s.Slice) > 0 {
s.Slice[0] = 100
}
}
// modify2 重新赋值切片字段
func modify2(s MyStruct) {
// append操作可能返回一个新的切片,此时重新赋值只是修改了局部变量,不会影响调用者的结构体
s.Slice = append(s.Slice, 200)
}
func main() {
// 初始化结构体
orig := MyStruct{
Slice: []int{1, 2, 3},
}
// 调用修改切片内数据的函数
modify1(orig)
fmt.Println("调用 modify1 后的结果:", orig.Slice) // 输出结果:[100, 2, 3]
// 调用重新赋值切片的函数
modify2(orig)
fmt.Println("调用 modify2 后的结果:", orig.Slice) // 仍然输出:[100, 2, 3],没有看到 append 的效果
// 如果想让 append 的操作生效,有两种方法:
// 1. 传入指针,这样可以直接修改结构体中的切片字段
modifyWithPtr(&orig)
fmt.Println("通过指针调用修改函数后的结果:", orig.Slice)
}
// 使用指针传递,允许修改结构体字段本身
func modifyWithPtr(s *MyStruct) {
s.Slice = append(s.Slice, 300)
}
make和new的区别 page 40
new用于内存分配,适用任何类型。为指定类型分配零值内存,返回该内存地址的指针。
make专门用于切片、map、Channel的初始化。返回的是引用,不是指针。
golang切片是如何扩容的 page 40
扩容触发。当append发现长度已达到容量上限时,调用growslice函数。在go 1.18前,如原容量小于1024,则翻倍扩容。在go 1.18之后,是256。如果切片大于这个数,以增长因子扩容。
扩容策略。go 1.18之前,如果容量大于1024,则按1.25倍扩容。go 1.18之后,如容量大于256,用公式计算新容量,新容量增量是 (newcap + 3 * threshold) / 4。
golang如何想要按照特定顺序遍历map,怎么做 page 40
提取key。将map中所有key存到切片。
对切片排序。
遍历排序后的切片。
Golang 的错误处理和 Java 的异常处理对⽐ page 40
golang将错误作为返回值检查,每个可能失败的函数调用后会返回一个error类型的值。而java采用异常机制,throw抛出异常,在try catch捕获异常。
介绍⼀下golang的panic和recover page 40
panic和recover是处理异常的内置机制。
panic的作用。1.中断程序执行。当程序遇到错误时如数组越界,调用panic中断,函数停止运行,逐层向上调用defer。2. 用于不可恢复的错误。golang用返回错误值处理预期内的错误。
recover的作用。1.他是内置函数,只能在defer中用,捕获panic。
golang面向对象是怎么实现的?page 39
用结构体和接口实现,go没有继承。
golang用结构体组合实现对象的组合。可以为结构体绑定方法对内部数据操作。用首字母大小写表示私有和公有。
封装。结构体struct表示对象数据属性。方法,在func关键字和方法名之间,用接受者声明为一个类型绑定方法,把操作和数据封装到一起。例如实现String string方法后,fmt包会自动调用。
嵌入字段。在结构体只写类型名不写字段名,匿名字段,既是字段也是名称。被嵌入类型的所有字段和方法都无条件提升到外层结构体,外层类型能直接调用。屏蔽,如外层类型定义了同名方法或字段,会屏蔽,但也可以用链式选择访问被屏蔽的方法。
接口。1.隐式实现,go不用显式声明实现,用方法集自动判定。2.接受者,值接受者的方法集只包含值方法,指针接受者的方法集包含值方法和指针方法。值方法传递的是值的副本,修改后原值不改变,指针方法传递的是地址副本,修改后原值改变。
讲讲golang并发模型 page 39
思想来源于CSP,提倡不用共享内存来通信,而用通信共享内存,用Goroutine和Channel实现。
Goroutine调度基于gmp模型。g表示协程,m表示操作系统线程,p表示调度器上下文环境。
Channel分为有缓冲和无缓冲,是在Goroutine间传递数据的工具。
如何控制Goroutine生命周期? page 39
Context包。可以实现超时控制、取消、传递参数。
Channel。可以定义退出Channel,在要停止Goroutine时,向Channel发送信号,在Goroutine内部,用select监听此Channel。
waitgroup。等待一组Goroutine。
golang Mutex有几种模式?page 39
normal正常模式和starvation饥饿模式。
golang Mutex有⼏种状态?page 39
mutexLocked锁定。 mutexWoken从正常模式被唤醒。 mutexStarving饥饿状态。
Go什么时候发⽣阻塞?阻塞时调度器会怎么做。page 39
分几种情况。
用户态阻塞。如Channel、互斥锁。调度器将G从P的本地队列移除,并放入对应的等待队列,不阻塞M,M可运行其他G。
网络IO阻塞。Go的网络IO是基于Net poller实现,当发起网络请求时,G会挂起,不阻塞M。
阻塞系统调用。Goroutine调用了阻塞OS线程的系统调用,Goroutine进入Gsyscall状态,绑定的M也阻塞。调度器解除M和P的绑定。
Goroutine什么时候发生内存泄漏,如何避免。page 39
在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。 暂时性内存泄露如获取⻓字符串中的⼀段导致⻓字符串未释放、获取⻓slice中的⼀段导致⻓slice未释放。
可以通过复制截取数据避免。
讲讲golang GMP模型原理。page 39
当⼀个 Goroutine 被创建时,它会被放⼊⼀个 P 的本地队列。
当 P 的本地队列空了,或者某个 Goroutine ⻓时间没有被调度执⾏时,P 会尝试从全局队列中获取 Goroutine。如果全局队列也为空,P 会从其他 P 的本地队列中偷取⼀些 Goroutines,以保证尽可能多地利⽤所有的处理器。
M 的数量决定了同时并发执⾏的 Goroutine 数⽬。如果某个 M 阻塞(⽐如在系统调⽤中),它的⼯作会被其他 M 接管。
Go 中的内存逃逸现象是什么?page 39
编译器在逃逸分析过程中发现某个本应分配在栈上的变量,因为其作用域或生命周期超出了当前函数的范围,被迫分配到堆上。
返回局部变量地址:如果函数返回局部变量的指针,编译器会认为该变量的生命周期可能会超过函数调用,从而将它分配到堆上。
当将局部变量通过 channel 或 goroutine 传递给其他部分,⽽这些部分可能在原始函数退出后访问这个变量时,也会导致内存逃逸。
如果在函数内部使⽤ new 或 make 分配的变量,即使返回的是指针,但这个指针可能被外部持有,从⽽导致变量在堆上分配。
如何优化。看编译器的逃逸分析报告,-gcflags=”-m -l”
讲讲微服务的理解(微服务架构和单体架构的区别)(得物,腾讯)page 1
微服务是传统单体应用向更灵活高效的服务化架构的必然趋势。传统单体应用架构如MVC架构,优点是学习曲线低,但随业务团队规模扩大,暴露很多问题。部署效率低,依赖增多,部署时间上升。团队协作成本高,一个小模块的异常影响整个系统部署。上线发布周期长。
随着容器化和devOps实践,服务化思想演进成微服务架构。更细力度的服务拆分,一个子模块成为一个服务。独立打包部署,每个微服务有独立的运行环境。服务治理,用服务治理框架,负责服务注册发现、路由、负载均衡。
如果微服务架构其中一个微服务宕机了,网关的心跳机制有延迟,在这段时间如果有很多请求打过来怎么办?(腾讯)
在下游服务故障时自动断开调用,避免很多请求一直向故障服务发送请求。
熔断是在调用远程服务时,监控失败次数,超过阈值后断开对该服务的调用,直接返回错误,快速失败fail fast。用有限状态机实现,有三个状态,关闭(调用远程服务)、半打开(尝试调用远程服务)、打开(返回错误)。
当调用失败次数累积阈值,熔断从关闭态切换到打开态。如成功调用一次,重置失败次数。当熔断处于打开态时,启动一个超时定时器,超时进入半打开态。当熔断处于半打开态时,请求可以达到 后端服务,累积成功次数后状态切换到关闭态。如出现失败则进入打开态。
怎么保证微服务高可用(得物)page 1
在设计高可用方案时,最重要的是从架构上容忍故障。高可用用SLA衡量,在架构设计,代码实现和部署流程都考虑异常和故障情况,要实现三个九。
容错设计。服务内部和依赖的第三方服务、硬件设备要做容错,用冗余实例、服务降级、超时控制、熔断限流,如Redis这种基础组件,用Redis cluster或用云服务避免单点故障。
限制故障范围。将整个系统拆分成多个相互解耦的模块,设计时区分核心业务和非核心业务,将业务组件与共享基础设施分离。
快速发现快速修复。要有完善的监控和告警,对各个服务、基础设施以及第三方依赖进行监控。
规范变更流程。变更要经过评审、灰度和充分测试,且每次发布要有完善的回滚。
怎么进行微服务拆分(作业帮)page 1
三个火枪手原则。将一个微服务划分给3人。
拆分维度。1.基于业务逻辑,按职责域如商品、订单、用户分服务,但要结合火枪手原则。2.基于可拓展性,将稳定不变功能合并,常变动功能拆细。3.基于可靠性,把核心高可用服务单独拆出,重点保证SLA,非核心或异步类服务隔离,防止宕机蔓延。
你怎么理解云原生的?(腾讯音乐)page 1
云原生是用开源技术栈,在任意云环境如公有云私有云中,以微服务、容器化、动态编排方式构建部署和运行应用的模式,目标是提高资源利用率、可观测性和持续交付。典型实践有遵循twelve Factor原则,用k8s做容器编排、用CICD工具链如Jenkins X实现流水线全自动化,借助gitops、serverless解耦。
go协程的recover怎么写,具体用法(shopee)
recover只有在同一个Goroutine且在defer函数内部才能捕获panic,在Goroutine内部直接写defer recover。