Golang面经齐全整理55问

春招面试季吐血整理,感谢关注收藏,梳理了已有大牛整理的面经+面试go问答实录+本人备试过程的疑问解惑。

1. 相比较于其他语言, Go 有什么优势或者特点?

1.Go 允许跨平台编译,编译出来的是二进制的可执行文件,直接部署在对应系统上即可运行。
2.Go 在语言层次上天生支持高并发,通过 goroutine 和 channel 实现。
channel 的理论依据是 CSP 并发模型, 即所谓的通过通信来共享内存;
3.Go 在 runtime 运行时里实现了属于自己的调度机制:GMP,降低了内核态和用户态的切换成本。
4.Go 的代码风格是强制性的统一,如果没有按照规定来,会编译不通过。

 

2. Golang 里的 GMP 模型?
图解GMP模型及goroutine调度机制,讲的非常好:https://www.cnblogs.com/secondtonone1/p/11803961.html

GMP 模型是 golang 自己的一个调度模型,它抽象出了下面三个结构:
G: 也就是协程 goroutine,由 Go runtime 管理。我们可以认为它是用户级别的线程。
P: processor 处理器。每当有 goroutine 要创建时,会被添加到 P 上的 goroutine本地队列上。
如果 P 的本地队列已满,则会维护到全局队列里。
M: 系统线程。在 M 上有调度函数,它是真正的调度执行者,M 需要跟 P 绑定,
并且会让 P 按下面的原则挑出个 goroutine 来执行:
优先从 P 的本地队列获取 goroutine 来执行;如果本地队列没有,从全局队列获取;
如果全局队列也没有,会从其他的 P 上偷取 goroutine。

 

3. goroutine有什么特点,和线程相比?

1.goroutine 非常的轻量,初始分配只有 2KB,当栈空间不够用时,会自动扩容。
同时,自身存储了执行 stack 信息,用于在调度时能恢复上下文信息。
而线程比较重,一般初始大小有几 MB(不同系统分配不同)。
2.线程是由操作系统调度,是操作系统的调度基本单位。
而 golang 实现了自己的调度机制,goroutine 是它的调度基本单位。

 

4. Go 的垃圾回收机制?

Go 采用的是三色标记法,将内存里的对象分为了三种:
白色对象:未被使用的对象;
灰色对象:当前对象有引用对象,但是还没有对引用对象继续扫描过;
黑色对象,对上面提到的灰色对象的引用对象已经全部扫描过了,下次不用再扫描它了。
当垃圾回收开始时,Go 会把根对象标记为灰色,其他对象标记为白色,然后从根对象遍历搜索,
按照上面的定义去不断的对灰色对象进行扫描标记。当没有灰色对象时,表示所有对象已扫描过,
然后就可以开始清除白色对象了。


5. go 的内存分配是怎么样的?

Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。
内存池主要是预先分配内存,减少向系统申请的频率;
多级对象有:mheap、mcentral、mcache、mspan、arenas。它们以 mspan 作为基本分配单位。
具体的分配逻辑如下:
1.当要分配大于 32K 的对象时,从 mheap 分配。
2.当要分配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 分配;
如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请;
如果 mheap 也没有,则从操作系统申请内存。
3.当要分配的对象小于等于 16B 时,从 mcache 上的微型分配器上分配。

 

6. channel 的内部实现是怎么样的?

channel 内部维护了两个 goroutine 队列,
一个是待发送数据的 goroutine 队列,另一个是待读取数据的 goroutine 队列。
每当对 channel 的读写操作超过了可缓冲的 goroutine 数量,
那么当前的 goroutine 就会被挂到对应的队列上,直到有其他 goroutine 执行了与之相反的
读写操作,将它重新唤起。

 

7. 对已经关闭的 channel 进行读写,会怎么样?

当 channel 被关闭后,如果继续往里面写数据,程序会直接 panic 退出。
如果是读取关闭后的 channel,不会产生 pannic,还可以读到数据。
但关闭后的 channel 没有数据可读取时,将得到零值,即对应类型的默认值。
为了能知道当前 channel 是否被关闭,可以使用下面的写法来判断。

 if v, ok := <-ch; !ok {
  fmt.Println("channel 已关闭,读取不到数据")
 }
还可以使用下面的写法不断的获取 channel 里的数据:

 for data := range ch {
  // get data dosomething
 }
这种用法会在读取完 channel 里的数据后就结束 for 循环,执行后面的代码。

 

8. map 为什么是不安全的?

map 在扩缩容时,需要进行数据迁移,迁移的过程并没有采用锁机制防止并发操作,
而是会对某个标识位标记为 1,表示此时正在迁移数据。
如果有其他 goroutine 对 map 也进行写操作,当它检测到标识位为 1 时,将会直接 panic。
如果我们想要并发安全的 map,则需要使用 sync.map。

 

9. map 的 key 为什么得是可比较类型的?

map 的 key、value 是存在 buckets 数组里的,每个 bucket 又可以容纳 8 个 key 和 8 个 value。
当要插入一个新的 key - value 时,会对 key 进行 hash 运算得到一个 hash 值,
然后根据 hash 值 的低几位(取几位取决于桶的数量,比如一开始桶的数量是 5,则取低 5 位)
来决定命中哪个 bucket。
在命中某个 bucket 后,又会根据 hash 值的高 8 位来决定是 8 个 key 里的哪个位置。
如果不巧,发生了 hash 冲突,即该位置上已经有其他 key 存在了,则会去其他空位置寻找插入。
如果全都满了,则使用 overflow 指针指向一个新的 bucket,重复刚刚的寻找步骤。
从上面的流程可以看出,在判断 hash 冲突,即该位置是否已有其他 key 时,肯定是要进行比较的,
所以 key 必须得是可比较类型的。像 slice、map、function 就不能作为 key。

 

10. mutex 的正常模式、饥饿模式、自旋?

正常模式:
当 mutex 调用 Unlock() 方法释放锁资源时,如果发现有正在阻塞并等待唤起的 Goroutine 队列时,
则会将队头的 Goroutine 唤起。
队头的 goroutine 被唤起后,会采用 CAS 这种乐观锁的方式去修改占有标识位,如果修改成功,
则表示占有锁资源成功了,当前占有成功的 goroutine 就可以继续往下执行了。

饥饿模式:
由于上面的 Goroutine 唤起后并不是直接的占用资源,而是使用 CAS 方法去尝试性占有锁资源。
如果此时有新来的 Goroutine,那么它也会调用 CAS 方法去尝试性的占有资源。
对于 Go 的并发调度机制来讲,会比较偏向于 CPU 占有时间较短的 Goroutine 先运行,即新来的 Goroutine 比较容易占有资源,而队头的 Goroutine 一直占用不到,导致饿死。

针对这种情况,Go 采用了饥饿模式。即通过判断队头 Goroutine 在超过一定时间后还是得不到资源时,
会在 Unlock 释放锁资源时,直接将锁资源交给队头 Goroutine,
并且将当前状态改为饥饿模式,后面如果有新来的 Goroutine 发现是饥饿模式时,则会直接自觉添加到等待队列的队尾。

自旋:
如果 Goroutine 占用锁资源的时间比较短,那么每次释放资源后,都调用信号量来唤起正在阻塞等候的 goroutine,将会很浪费资源。
因此在符合一定条件后,mutex 会让等候的 Goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终才加入到等待队列里。

补充:CAS定义

CAS(Compare and Swap)乐观锁是一种并发控制机制,常用于多线程环境下对共享数据的并发访问和修改操作。CAS乐观锁通过比较内存中的值与预期值的方式来确定共享数据是否被其他线程修改过,从而实现对共享数据的安全更新。

CAS乐观锁的基本思想是:在更新共享数据之前,先比较当前内存中的值与预期值是否相等,如果相等,则认为没有其他线程修改过数据,可以进行更新操作;如果不相等,则说明有其他线程修改过数据,当前线程需要重新读取最新的数据并重新尝试更新,直到成功为止。

CAS乐观锁通常使用原子操作实现。原子操作是不可中断的操作,可以保证在多线程环境下的原子性。在CAS操作中,通过比较预期值与当前值是否相等,如果相等则将新值写入内存,否则不做任何操作。CAS操作是一个原子的操作,不会被其他线程中断,因此可以保证并发的安全性。

CAS乐观锁相对于悲观锁(如互斥锁)来说,不需要使用互斥量或者临界区来保护共享数据,因此减少了线程切换的开销,提高了并发性能。但是CAS乐观锁也存在一些问题,例如ABA问题,即当共享数据的值从A变成B再变回A时,CAS操作无法感知到中间的变化,导致可能出现错误的更新。为了解决这个问题,通常可以使用版本号或者标记位等方式进行扩展,以确保CAS操作的正确性。

 

11. Go 的逃逸行为是指?

在传统的编程语言里,会根据程序员指定的方式来决定变量内存分配是在栈还是堆上,比如声明的变量是值类型,则会分配到栈上,或者 new 一个对象则会分配到堆上。

在 Go 里变量的内存分配方式则是由编译器来决定的。如果变量在作用域(比如函数范围)之外,还会被引用的话,那么称之为发生了逃逸行为,此时将会把对象放到堆上,即使声明为值类型;
如果没有发生逃逸行为的话,则会被分配到栈上,即使 new 了一个对象。

 

12. context 使用场景及注意事项

Go 里的 context 有 cancelCtx 、timerCtx、valueCtx。它们分别是用来通知取消、通知超时、存储 key - value 值。context 的 注意事项如下:

context 的 Done() 方法往往需要配合 select {} 使用,以监听退出。
尽量通过函数参数来暴露 context,不要在自定义结构体里包含它。
WithValue 类型的 context 应该尽量存储一些全局的 data,而不要存储一些可有可无的局部 data。
context 是并发安全的。
一旦 context 执行取消动作,所有派生的 context 都会触发取消。

 

13. context 是如何一层一层通知子 context?

当 ctx, cancel := context.WithCancel(父Context)时,会将当前的 ctx 挂到父 context 下,然后开个 goroutine 协程去监控父 context 的 channel 事件,
一旦有 channel 通知,则自身也会触发自己的 channel 去通知它的子 context, 关键代码如下

go func() {
   select {
   case <-parent.Done():
    child.cancel(false, parent.Err())
   case <-child.Done():
   }
 }()

 

14. waitgroup 原理

waitgroup 内部维护了一个计数器,当调用 wg.Add(1) 方法时,就会增加对应的数量;当调用 wg.Done() 时,计数器就会减一。
直到计数器的数量减到 0 时,就会调用 runtime_Semrelease 唤起之前因为 wg.Wait() 而阻塞住的 goroutine。

 

15. sync.Once 原理

内部维护了一个标识位,当它 == 0 时表示还没执行过函数,此时会加锁修改标识位,然后执行对应函数。后续再执行时发现标识位 != 0,则不会再执行后续动作了。关键代码如下:

type Once struct {
 done uint32
 m    Mutex
}

func (o *Once) Do(f func()) {
    // 原子加载标识值,判断是否已被执行过
 if atomic.LoadUint32(&o.done) == 0 {
  o.doSlow(f)
 }
}

func (o *Once) doSlow(f func()) { // 还没执行过函数
 o.m.Lock()
 defer o.m.Unlock()
 if o.done == 0 { // 再次判断下是否已被执行过函数
  defer atomic.StoreUint32(&o.done, 1) // 原子操作:修改标识值
  f() // 执行函数
 }
}

 

16. 定时器原理

一开始,timer 会被分配到一个全局的 timersBucket 时间桶。每当有 timer 被创建出来时,就会被分配到对应的时间桶里了。

为了不让所有的 timer 都集中到一个时间桶里,Go 会创建 64 个这样的时间桶,然后根据 当前 timer 所在的 Goroutine 的 P 的 id 去哈希到某个桶上:

// assignBucket 将创建好的 timer 关联到某个桶上
func (t *timer) assignBucket() *timersBucket {
 id := uint8(getg().m.p.ptr().id) % timersLen
 t.tb = &timers[id].timersBucket
 return t.tb
}
接着 timersBucket 时间桶将会对这些 timer 进行一个最小堆的维护,每次会挑选出时间最快要达到的 timer。如果挑选出来的 timer 时间还没到,那就会进行 sleep 休眠;
如果 timer 的时间到了,则执行 timer 上的函数,并且往 timer 的 channel 字段发送数据,以此来通知 timer 所在的 goroutine。

 

17. Slice 相关注意点

Slice 的扩容机制
如果 Slice 要扩容的容量大于 2 倍当前的容量,则直接按想要扩容的容量来 new 一个新的 Slice,否则继续判断当前的长度 len。
如果 len 小于 1024,则直接按 2 倍容量来扩容,否则一直循环新增 1/4,直到大于想要扩容的容量。主要代码如下:

newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
    newcap = cap
} else {
    if old.len < 1024 {
        newcap = doublecap
    } else {
        for newcap < cap {
            newcap += newcap / 4
        }
    }
}
除此之外,还会根据 slice 的类型做一些内存对齐的调整,以确定最终要扩容的容量大小。

Slice 的一些注意写法
// =========== 第一种

a := make([]string, 5)
fmt.Println(len(a), cap(a))   //  输出5   5

a = append(a, "aaa")
fmt.Println(len(a), cap(a))   // 输出6  10


// 总结: 由于make([]string, 5) 则默认会初始化5个 空的"", 因此后面 append 时,则需要2倍了


// =========== 第二种
a:=[]string{}
fmt.Println(len(a), cap(a))   //  输出0   0

a = append(a, "aaa")
fmt.Println(len(a), cap(a))   // 输出1  1

// 总结:由于[]string{}, 没有其他元素, 所以append 按 需要扩容的 cap 来

// =========== 第三种
a := make([]string, 0, 5)
fmt.Println(len(a), cap(a))   //  输出0   5

a = append(a, "aaa")
fmt.Println(len(a), cap(a))   // 输出1  5

// 总结:注意和第一种的区别,这里不会默认初始化5个,所以后面的append容量是够的,不用扩容

// =========== 第四种
b := make([]int, 1, 3)
a := []int{1, 2, 3}
copy(b, a)

fmt.Println(len(b))  // 输出1

// 总结:copy 取决于较短 slice 的 len, 一旦最小的len结束了,也就不再复制了

range slice

以下代码的执行是不会一直循环下去的,原因在于 range 的时候会 copy 
这个 slice 上的 len 属性到一个新的变量上,然后根据这个 copy 值去遍历 slice,
因此遍历期间即使 slice 添加了元素,也不会改变这个变量的值了。

v := []int{1, 2, 3}
for i := range v {
 v = append(v, i)
}
另外,range 一个 slice 的时候是进行一个值拷贝的,如果 slice 里存储的是指针集合,那在 遍历里修改是有效的,
如果 slice 存储的是值类型的集合,那么就是在 copy 它们的副本,期间的修改也只是在修改这个副本,跟原来的 slice 里的元素是没有关系的。

slice 入参注意点(详细见问题51)

如果 slice 作为函数的入参,通常希望对 slice 的操作可以影响到底层数据,
但是如果在函数内部 append 数据超过了 cap,导致重新分配底层数组,
这时修改的 slice 将不再是原来入参的那个 slice 了(虽然silce底层确实有个指向它的指针,
但是在函数内部扩容后capcity变量会改变,而函数外部原slice的capcity还是原值。
因此对后续扩容出来的容量部分的操作,原slice是感知不到的。)
因此通常不建议在函数内部对 slice 有 append 操作,若有需要则显示的 return 这个 slice。

 

18. make 和 new 的区别

new 是返回某个类型的指针,将会申请某个类型的内存。
而 make 只能用于 slice, map, channel 这种 golang 内部的数据结构,它们可以只声明不初始化,
或者初始化时指定一些特定的参数,比如 slice 的长度、容量;map 的长度;channel 的缓冲数量等。

 

19. defer、panic、recover 三者的用法

defer制定了函数退出前执行的操作、panic是遇到异常程序崩溃退出、recover捕获并处理异常
defer 函数调用的顺序是后进先出,当产生 panic 的时候,会先执行 panic 前面的 defer 函数后
才真的抛出异常。一般的,recover 会在 defer 函数里执行并捕获异常,防止程序崩溃。

package main

import "fmt"

func main() {
    defer func(){
       fmt.Println("b")
    }()

    defer func() {
       if err := recover(); err != nil {
            fmt.Println("捕获异常:", err)
        }
    }()

    panic("a")
}

// 输出
// 捕获异常: b
// a

 

20. slice 和 array 的区别

array 是固定长度的数组,并且是值类型的,也就是说是拷贝复制的.
 slice 是一个引用类型,指向了一个动态数组的指针,会进行动态扩容。

 

21.go的内存管理机制?

Go语言采用了一种称为"垃圾回收"(Garbage Collection,简称GC)的内存管理机制。Go的内存管理机制主要由以下几个组件组成:

自动内存分配:在Go中,不需要显式地分配和释放内存。当创建一个新的对象或变量时,Go的运行时系统会自动为其分配所需的内存空间。

垃圾回收器:Go语言的垃圾回收器负责自动检测和回收不再使用的内存。它会周期性地扫描程序的堆(Heap),找出不再被引用的对象,并将其释放回内存池,
使之可被再次使用。

引用计数:Go语言的垃圾回收器并不使用传统的引用计数技术。相反,它使用了一种称为"标记-清除"(Mark and Sweep)的算法。
该算法通过从根对象开始,递归地遍历对象图,并标记所有可达的对象,然后清除未被标记的对象。即“三色标记法”。

并发垃圾回收:为了减少垃圾回收对程序执行的影响,Go语言的垃圾回收器采用了并发机制。它可以在程序运行的同时进行垃圾回收操作,
与程序的执行并行进行,以减少垃圾回收对程序性能的影响。

总的来说,Go语言的内存管理机制通过自动内存分配和垃圾回收器来管理程序的内存。开发者无需手动管理内存,
而是依靠垃圾回收器自动回收不再使用的内存,使得开发更加方便和安全。
同时,Go语言的并发垃圾回收机制保证了垃圾回收的高效性和对程序执行性能的最小干扰。

 

22.go的内存分配机制?

内存池+多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;
多级对象有:mspan、arenas、mcache < mcentral < mheap。它们以 mspan 作为基本分配单位。具体的分配逻辑如下:
当要分配>=32K 的对象时,从 mheap 分配。
当要分配的对象<=32K >16B 时,从 P(处理器) 上的 mcache 分配。
如果 mcache 没有内存,则从 mcentral 获取--->如果 mcentral 也没有,则向 mheap 申请---->如果 mheap 也没有,则从操作系统申请内存。
当要分配的对象<= 16B 时,从 mcache 上的微型分配器上分配。

 

23.channel的底层结构,原理?
这里引用查阅的两篇讲的很好的解释文章

【Go基础篇】彻底搞懂 Channel 实现原理 - 程序员祝融的文章 - 
https://zhuanlan.zhihu.com/p/599607814

CSP模型:Go语言的CSP模型 - 路由器没有路的文章 - 
https://zhuanlan.zhihu.com/p/313763247


 

24.怎样实现goroutine的高并发?

一.从本身结构和性质看
1)相对线程,协程的优势就在于它非常轻量级,进行上下文切换的代价非常的小。
2)对于一个goroutine ,每个结构体G中有一个sched的属性就是用来保存它上下文的。这样,goroutine 就可以很轻易的来回切换。
3)由于其上下文切换在用户态下发生,根本不必进入内核态,所以速度很快。而且只有当前goroutine 的 PC, SP等少量信息需要保存。
4)在Go语言中,每一个并发的执行单元为一个goroutine。

二.从并发模型看
goroutine并发, 采用的式CSP(communicating sequential processes)并发模型,
讲究的是以通讯的方式来进行数据共享,是通过goroutine配合channel的方式来实现的,性能高开销小。

三.从调度模型看
goroutine实现了MPG模型:
S(Sched):结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。
M(Machine):一个M直接关联了一个内核线程。(优先从 P 的本地队列获取 goroutine 来执行;如果本地队列没有,从全局队列获取,如果全局队列也没有,会从其他的 P 上偷取 goroutine。)
P(processor):代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。
G(Goroutine):其实本质上也是一种轻量级的线程。
介绍:
一个M会关联两个东西,一个是内核线程,一个是可执行的进程。
一个上下文P会有两类Goroutine,一类是正在运行的,图中的蓝色G;一类是正在排队的,图中灰色G,这个会存储在该进程中的runqueue里面。
这里的上下文P的数量也表示的是Goroutinue运行的数量,一般设置为几个,机器中就会并发运行几个。
当然这里P的数量是可以设置的,通过环境变量GOMAXPROCS的值,或者通过运行时调用函数runtime.GOMAXPROCS()进行设置,最大值是256。

补充:256?不是单核处理器就一个P吗?这里的P指的处理器和平时说的单核多核处理器是一个个概念吗?

P 不是指单核或多核处理器的物理概念,而是指调度器的逻辑执行单元。
默认情况下,它与系统的物理处理器核心数相等,但可以通过设置来更改。
最大值可以设置为 256,但实际上,这个值一般不需要设置得太大,
因为过多的 P 数量可能导致调度器开销增加,反而影响性能。

 

25.如何限制10个goroutine按次序执行?
查阅文章链接:http://t.csdnimg.cn/WUw4d

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    ch := make(chan int)

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            // 接收前一个 Goroutine 发送的信号
            prev := <-ch

            // 执行当前 Goroutine 的任务
            fmt.Printf("Goroutine %d: Executing task\n", id)

            // 发送信号给下一个 Goroutine
            next := (id + 1) % 10
            ch <- next

            // 等待下一个 Goroutine 完成任务
            if id != 9 {
                <-ch
            }
        }(i)
    }

    // 启动第一个 Goroutine
    ch <- 0

    // 等待所有 Goroutine 完成
    wg.Wait()
}

 

26.go的channel和协程?
根据上面说过的点结合答就可以

1.讲channel的结构和功能
2.讲协程的并发和有序,及GMP调度模型
3.两者配合实现csp模型,高效并发

 

27.go是怎么进行协程切换的?

根据go实现的GMP模型,M(内核线程)绑定某个P(处理器),P管理调度一批goroutine。
除了正在执行的goroutine,还有一个属于P的本地goroutine队列(LRQ)存放等待执行的协程。
当前协程执行完后会从LRQ中继续取协程执行,同时LRQ也会从调度器维护的全局协程队列
(未分配给P的协程)取协程到LRQ中等待执行。
当然,如果某个M下执行协程太慢了,会暂时将M休眠,让M'绑定P继续工作,
并把M那里的执行的协程拿过来。同理M'太慢了也会唤醒M来帮忙;M1如果结束了而M2还在工作的话,
M1会从M2的队列中拿一半来执行;

 

28.无阻塞channel和有阻塞channel有什么区别?

无阻塞channel又称有缓冲channel,在读的时候,只要channel里有东西就能读,
除非channel空了才阻塞;写也类似。
阻塞channel又称无缓冲channel,在读的时候,如果没有人要往channel中写数据,
就会一直等待直到有人写;写也类似。即收发双方必须同时准备就绪才能进行。

 

29.gin的前缀树了解过吗?

Gin框架本身是基于HTTP路由的,它使用路由树(Radix Tree)作为内部的数据结构来高效地匹配和处理请求。路由树是一种高效的数据结构,用于管理和匹配URL路径。
它使用路径片段作为节点,从根节点到叶节点的路径表示一个完整的URL路径。路由树可以有效地处理动态和静态路由,并且在路由匹配时具有较低的时间复杂度。

Gin框架的路由树是基于httprouter库实现的,该库使用Radix Tree来管理路由。此树的实现方式类似于前缀树(用于快速匹配和搜索字符串,节点存字符),
但具有一些差异。它将请求路径的每个片段作为树的节点,并使用参数和通配符来处理动态路径。这样,Gin框架可以高效地匹配和处理各种类型的URL路径。

总结起来,Gin框架并没有直接使用前缀树作为其内部实现之一。相反,它使用了一种名为路由树(Radix Tree)的数据结构来管理和匹配URL路径,以提供高效的路由处理能力

 

30.了解过IO复用机制吗?
查阅文章链接:https://zhuanlan.zhihu.com/p/115220699

 

31.进程、线程 和 协程 的区别?

进程、线程和协程是计算机中常见的并发编程概念,它们在多任务处理和并发执行方面有不同的特点和用途。

进程(Process):
进程是操作系统中资源分配和调度的基本单位。
进程拥有独立的内存空间、上下文和系统资源,如文件描述符、句柄等。
进程之间相互独立,通信需要通过进程间通信(IPC)机制。
进程切换开销较大,包括上下文切换、内存切换等,因此进程间切换代价较高。

线程(Thread):
线程是操作系统中调度的基本单位,是进程中的执行单元。
线程与所属进程共享资源,如内存空间、打开的文件等。
线程通过共享内存进行通信,访问共享数据时需要考虑同步和互斥。
线程切换开销较进程小,因为线程共享进程的上下文,包括寄存器、内存等。

协程(Coroutine):
协程是一种轻量级的线程,也被称为用户态线程或纤程。
协程由程序员控制调度,而不是由操作系统内核调度。
协程可以在一个或多个线程上执行,一个线程可以同时运行多个协程。
协程之间可以通过共享数据进行通信,也可以使用消息传递等方式。
协程切换开销很小,因为它们在用户态进行切换,不涉及内核态的上下文切换。

简而言之,进程是资源分配的基本单位,线程是执行的基本单位,而协程是调度的基本单位。
进程之间相互独立,线程共享进程资源,而协程由程序员控制调度,可以在一个或多个线程上执行,并且切换开销较小。

 

32.Go 为什么字段和方法分开写?优势是什么?知道函数接收器吗?

在 Go 语言中,字段(Fields)和方法(Methods)通常是分开定义的。字段是结构体(Struct)或类型的属性,而方法是与结构体或类型相关联的函数。

将字段和方法分开写的设计选择有以下几个优势:

可读性和可维护性:将字段和方法分开定义可以使代码更加清晰和易于理解。字段定义位于结构体的顶部,使得可以快速查看和了解结构体的组成。
方法定义则可以独立地放在结构体下方,使代码更具有结构性和可读性。这种分离还有助于更好地组织和维护代码,使得修改和扩展更加方便。

封装性和可见性:将字段和方法分开定义可以更好地控制访问权限。在结构体中,字段可以设置为私有(小写字母开头),
只能在同一个包内访问,而方法可以设置为公共(大写字母开头),允许在其他包中调用。这样可以提供更好的封装性,隐藏实现细节,只暴露必要的接口,增加代码的安全性和稳定性。

可扩展性和灵活性:将方法与结构体分开定义,使得可以独立地对结构体进行方法的扩展和修改,而无需修改结构体的定义。
这样,即使有多个方法与同一个结构体相关联,也可以分别对它们进行独立的修改和扩展,而不影响其他方法。这种分离还允许在不改变结构体定义的情况下,为现有类型添加新的方法,
Go 语言中的方法接收器(Receiver),它用于将函数与特定类型关联起来,让该函数成为该类型的方法。
Go 语言中的方法接收器可以是任何类型,包括自定义类型(结构体、接口等)和内置类型(如整数、字符串等),并且可以是指针类型或非指针类型。

方法接收器有两种类型:值接收器(Value Receiver)和指针接收器(Pointer Receiver)。

值接收器:使用值接收器时,方法接收器是该类型的一个副本。在方法内部对接收器的修改不会影响原始值。值接收器适用于不需要修改原始值的场景。
type MyStruct struct {
    data int
}

// 值接收器方法
func (s MyStruct) MyMethod() {
    // 通过 s 访问 MyStruct 的字段和方法
    // 对 s 的修改不会影响原始值
}
指针接收器:使用指针接收器时,方法接收器是指向该类型的指针。在方法内部对接收器的修改会影响原始值。指针接收器适用于需要修改原始值的场景。
go
复制
type MyStruct struct {
    data int
}

// 指针接收器方法
func (s *MyStruct) MyMethod() {
    // 通过 s 访问 MyStruct 的字段和方法
    // 对 s 的修改会影响原始值
}
通过使用方法接收器,可以将函数与类型进行绑定,使其成为类型的方法。这样,我们可以通过类型的实例调用该方法,以便对实例进行操作或执行特定的行为。
方法接收器为 Go 语言提供了一种简洁而可扩展的方式来定义和组织与类型相关联的行为

 

33.了解 Go 的错误类型吗?具体讲讲

当涉及到错误处理时,Go语言引入了一个内置的error接口类型和一些惯用的错误处理模式。下面是Go语言中错误处理的一些重要概念和机制:

error接口类型:
error是Go语言的内置接口类型,它定义了一个表示错误的值的约定。
error接口只有一个方法,即Error() string,用于返回错误的描述信息。

内置错误类型:
Go语言提供了一个内置的错误类型errors.New,用于创建基本的错误值。
该函数接受一个字符串参数,并返回一个满足error接口的错误值。
例如,可以使用err := errors.New("something went wrong")创建一个描述错误的值。

自定义错误类型:
在Go语言中,可以通过实现error接口来创建自定义的错误类型。
自定义错误类型可以包含任意的字段和方法,以满足特定的错误处理需求。
例如,可以创建一个名为CustomError的结构体类型,并为其实现Error() string方法。

错误处理:
在Go语言中,通常使用以下模式进行错误处理:
result, err := SomeFunction()
if err != nil {
    // 处理错误
} else {
    // 处理结果
}
当函数返回一个error类型的值时,通常将其作为多返回值的最后一个值。
通过检查错误值是否为nil,可以确定函数调用是否成功。
如果发生错误,可以根据具体情况进行错误处理,例如打印错误、返回错误或尝试恢复错误。

错误链:
在处理错误时,可以使用fmt.Errorf、errors.Wrap和errors.Wrapf等函数来创建错误链。
错误链可以用于在一个错误上附加更多的上下文信息,以便更好地理解错误的来源。
错误链可以通过errors.Unwrap或errors.Is函数来遍历和匹配。
总而言之,Go语言中的错误处理采用了简单而明确的模式。通过使用error接口类型和惯用的错误处理模式,开发者可以更好地管理和处理函数调用中可能发生的错误。

 

34.recover 可以捕获子 goroutine 的 panic 吗?不能的话,该如何解决呢?

在 Go 语言中,recover 函数可以捕获同一个 Goroutine 内部的 panic,但无法捕获来自其他 Goroutine 的 panic。
当 panic 发生在一个 Goroutine 中时,它只会影响该 Goroutine 自身,而不会传播到其他 Goroutine。

如果您希望在一个 Goroutine 中捕获另一个 Goroutine 的 panic,可以使用通道(Channel)来进行通信和错误处理。以下是一个示例代码:
package main

import (
    "fmt"
)

func main() {
    ch := make(chan error, 1)

    go func() {
        defer func() {
            if r := recover(); r != nil {
                ch <- fmt.Errorf("panic occurred: %v", r)
            }
        }()

        // 在这里执行可能会发生 panic 的代码
        // ...
    }()

    err := <-ch
    if err != nil {
        // 处理捕获到的 panic
        fmt.Println(err)
    }
}
在上面的代码中,我们创建了一个通道 ch,用于接收可能发生的 panic。在另一个 Goroutine 中,我们使用 defer 和 recover 来捕获 panic,并将错误信息发送到通道 ch。
主 Goroutine 通过从通道 ch 接收错误信息,并进行相应的处理。

通过使用通道进行错误传递,我们可以在一个 Goroutine 中捕获另一个 Goroutine 的 panic,并进行适当的错误处理。

 

35.基本类型中哪些是值类型,哪些是指针类型?

查阅的文章链接http://t.csdnimg.cn/gxGqp

指针类型包括:

值类型:都有对应的指针类型,形式为 *数据类型
            比如 int 对应的指针类型是 * int
            值类型包括,基本数据类型,数组array,结构体 struct
引用类型: 指针,slice切片,map,管道chan ,interface

在Go语言中,有以下几种数据类型:

基本数据类型:
布尔类型(bool)
数值类型:
整数类型int8、int16、int32、int64、int
无符号整数类型uint8、uint16、uint32、uint64、uint
浮点数类型float32、float64
复数类型complex64、complex128
字符串类型(string)
字符类型(rune)
错误类型(error)

复合数据类型:
数组类型(array)(值)
结构体类型(struct)(值)
切片类型(slice)
字典类型(map)
接口类型(interface)
指针类型(pointer)
函数类型(function)

通道类型(channel)
别名类型(alias):通过type关键字可以定义的自定义类型,可以是任何其他类型的别名。

 

36.如何理解“不要通过共享内存来通信,而应该通过通信来共享内存”?

以下是本人个人理解,由于作者个人水平限制,理解可能存在偏差,可查阅更详细深入的解释。

个人理解:

首先明确一下“通信”和“共享内存”的概念,golang强调前者,以往大部分其他语言强调后者。
一定的资源,线程有序地请求访问它们就是通信,为每个线程虚拟出一个独占资源的假象就叫资源共享。
以往其他编程,调度主要还是通过共享内存实现,线程共享访问内存,从而交换信息,一般是控制信息,如A释放了锁,B抢占了锁等,没有数据交换,那么只要需要共享数据的保护和安全,就必然涉及线程的同步、死锁、上下文切换的开销等,十分麻烦。
而对于golang,直接把需要的内存数据包装到对象传到channel中,让需要的内存数据在channel中传递起来,且golang保证了goroutine的并发,
就使得内存数据通过channel的有序传递,在不同的goroutine之间传递和共享,这一块内存数据就真正的让所有请求都能够获取到,拿到,并且是以一种不争不抢有序的优雅状态,提高了性能也减少了同步带来的各种开销。这就实现了通过通信来传递内存数据,即通过通信来共享内存。而不是使用共享内存来通信。

往大了说,前者属于请求去调度资源,而后者是资源去调度请求。

 

37.channel 长度为 0 的情况,具体讲讲?

即无缓冲channel,即阻塞channel,收发双方必须同时就绪才能进行,且数据直接传递没有缓冲区暂存。

 

38.channel 内部结构是什么?

channel内部实现的是一个叫做hchan的结构体,其中维护了一个环形序列暂存待发送和接收的数据,记录管道的相关信息如剩余数据数量、是否已关闭、元素类型元素大小的字段
、以及接收发送数据的指针在缓冲区中的位置,同时还维护了发送等待和接受等待队列,用于在channel满/空时暂存发送数据/接受数据的goroutine,俗称生产者/消费者。
并且该结构体具有一个互斥锁保护以上字段。

 

39.了解 Go 的单元测试吗?
查阅文章链接:https://cloud.tencent.com/developer/article/2230689

Go 语言的单元测试默认采用官方自带的测试框架,通过引入 testing 包以及 执行 go test 命令来实现单元测试功能。
在源代码包目录内,所有以 _test.go 为后缀名的源文件会被 go test 认定为单元测试的文件,
这些单元测试的文件不会包含在 go build 的源代码构建中,而是单独通过 go test 来编译并执行。

 

40.讲讲哈希表处理冲突的方法, map 底层数据结构?
引用查阅的文章,Go map在哈希冲突过多时,处理方法:开放地址法、拉链法

https://juejin.cn/post/7060128992870793246?from=search-suggest

map底层结构引用查阅的文章:http://:https://juejin.cn/post/7060128992870793246?from=search-suggest

map常用底层结构有哈希查找表/搜索树(AVL树,红黑树),go选择的是哈希查找表。
hmap(哈希查找表)是Go map的底层实现,每个hmap内都含有多个bmap(哈希桶)(buckets桶、oldbuckets旧桶、overflow溢出桶),既每个哈希表都由多个桶组成。

buckets:是一个指针,指向一个bmap数组,存储多个桶。
oldbuckets:是一个指针,指向一个bmap数组,存储多个旧桶,用于扩容。
overflow:是一个指针,指向一个元素个数为2的数组,数组的类型是一个指针,指向一个slice,slice的元素是桶(bmap)的地址,这些桶都是溢出桶。
为什么有两个?因为Go map在哈希冲突过多时,会发生扩容操作。[0]表示当前使用的溢出桶集合,[1]是在发生扩容时,保存了旧的溢出桶集合。overflow存在的意义在于防止溢出桶被gc。

bmap哈希桶:是一个隶属于hmap的结构体,一个桶(bmap)可以存储8个键值对。有一个字段是存放8个key每个key高八位的数组,还有key数组和value数组,长度也是8
如果有第9个键值对被分配到该桶,那就需要再创建一个桶,通过overflow指针将两个桶连接起来。在hmap中,多个bmap桶通过overflow指针相连,组成一个链表。


41.Go map 的扩容机制

引用查阅的文章
http://t.csdnimg.cn/9WTEt

 

42.Go 的 GC 讲一下

Go语言采用了一种称为"垃圾回收"(Garbage Collection,简称GC)的内存管理机制,详细见问题2。

 

43.讲一下 defer

defer 语句由 defer 关键字后跟随的函数或方法调用组成,它会在包裹该 defer 语句的函数执行结束前被调用。
当函数执行到 defer 语句时,它不会立即执行,而是将该调用推迟到当前函数返回之前。

以下是一些 defer 的常见用法和特点:

延迟函数调用:通过使用 defer,可以将函数调用推迟到包裹该 defer 语句的函数执行结束前。这在需要在函数退出之前执行一些清理操作时非常有用。

延迟参数求值:defer 语句中的函数参数会在 defer 语句执行时被求值,而不是在函数返回时。这意味着 defer 中的函数参数可能会在其他语句执行之前被计算并保存。

延迟顺序:如果一个函数中有多个 defer 语句,它们会按照后进先出(LIFO)的顺序执行。也就是说,最后一个 defer 语句会最先执行,而第一个 defer 语句会最后执行。

错误处理:defer 语句经常与错误处理一起使用。通过将错误处理代码放在 defer 中,可以确保在函数退出之前,相关的清理操作都能得到执行,即使发生了错误。

资源释放:defer 语句常用于释放打开的文件、关闭数据库连接、解锁互斥锁等资源的操作。这样可以确保资源在函数退出前得到释放,避免资源泄漏。

注意事项:

defer 适用于那些在函数执行结束时需要执行的代码,但不适用于无限循环或长时间运行的函数。
defer 语句中的函数参数会在 defer 语句执行时进行求值,因此应该注意参数的值是否符合预期。

 

44.什么情况下会引发 panic?

例如在你访问超出 slice 边界的元素时,这种行为是未定义的,因此 Go 会在运行时 panic。
其他一些会引发 panic 的就是通过值为 nil 的指针访问结构体的字段、关闭已经关闭的 channel 等。
适当的选择panic或代码里显示处理err,在使用多个err的id导致可读性很差的时候,选择直接panic。

 

45.了解内存泄漏吗?在 Go 中,那些情况会引发内存泄漏?

内存泄漏是指你向系统申请分配内存进行使用(new/malloc),然后系统在堆内存中给这个对象申请一块内存空间.
但当我们使用完了却没有归系统(delete),导致这个不使用的对象一直占据内存单元,造成系统将不能再把它分配给需要的程序。
一次内存泄漏的危害可以忽略不计,但是内存泄漏堆积则后果很严重,无论多少内存,迟早会被占完,造成内存泄漏。

引起内存泄漏的原因?

1、分配给程序的内存忘记回收:这个是不应该发生的事情,但也是代码中常见的问题。分配的内存用完之后,就一定要回收,避免造成内存泄漏。
2、程序代码有问题,造成系统没有办法回收;

    Temp1 = new BYTE[100];
    Temp2 = new BYTE[100];
    Temp2 = Temp1;

这样,Temp2的内存地址就丢掉了,而且永远都找不回了,这个时候Temp2的内存空间想回收都 没有办法。

3、某些API函数操作不正确,造成内存泄漏;

内存泄漏危害?

1、频繁GC:系统分配给每个应用的内存资源都是有限的,内存泄漏导致其他组件可用的内存变少后,一方面会使得GC的频率加剧,再发生GC的时候,所有进程都必须等待。
GC的频率越高,用户越容易感应到卡顿。另一方面内存变少,可能使得系统额外分配给该对象一些内存,而影响整个系统的运行情况。
2、导致程序运行崩溃:一旦内存不足以为某些对象分配所需要的空间,将会导致程序崩溃,造成体验差。

Go中可能引起内存泄漏的情况?

7种情景:Golang内存泄漏的7种场景 - 上山打老虎的文章 - 知乎 https://zhuanlan.zhihu.com/p/469817707
包括:数组使用不当(大数组做形参,内存激增)、goroutine泄露、互斥锁未释放、死锁、channel空、只出不进、只进不出、定时器忘记stop()。

在开发中你知道哪些工具可以来排查内存泄漏吗?

内存泄漏检测工具:
使用专门的内存泄漏检测工具是最常见的做法。这些工具能够在程序运行时追踪内存分配,并在内存泄漏发生时发出警告。常见的内存泄漏检测工具有Valgrind、AddressSanitizer、Dr. Memory等。这些工具的使用方法因平台和编程语言而异,你需要查阅相关文档以了解如何配置和使用它们。
日志记录:
在代码中添加日志记录是一种简单而有效的内存泄漏检测方法。你可以在内存分配和释放的地方添加日志语句,以记录内存操作的信息。当程序运行时,观察日志输出可以帮助你发现哪些内存没有被正确释放。这种方法对于小型程序或原型开发特别有用。
静态代码分析:
静态代码分析是一种检查源代码以查找潜在问题的自动化工具。通过静态代码分析,你可以发现一些明显的内存泄漏模式,例如未初始化的指针、悬挂指针等。许多集成开发环境(IDE)和代码编辑器提供了内置的静态代码分析功能,你可以使用它们来提高代码质量。
动态分析:
动态分析是在程序运行时检查内存泄漏的方法。你可以使用一些编程技巧来追踪内存分配,例如在分配和释放内存的操作前后打印计数器的值。通过比较这些计数器的值,你可以确定哪些内存没有被正确释放。这种方法需要更多的工作,但对于一些复杂的问题可能非常有用。
性能分析工具:
性能分析工具可以帮助你了解程序在运行时的资源使用情况。通过分析这些工具的输出,你可以发现哪些部分的代码导致了内存泄漏。常见的性能分析工具包括gprof、perf、Massif等。


46.select和epoll的区别

查阅文章链接,深入理解select、poll和epoll及区别:http://t.csdnimg.cn/O2FLS 

(1)select==>时间复杂度O(n)

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,
此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,
而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。  
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。

select的使用?

select 是 Go 语言中用于处理多个通道操作的控制结构。它可以同时等待多个通道的操作,并执行第一个准备就绪的操作。

select 语句的基本语法如下:
select {
case <-channel1:
    // channel1 有数据可读时执行的逻辑
case data := <-channel2:
    // 从 channel2 读取数据时执行的逻辑
case channel3 <- data:
    // 向 channel3 写入数据时执行的逻辑
default:
    // 没有任何通道操作准备就绪时执行的逻辑
}
select 语句包含多个 case 子句,每个 case 子句描述了一个通道操作。在 select 语句中,程序会按顺序检查每个 case 子句,找到第一个准备就绪的通道操作,然后执行相应的逻辑。
如果有多个通道都准备就绪,那么选择其中一个进行操作,选择的方式是随机的。

select 语句可以包含以下几种类型的 case 子句:

<-channel: 从通道读取数据。当通道中有数据可读时,相应的逻辑将被执行。

data := <-channel: 从通道读取数据并将其赋值给变量 data。当通道中有数据可读时,相应的逻辑将被执行。

channel <- data: 将数据写入通道。当通道有足够的空间来接收数据时,相应的逻辑将被执行。

default: 当没有任何通道操作准备就绪时,会执行 default 子句中的逻辑。default 子句是可选的。

对于 select 语句,它实际上是一个阻塞操作。当 select 语句执行时,如果没有任何通道操作准备就绪,
程序将会阻塞在 select 语句处,直到至少有一个通道操作准备就绪或有 default 子句。

select 语句非常适用于处理并发场景,可以在多个通道之间进行选择和协调操作。通过 select,可以实现非阻塞的通道操作,并以高效和简洁的方式处理并发任务。

 

47.如何理解“协程之间虽然解耦,但是它们和Channel有着耦合”?

 Go 语言中,协程(Goroutine)是一种轻量级的并发执行单位,可以并发执行多个协程,而不需要像线程那样显式地进行线程管理。
协程之间相对于线程来说是解耦的,它们可以独立运行,不需要关心其他协程的状态或执行顺序。

然而,尽管协程之间相对解耦,它们与通道(Channel)之间存在一种耦合关系。通道是 Go 语言中用于协程之间通信和同步的主要机制。
通过通道,协程可以发送和接收数据,实现协程之间的数据传递和共享。

这种耦合的关系可以解释为以下几个方面:

数据传递:协程之间可以通过通道进行数据传递。一个协程可以将数据发送到通道,而另一个协程可以从通道接收到这些数据。这种方式实现了协程之间的数据共享和协作。

同步机制:通道还可以用作协程之间的同步机制。通过在通道上进行发送和接收操作,可以实现协程之间的同步点。
当一个协程向通道发送数据时,它会阻塞直到另一个协程准备好接收数据。这样可以确保协程之间的执行顺序和同步性。

错误处理:通道还可以用于传递错误信息。一个协程可以将错误信息发送到通道,而另一个协程可以从通道接收到这些错误信息,并进行相应的错误处理。这种方式可以有效地传递和处理协程中发生的错误。

通过通道,协程之间可以实现数据传递、同步和错误处理等功能,从而实现协程之间的耦合。通道提供了一种可靠的机制,使得协程能够有效地进行通信和协作,
而不需要显式地共享状态或进行复杂的同步操作。这种协程与通道之间的耦合关系是 Go 语言并发模型的核心特性之一,也是实现并发编程的重要手段。

 

48.CAS?

CAS 是 Compare and Swap(比较并交换)的缩写,是一种乐观锁技术。它是一种并发控制的机制,用于实现多线程环境下的原子操作。
CAS 操作涉及三个操作数:内存地址 V、旧的预期值 A 和新的值 B。CAS 操作会比较内存地址 V 上的值是否等于预期值 A,如果相等,则将新值 B 写入该内存地址;
如果不相等,则表示其他线程已经修改了该值,CAS 操作失败,不进行写入。
CAS 操作是原子的,意味着它在执行期间不会被中断。通过使用 CAS,可以避免使用传统的互斥锁机制,提高并发性能。

 

49.slice底层结构?

在 Go 语言中,切片(slice)是对底层数组的一个动态长度的可索引视图。它包含以下三个主要字段:

指针(Pointer):切片指向底层数组的起始位置的指针。通过指针,可以访问和修改底层数组中的元素。

长度(Length):切片当前包含的元素数量。长度是切片可索引的范围,通过索引可以访问长度范围内的元素。

容量(Capacity):切片从起始位置到底层数组末尾的最大可访问范围。容量表示切片可以扩展的最大限度,超过容量的索引无效。

这些字段组成了切片的底层结构。在内存中,切片本身是一个小的数据结构,而底层数组通常是在堆上分配的一段连续内存。

通过这种底层结构,切片提供了对底层数组的灵活访问和修改。可以通过索引访问切片中的元素,并使用内置函数(如 append、copy 等)对切片进行动态操作。

需要注意的是,
切片是引用类型,当将切片传递给函数或赋值给其他变量时,实际上是将切片的副本传递或复制给目标对象,它们都引用相同的底层数组。这使得切片在函数之间传递和共享数据变得高效和方便。

 

50.goroutine泄露有哪些场景?

Goroutine 泄漏的场景包括:

忘记调用 WaitGroup 的 Wait() 方法或关闭 Channel,导致无法等待或阻塞 Goroutine。
存在无限循环或递归调用,没有适当的退出条件。
阻塞的 IO 操作没有使用超时机制或正确处理错误。
未正确使用 Goroutine 的生命周期管理,例如没有正确启动和关闭 Goroutine。
存在 Goroutine 之间的循环引用,导致无法回收。
为避免 Goroutine 泄漏,应正确管理和终止 Goroutine 的生命周期,使用适当的同步机制,处理阻塞操作和错误,避免循环引用,并使用调试和性能工具进行排查。

 

51.为什么slcie入参后改变容量外面感知不到?

在 Go 语言中,切片(slice)是对底层数组的一个引用,它包含了指向底层数组的指针、长度和容量信息。当我们将一个切片作为函数的参数传递时,实际上是将切片的副本传递给函数。
当在函数内部对切片进行操作时,操作的是切片的副本,但这个副本仍然引用原始切片所指向的底层数组。因此,对底层数组的修改会在函数内外都可见,可以影响到原始切片的内容。
然而,当使用 append 函数向切片追加元素时,如果超过了切片的容量(即当前长度等于容量),
Go 语言会为切片分配一个新的底层数组,并将原有的元素复制到新的底层数组中。此时,修改切片的副本将不再影响原始切片,因为它们引用的不再是同一个底层数组。
这样做的目的是为了避免在添加新元素时频繁地重新分配和复制底层数组,以提高性能。

 

52.make,new申请的空间在堆还是栈?

make 函数主要用于创建引用类型的对象,例如切片、映射和通道。
make 函数会在堆上分配内存,并返回指向该内存的引用。
这是因为引用类型的大小在编译时无法确定,需要在运行时动态分配。因此,使用 make 创建的对象在运行时会被分配到堆上。
new 函数主要用于创建值类型的对象,例如基本类型和结构体。
new 函数会在堆上分配内存,并返回指向该内存的指针。值类型的大小在编译时是已知的,但为了保持一致性,new 函数也会将值类型的对象分配到堆上。
无论是使用 make 还是 new,它们都在堆上分配内存。在 Go 语言中,堆是用于存储动态分配的内存,而栈主要用于存储函数调用时的局部变量和函数调用的上下文信息。

 

53.内存分配机制提到的内存池预先分配多大?

在 Go 的内存池和多级分配机制中,内存池主要是用于预先分配一定数量的小块内存,
并在需要时分配给程序使用。内存池的大小是根据应用程序的需求和系统资源来确定的,并没有固定的大小。

 

54.defer和panic顺序?

当发生 panic 时,会先执行当前函数中的所有延迟语句(按照后进先出的顺序),然后才会触发真正的 panic。

 

55.为什么GMP,降低了内核态和用户态的切换成本?

1.用户态线程,切换在用户级由go自行管理,无需操作系统内核调度。
2.轻量,协程销毁和切换的开销较小。
3.GMP非阻塞的调度机制,goroutine阻塞时M会主动释放并由其他线程处理该goroutine。减少阻塞造成的开销。

最后由于整理结合了已有大佬面经和本人自己的查阅梳理,如果对于部分问题解答存在疑问或发现错误,欢迎各位同学和大佬私信或评论区指正!

也欢迎大家在评论区分享补充自己遇到的golang相关问题,共同进步!

 

  • 15
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Golang八股文是指在面试或考试中常被到的一些基础知识和常见题。下面是一份简单的整理: 1. Golang的特点:静态类型、垃圾回收、并发模型、内存安全、编译型语言等。 2. Golang中的基本数据类型:整型、浮点型、布尔型、字符串、数组、切片、映射、结构体等。 3. Golang中的控制流程:条件语句(if-else)、循环语句(for、range)、选择语句(switch)、跳转语句(break、continue)等。 4. Golang中的函数:函数定义、函数调用、参数传递(值传递和引用传递)、多返回值、匿名函数、闭包等。 5. Golang中的并发编程:goroutine的创建与调度、通道(channel)的使用、并发安全、锁机制(互斥锁、读写锁)等。 6. Golang中的错误处理:错误类型(error)、错误处理机制(defer、panic、recover)、错误码设计等。 7. Golang中的面向接口编程:接口的定义与实现、接口的多态性、空接口(interface{})、类型断言等。 8. Golang中的包管理:go mod的使用、依赖管理、版本管理等。 9. Golang中的测试与性能优化:单元测试(testing包)、性能剖析(pprof包)、内存分析、代码优化等。 10. Golang中的常用标准库:fmt、os、io、net、http、json等。 以上是一些常见的Golang八股文内容,希望对你有所帮助。当然,实际应用中还有很多其他方面的知识和技巧需要掌握。如果你有具体的题,欢迎继续提

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值