golang面试笔记

底层实现

Go的GMP模型
  • G(Goroutine): 即Go协程,每个go关键字都会创建一个协程。
  • M(Machine):工作线程,在Go中称为Machine,数量对应真实的CPU数(真正干活的对象)。
  • P(Processor): 处理器(Go中定义的一个摡念,非CPU),包含运行Go代码的必要资源,用来调度 G 和 M 之间的关联关系,其数量可通过 GOMAXPROCS() 来设置,默认为核心数。

M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。

Go的垃圾回收机制

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

  • 初始状态下所有对象都是白色的。
  • 从根节点开始遍历所有对象,把遍历到的对象变成灰色对象
  • 遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。
  • 循环步骤3,直到灰色对象全部变黑色。
  • 通过写屏障(write-barrier)检测对象有变化,重复以上操作
  • 收集所有白色对象(垃圾)。

写屏障(Write Barrier):通过写屏障技术尽可能地缩短STW的时间

golang的内存逃逸,什么情况下会发生内存逃逸?

逃逸分析是编译器用来确定由程序创建的值所处位置的过程。具体来说,编译器执行静态代码分析,以确定是否可以将值放在构造函数的栈(帧)上,或者该值是否必须逃逸到堆上。

golang程序中是在编译阶段确定逃逸的,而非运行时,因此我们可以使用

  • 1.通过编译工具查看详细的逃逸分析过程(go build -gcflags '-m -l' main.go)
  • 2.通过反编译命令查看go tool compile -S main.go
怎么避免内存逃逸?
  • 底层分配到堆,还是栈。实际上对你来说是透明的,不需要过度关心
  • 每个 Go 版本的逃逸分析都会有所不同(会改变,会优化)
  • 直接通过go build -gcflags '-m -l' 就可以看到逃逸分析的过程和结果
  • 到处都用指针传递并不一定是最好的,要用对
  • map & slice 初始化时,预估容量,避免由扩展导致的内存分配。但是如果太大(10000)也会逃逸,因为栈的空间是有限的
Go的内存是怎么分配的
Golang安全读写共享变量

Go 中 Goroutine 可以通过 Channel 进行安全读写共享变量

mutex加锁

Go主协程如何等其余协程完再操作?

GC的触发条件都有哪些?

  • 主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。
  • 被动触发,分为两种方式:
    1. 使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。
    2. 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC。

并发问题

协程之间传递变得更加便捷,也可以把控一组协程的退出时机

  1. 从请求上下文中获取用户信息
  2. 从请求上下文中获取请求的唯一标识(traceId,常用于分布式日志追踪)
  3. 控制请求超时时间

WithCancel创建一个支持主动取消的上下文
WithDeadline带截止时间的上下文

WithTimeout带超时时间的上下文

WithValue带值传播的上下文

goroutine 泄漏如何处理
  • Goroutine 内正在进行 channel/mutex 等读写操作,但由于逻辑问题,某些情况下会被一直阻塞。
  • Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
  • Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。

解决:1. runtime.NumGoroutine() 获取当前运行中的 goroutine 数量,进行前后对比业务服务的运行场景中,Goroutine 内导致的泄露,更多的是使用 PProf

2. 多个goroutine抢占用排他锁mutex加锁,解锁的时候 defer mutext.Unlock()

go 锁 (Mutex)
  • 互斥锁 Mutex

当拿不到锁的时候,会阻塞等待,会睡眠,等待锁释放后被唤醒;

互斥锁在设计上主要有两种模式: 正常模式和饥饿模式。

正常模式下,所有阻塞在等待队列中的goroutine会按顺序进行锁获取,当唤醒一个等待队列中的goroutine时,此goroutine并不会直接获取到锁,而是会和新请求锁的goroutine竞争。 通常新请求锁的goroutine更容易获取锁,这是因为新请求锁的goroutine正在占用cpu片执行,大概率可以直接执行到获取到锁的逻辑。

饥饿模式下, 新请求锁的goroutine不会进行锁获取,而是加入到队列尾部阻塞等待获取锁。

饥饿模式的触发条件:

    • 当一个goroutine等待锁的时间超过1ms时,互斥锁会切换到饥饿模式

饥饿模式的取消条件:

    • 当获取到锁的这个goroutine是等待锁队列中的最后一个goroutine,互斥锁会切换到正常模式
    • 当获取到锁的这个goroutine的等待时间在1ms之内,互斥锁会切换到正常模式
  • RWMutext

RLock(): 申请读锁,每次执行此函数后,会对readerCount++,此时当有写操作执行Lock()时会判断readerCount>0,就会阻塞。

RUnLock(): 解除读锁,执行readerCount--,释放信号量唤醒等待写操作的goroutine。

写操作不会被饿死的原因: 写操作到来时,RWMutex.readerCount值拷贝到RWMutex.readerWait中,readerWait值变为0时唤醒写操作

对已经关闭的的 chan 进行读写,会怎么样?为什么?
  • 已经关闭的 chan 会 panic
  • 已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。
    • 如果 chan 关闭前,buffer 内有元素还未读 , 会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。
    • 如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。
对未初始化的的 chan 进行读写,会怎么样?为什么?

读写未初始化的 都会阻塞

Go的defer原理是什么?

特点:后进先出、延时处理

Go 语言中延迟函数 defer 充当着 try...catch 的重任;多个 defer 的执行顺序为“后进先出/先进后出”;

‍defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着 defer 开始执行一些收尾工作;最后 RET 指令携带返回值退出函数。

Channel是同步的还是异步的?

Channel是异步进行的, channel存在3种状态:

  • nil,未初始化的状态,只进行了声明,或者手动赋值为nil
  • active,正常的channel,可读或者可写
  • closed,已关闭,千万不要误认为关闭channel后,channel的值是nil

操作

一个零值nil通道

一个非零值但已关闭的通道

一个非零值且尚未关闭的通道

关闭

产生恐慌

产生恐慌

成功关闭

发送数据

永久阻塞

产生恐慌

阻塞或者成功发送

接收数据

永久阻塞

永不阻塞

阻塞或者成功接收

实践问题

new 和 make 的区别
  • new、make 二者都是用来做初始化(内存分配)。引用类型的变量需要声明且初始化,不然不能赋值
  • make (make(map[string]int, 10)) 只用于slice、map、channel的初始化,返回的还是这三个引用类型
  • new (int) 用于类型的内存分配,内存对应的值为零值(false,0,nil),返回的是指向类型的指针
nil 切片和空切片的区别

切片、函数、指针变量的默认为nil

  • nil切片和空切片指向的地址不一样。nil切片引用数组指针地址为0(无指向任何实际地址)
  • 空切片的引用数组指针地址是有的,且固定为一个值
string和[]byte的转换

s := genString(10000) bs := []byte(s) 这种直接转换会重新分配内存

unsafe;unsafe.Pointer(&s) 复用原来的内存

type StringHeader struct {
	Data uintptr
	Len  int
}
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

两者类型基本一样,Slice多了一个Cap,其实这也决定了[]byte可以直接使用指针强转成string,但是反过来却不行

两个interface{} 能不能比较?

可以,在golang中可比较的类型有int,string,bool,pointer,channel,interface,array 不可比较的类型有slice,map,func

for循环select时,如果通道已经关闭会怎么样?如果select中的case只有一个,又会怎么样?
  • for循环select时,如果其中一个case通道已经关闭,则每次都会执行到这个case。
  • 如果select里边只有一个case,而这个case被关闭了,则会出现死循环。

一个为nil的切片可以追加数据吗?

可以对nil切片进行append操作。在切片容量未知的前提下,建议优先声明为nil切片,而不用担心容量问题。因为它的每次重分配容量都是倍增的

map扩容的时机,满足什么条件时扩容?

hmapbmap;hmap是来表示map的结构体。bmap就是,hmap的buckets指向的就是bmap数组。

当元素数量变多时,会导致碰撞变多,那么bmap里的值就更多,查找效率就会变低,所以到一定程度时就需要扩容。

需要一个指标衡量,就是负载因子。元素数量除以桶的数量,默认为6.5)

    • 正常元素过多造成的扩容:这种情况只需要最简单的扩容,即把B加1,使桶扩容两倍的大小。但Go并没有直接把桶的元素转移,而是采取了类似于redis的渐进式扩容,这也就解释了hmap里oldbucket的作用。

扩容后会先将olbucket指向数组,每次插入修改删除时都会调用growwork()方法,尝试搬迁,搬迁后 序号不变。

map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。

map的iterator是否安全?能不能一边delete一边遍历?

map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。

普通map如何不用锁解决协程安全问题?

go 语言中有一个其他的工具 sync.WaitGroup 计数器。WaitGroup对象内部有个计数器, 最初从0 开始, 他有3个方法 Add() , Done(), Wait()用来控制计数器的数量。 Add(n) 把计数器设置成n, Done() 每次把计数器-1, wait() 会阻塞代码的运行, 直到计数器的值减为0

Go 1.9 以后官方给出了sync.Map 同步map

Go值接收者和指针接收者的区别?
Go中两个Nil可能不相等吗?

可能不相等

slice怎么实现,如何扩容
  • 正常情况就是双倍扩容
  • cap是老数组的容量+新加元素数量,即至少扩容值
  • 如果两倍扩容达不到这个cap,新数组的容量就为这个cap
  • 如果两倍扩容达到了这个最小值,就根据老数组元素数量是否小于1024来决定扩容容量
  • 如果小于1024,就正常扩容两倍。
  • 如果大于等于1024,就循环扩容1.25倍,直到达到或者超过cap
go 打印时 %v %+v %#v 的区别
  • %v 只输出所有的值;
  • %+v 先输出字段名字,再输出该字段的值;
  • %#v 先输出结构体名字值,再输出结构体(字段名字+字段的值);
  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
GoLang学习笔记主要包括以下几个方面: 1. 语法规则:Go语言要求按照语法规则编写代码,例如变量声明、函数定义、控制结构等。如果程序中违反了语法规则,编译器会报错。 2. 注释:Go语言中的注释有两种形式,分别是行注释和块注释。行注释使用`//`开头,块注释使用`/*`开头,`*/`结尾。注释可以提高代码的可读性。 3. 规范代码的使用:包括正确的缩进和空白、注释风格、运算符两边加空格等。同时,Go语言的代码风格推荐使用行注释进行注释整个方法和语句。 4. 常用数据结构:如数组、切片、字符串、映射(map)等。可以使用for range遍历这些数据结构。 5. 循环结构:Go语言支持常见的循环结构,如for循环、while循环等。 6. 函数:Go语言中的函数使用`func`关键字定义,可以有参数和返回值。函数可以提高代码的重用性。 7. 指针:Go语言中的指针是一种特殊的变量,它存储的是另一个变量的内存地址。指针可以实现动态内存分配和引用类型。 8. 并发编程:Go语言提供了goroutine和channel两个并发编程的基本单位,可以方便地实现多线程和高并发程序。 9. 标准库:Go语言提供了丰富的标准库,涵盖了网络编程、文件操作、加密解密等多个领域,可以帮助开发者快速实现各种功能。 10. 错误处理:Go语言中的错误处理使用`defer`和`panic`两个关键字实现,可以有效地处理程序运行过程中出现的错误。 通过以上内容的学习,可以掌握Go语言的基本语法和编程思想,为进一步学习和应用Go语言打下坚实的基础。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Golang学习笔记](https://blog.csdn.net/weixin_52310067/article/details/129467041)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [golang学习笔记](https://blog.csdn.net/qq_44336275/article/details/111143767)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小斌0810

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值