写点什么

2025Go 面试八股(含 100 道答案)

作者:王中阳Go
  • 2025-06-05
    湖南
  • 本文字数:28910 字

    阅读完需:约 95 分钟

2025Go面试八股(含100道答案)

最近很多朋友都在找 Go 开发相关的岗位,本文总结了 100 道 Go 常见的面试题(含答案),内容如下:

01 = 和 := 的区别?

=是赋值变量,:=是定义变量。

02 指针的作用

一个指针可以指向任意变量的地址,它所指向的地址在 32 位或 64 位机器上分别固定占 4 或 8 个字节。指针的作用有:


  • 获取变量的值


 import fmt  func main(){  a := 1  p := &a//取址&  fmt.Printf("%d\n", *p);//取值* }
复制代码


  • 改变变量的值


// 交换函数func swap(a, b *int) {        *a, *b = *b, *a}
复制代码


  • 用指针替代值传入函数,比如类的接收器就是这样的。


type A struct{}
func (a *A) fun(){}
复制代码

03 Go 允许多个返回值吗?

可以。通常函数除了一般返回值还会返回一个 error。

04 Go 有异常类型吗?

有。Go 用 error 类型代替 try...catch 语句,这样可以节省资源。同时增加代码可读性:


 _, err := funcDemo()if err != nil {    fmt.Println(err)        return}
复制代码


也可以用 errors.New()来定义自己的异常。errors.Error()会返回异常的字符串表示。只要实现 error 接口就可以定义自己的异常,


 type errorString struct {  s string }  func (e *errorString) Error() string {  return e.s }  // 多一个函数当作构造函数 func New(text string) error {  return &errorString{text} }package main
import ( "errors" "fmt")
func divide(a, b int) (int, error) { if b == 0 { //通过errors.New 可类似实现throw new Exception捕获异常 return 0, errors.New("division by zero") } return a / b, nil}
func main() { result, err := divide(10, 2) if err != nil { fmt.Println("Error:", err) } else { fmt.Println("Result:", result) }
result, err = divide(10, 0) if err != nil { fmt.Println("Error:", err) } else { fmt.Println("Result:", result) }}
复制代码

05 什么是协程(Goroutine)

协程是用户态轻量级线程,它是线程调度的基本单位。通常在函数前加上 go 关键字就能实现并发。一个 Goroutine 会以一个很小的栈启动 2KB 或 4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个 goroutine 同时启动。

06 ❤ 如何高效地拼接字符串

拼接字符串的方式有:+ fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join


  1. +


使用+操作符进行拼接时,会对字符串进 行遍历,计算并开辟一个新的空间来存储原来的两个字符串。


  1. fmt.Sprintf


由于采用了接口参数,必须要用反射获取值,因此有性能损耗。


3 strings.Builder


用 WriteString()进行拼接,内部实现是指针+切片,同时 String()返回拼接后的字符串,它是直接把[]byte 转换为 string,从而避免变量拷贝。


4 bytes.Buffer


bytes.Buffer 是一个一个缓冲 byte 类型的缓冲器,这个缓冲器里存放着都是 byte,


bytes.buffer 底层也是一个[]byte 切片。


5 strings.join


strings.join 也是基于 strings.builder 来实现的,并且可以自定义分隔符,在 join 方法内调用了 b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的 n 的长度就是我们要拼接的 slice 的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。


性能比较


strings.Join` ≈ `strings.Builder` > `bytes.Buffer` > `+` > `fmt.Sprintf
复制代码


5 种拼接方法的实例代码:


func main(){        a := []string{"a", "b", "c"}        //方式1:+        ret := a[0] + a[1] + a[2]        //方式2:fmt.Sprintf        ret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2])        //方式3:strings.Builder        var sb strings.Builder        sb.WriteString(a[0])        sb.WriteString(a[1])        sb.WriteString(a[2])        ret := sb.String()        //方式4:bytes.Buffer        buf := new(bytes.Buffer)        buf.Write(a[0])        buf.Write(a[1])        buf.Write(a[2])        ret := buf.String()        //方式5:strings.Join        ret := strings.Join(a,"")}
复制代码


参考资料:字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔

答疑

在 Go 语言中,strings.Builder的性能通常比bytes.Buffer好,主要有以下几个原因:


  1. 零拷贝:strings.Builder在内部使用了可变长度的[]byte切片来存储字符串,而bytes.Buffer使用了固定长度的[]byte切片。当进行字符串拼接时,strings.Builder可以直接修改切片中的内容,而不需要进行额外的内存分配和拷贝操作,从而避免了不必要的性能开销。

  2. 预分配内存:strings.Builder在初始化时会预分配一定大小的内存空间,避免了频繁的内存分配和释放操作。这样可以减少内存分配的次数,提高性能。

  3. 字符串连接优化:strings.Builder提供了WriteString方法,可以直接将字符串追加到内部的[]byte切片中,而不需要进行类型转换和拷贝操作。这样可以减少不必要的中间步骤,提高字符串连接的效率。


需要注意的是,strings.Builderbytes.Buffer都是用于字符串拼接和缓冲的类型,选择使用哪个取决于具体的需求和场景。如果需要频繁进行字符串拼接操作,尤其是在循环中,strings.Builder通常会更高效。而如果只是简单的缓冲操作,bytes.Buffer也可以满足需求。


总结来说,strings.Builder的性能比bytes.Buffer好,主要是因为它采用了零拷贝、预分配内存和字符串连接优化等技术,避免了不必要的内存分配和拷贝操作,提高了字符串拼接的效率。

07 什么是 rune 类型

ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的 128 个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是 ASCII 的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为 Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。


Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。


sample := "我爱GO"runeSamp := []rune(sample)runeSamp[0] = '你'fmt.Println(string(runeSamp))  // "你爱GO"fmt.Println(len(runeSamp))  // 4
复制代码

08 如何判断 map 中是否包含某个 key ?

var sampleMap map[int]intif _, ok := sampleMap[10]; ok {        ...} else {        ...}// sampleMap[10] 返回vlaue 和 bool
复制代码

09 Go 支持默认参数或可选参数吗?

不支持。但是可以利用结构体参数,或者...传入参数切片数组。


// 传入结构体参数struct Options {        concurrent bool}func pread(offset int64, len int64, o *Options) {        ...}// 这个函数可以传入任意数量的整型参数func sumN(nums ...int) int {    total := 0    for _, num := range nums {        total += num    }    return total}
复制代码

10 defer 的执行顺序

defer 执行顺序和调用顺序相反,类似于栈后进先出(LIFO)。


defer 在 return 之前(*修改 @放)执行,但在函数退出之前,defer 可以修改返回值。下面是一个例子:


func test() int {        i := 0        defer func() {                fmt.Println("defer1")        }()        defer func() {                i += 1                fmt.Println("defer2")        }()        return i}
func main() { fmt.Println("return", test())}// defer2// defer1// return 0
复制代码


上面这个例子中,test 返回值并没有修改,这是由于 Go 的返回机制决定的,执行 Return 语句后,Go 会创建一个临时变量保存返回值。如果是有名返回(也就是指明返回值func test() (i int)


func test() (i int) {        i = 0        defer func() {                i += 1                fmt.Println("defer2")        }()        return i}
func main() { fmt.Println("return", test())}// defer2// return 1
复制代码


这个例子中,返回值被修改了。对于有名返回值的函数,执行 return 语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。

11 如何交换 2 个变量的值?

对于变量而言 a,b = b,a; 对于指针而言*a,*b = *b, *a

12 Go 语言 tag 的用处?

tag 可以为结构体成员提供属性。常见的:


  1. json 序列化或反序列化时字段的名称

  2. db: sqlx 模块中对应的数据库字段名

  3. form: gin 框架中对应的前端的数据字段名

  4. binding: 搭配 form 使用, 默认如果没查找到结构体中的某个字段则不报错值为空, binding 为 required 代表没找到返回错误给前端

13 如何获取一个结构体的所有 tag?

利用反射:


import reflecttype Author struct {        Name         int      `json:Name`        Publications []string `json:Publication,omitempty`}
func main() { // 获取结构体类型信息 t := reflect.TypeOf(Author{}) for i := 0; i < t.NumField(); i++ { name := t.Field(i).Name s, _ := t.FieldByName(name) fmt.Println(name, s.Tag) }}
复制代码


上述例子中,reflect.TypeOf 方法获取对象的类型,之后 NumField()获取结构体成员的数量。 通过 Field(i)获取第 i 个成员的名字。 再通过其 Tag 方法获得标签。

14 如何判断 2 个字符串切片(slice) 是相等的?

reflect.DeepEqual(), 但反射非常影响性能。

15 结构体打印时,%v 和 %+v 的区别

%v 输出结构体各成员


%+v 输出结构体各成员名称


%#v 输出结构体名称和结构体各成员的名称和值;

16 Go 语言中如何表示枚举值(enums)?

在常量中用 iota 可以表示枚举。iota 从 0 开始。


const (        B = 1 << (10 * iota)        KiB         MiB        GiB        TiB        PiB        EiB)
复制代码

17 空 struct{} 的用途

  • 用 map 模拟一个 set,那么就要把值置为 struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。


type Set map[string]struct{}
func main() { set := make(Set)
for _, item := range []string{"A", "A", "B", "C"} { set[item] = struct{}{} } fmt.Println(len(set)) // 3 if _, ok := set["A"]; ok { fmt.Println("A exists") // A exists }}
复制代码


  • 有时候给通道发送一个空结构体,channel<-struct{}{},也是节省了空间。


func main() {        ch := make(chan struct{}, 1)        go func() {                <-ch                // do something        }()        ch <- struct{}{}        // ...}
复制代码


  • 仅有方法的结构体


type Lamp struct{}
复制代码

18 go 里面的 int 和 int32 是同一个概念吗?

不是一个概念。go 语言中的 int 的大小是和操作系统位数相关的,如果是 32 位操作系统,int 类型的大小就是 4 字节。如果是 64 位操作系统,int 类型的大小就是 8 个字节。除此之外 uint 也与操作系统有关,占字节与 int 类型情况一致。而 int 后有数字的话,占用空间大小是固定的:


int8 占 1 个字节,int16 占 2 个字节,int32 占 4 个字节,int64 占 8 个字节。

19 init() 函数是什么时候执行的?

简答: 在 main 函数之前执行。


详细:init()函数是 go 初始化的一部分,由 runtime 初始化每个导入的包,初始化不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。


每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init()函数。同一个包,甚至是同一个源文件可以有多个 init()函数。init()函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init()函数的执行顺序不作保证。


执行顺序:import –> const –> var –>init()–>main()


一个文件可以有多个 init()函数!

20 ❤如何知道一个对象是分配在栈上还是堆上?

Go 和 C++不同,Go 的逃逸分析是在编译器完成的;go 局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。那么如何判断是否发生了逃逸呢?


go build -gcflags '-m -m -l' xxx.go.


关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。如果变量内存占用较大时,优先放在堆上;如果函数外部没有引用,优先放在栈中;如果变量在函数外部存在引用;必定在堆中。

21 2 个 interface 可以比较吗 ?

Go 语言中,interface 的内部实现包含了 2 个字段,类型 T 和 值 V,interface 可以使用 == 或 != 比较。2 个 interface 相等有以下 2 种情况


  1. 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)

  2. 类型 T 相同,且对应的值 V 相等。


看下面的例子:


type Stu struct{ Name string }type StuInt interface{}
func main() { var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"} var stu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"} fmt.Println(stu1 == stu2) // false fmt.Println(stu3 == stu4) // true}
复制代码


stu1 和 stu2 对应的类型是 *Stu,值是 Stu 结构体的地址,两个地址不同,因此结果为 false。 stu3 和 stu4 对应的类型是 Stu,值是 Stu 结构体,且各字段相等,因此结果为 true。

22 2 个 nil 可能不相等吗?

可能不等。interface 在运行时绑定值,只有值为 nil 接口值才为 nil,但是与指针的 nil 不相等。举个例子:


var p *int = nil var i interface{} = nil if(p == i){ fmt.Println("Equal") }


两者并不相同。总结:两个 nil 只有在类型相同时才相等

23 ❤简述 Go 语言 GC(垃圾回收)的工作原理

垃圾回收机制是 Go 一大特(nan)色(dian)。Go1.3 采用标记清除法, Go1.5 采用三色标记法,Go1.8 采用三色标记法+混合写屏障


标记清除法


分为两个阶段:标记和清除


标记阶段:从根对象出发寻找并标记所有存活的对象。


清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。


缺点是需要暂停程序 STW(stop the world)。


三色标记法


将对象标记为白色,灰色或黑色。


白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。


标记开始时,先将所有对象加入白色集合(需要 STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。


这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此 Go 采用了写屏障技术,当对象新增或者更新会将其着色为灰色。


一次完整的 GC 分为四个阶段:


  1. 准备标记(需要 STW),开启写屏障。

  2. 开始标记

  3. 标记结束(STW),关闭写屏障

  4. /清理(并发)


基于插入写屏障和删除写屏障在结束时需要 STW 来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:


  1. GC 开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需 STW);

  2. GC 期间,任何栈上创建的新对象均为黑色

  3. 被删除引用的对象标记为灰色

  4. 被添加引用的对象标记为灰色


总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得 GC 时间从 2s 降低到 2us。

24 函数返回局部变量的指针是否安全?

这一点和 C++不同,在 Go 里面返回局部变量的指针是安全的。因为 Go 会进行逃逸分析,如果发现局部变量的作用域超过该函数则会把指针分配到堆区,避免内存泄漏。

25 非接口的任意类型 T() 都能够调用 *T 的方法吗?反过来呢?

一个 T 类型的值可以调用*T 类型声明的方法,当且仅当 T 是可寻址的


反之:*T 可以调用 T()的方法,因为指针可以解引用。

26 go slice 是怎么扩容的?

1.7 版本:如果当前容量小于 1024,则判断所需容量是否大于原来容量 2 倍,如果大于,当前容量加上所需容量;否则当前容量乘 2。


如果当前容量大于 1024,则每次按照 1.25 倍速度递增容量,也就是每次加上 cap/4。


1.8 版本:Go1.18 不再以 1024 为临界点,而是设定了一个值为 256 的threshold,以 256 为临界点;超过 256,不再是每次扩容 1/4,而是每次增加(旧容量+3*256)/4;


  1. 当新切片需要的容量 cap 大于两倍扩容的容量,则直接按照新切片需要的容量扩容;

  2. 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;

  3. 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。

27 ❤无缓冲的 channel 和有缓冲的 channel 的区别?

(这个问题笔者也纠结了很久,直到看到一篇文章,阻塞与否是分别针对发送接收方而言的,才茅塞顿开)


对于无缓冲区 channel:


发送的数据如果没有被接收方接收,那么**发送方阻塞;**如果一直接收不到发送方的数据,接收方阻塞


有缓冲的 channel:


发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。


可以类比生产者与消费者问题。


编辑切换为居中

28 为什么有协程泄露(Goroutine Leak)?

协程泄漏是指协程创建之后没有得到释放。主要原因有:


  1. 缺少接收器,导致发送阻塞

  2. 缺少发送器,导致接收阻塞

  3. 死锁。多个协程由于竞争资源导致死锁。

  4. 创建协程的没有回收。

29 Go 可以限制运行时操作系统线程的数量吗? 常见的 goroutine 操作函数有哪些?

可以,使用runtime.GOMAXPROCS(num int)可以设置线程数目。该值默认为 CPU 逻辑核数,如果设的太大,会引起频繁的线程切换,降低性能。


runtime.Gosched(),用于让出 CPU 时间片,让出当前 goroutine 的执行权限,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。 runtime.Goexit(),调用此函数会立即使当前的 goroutine 的运行终止(终止协程),而其它的 goroutine 并不会受此影响。runtime.Goexit 在终止当前 goroutine 前会先执行此 goroutine 的还未执行的 defer 语句。请注意千万别在主函数调用 runtime.Goexit,因为会引发 panic。

30 如何控制协程数目。

The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.


从官方文档的解释可以看到,GOMAXPROCS 限制的是同时执行用户态 Go 代码的操作系统线程的数量,但是对于被系统调用阻塞的线程数量是没有限制的。GOMAXPROCS 的默认值等于 CPU 的逻辑核数,同一时间,一个核只能绑定一个线程,然后运行被调度的协程。因此对于 CPU 密集型的任务,若该值过大,例如设置为 CPU 逻辑核数的 2 倍,会增加线程切换的开销,降低性能。对于 I/O 密集型应用,适当地调大该值,可以提高 I/O 吞吐率。


另外对于协程,可以用带缓冲区的 channel 来控制,下面的例子是协程数为 1024 的例子


var wg sync.WaitGroupch := make(chan struct{}, 1024)for i:=0; i<20000; i++{        wg.Add(1)        ch<-struct{}{}        go func(){                defer wg.Done()                <-ch        }}wg.Wait()
复制代码


此外还可以用协程池:其原理无外乎是将上述代码中通道和协程函数解耦,并封装成单独的结构体。常见第三方协程池库,比如tunny等。

31 ❤new 和 make 的区别?

  • new 只用于分配内存,返回一个指向地址的指针。它为每个新类型分配一片内存,初始化为 0 且返回类型*T 的内存地址,它相当于 &T{}

  • make 只可用于 slice,map,channel 的初始化,返回的是引用

32 请你讲一下 Go 面向对象是如何实现的?

Go 实现面向对象的两个关键是 struct 和 interface。


封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。


继承:继承是编译时特征,在 struct 内加入所需要继承的类即可:


type A struct{}type B struct{        A}
复制代码


多态:多态是运行时特征,Go 多态通过 interface 来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。


Go 支持多重继承,就是在类型中嵌入所有必要的父类型。

33 uint 型变量值分别为 1,2,它们相减的结果是多少?

var a uint = 1var b uint = 2fmt.Println(a - b)
复制代码


答案,结果会溢出,如果是 32 位系统,结果是 2^32-1,如果是 64 位系统,结果 2^64-1.

34 讲一下 go 有没有函数在 main 之前执行?怎么用?

go 的 init 函数在 main 函数之前执行,使用方法:


func init() {        ...}
复制代码


init 函数非常特殊:


  • 初始化不能采用初始化表达式初始化的变量;

  • 程序运行前执行注册

  • 实现 sync.Once 功能

  • 不能被其它函数调用

  • init 函数没有入口参数和返回值:

  • 每个包可以有多个 init 函数,每个源文件也可以有多个 init 函数

  • 同一个包的 init 执行顺序,golang 没有明确定义,编程时要注意程序不要依赖这个执行顺序。

  • 不同包的 init 函数按照包导入的依赖关系决定执行顺序。

35 下面这句代码是什么作用,为什么要定义一个空值?

type GobCodec struct{        conn io.ReadWriteCloser        buf *bufio.Writer        dec *gob.Decoder        enc *gob.Encoder}
type Codec interface { io.Closer ReadHeader(*Header) error ReadBody(interface{}) error Write(*Header, interface{}) error}
var _ Codec = (*GobCodec)(nil)
复制代码


答:将 nil 转换为 GobCodec 类型,然后再转换为 Codec 接口,如果转换失败,说明 GobCodec 没有实现 Codec 接口的所有方法。

36 ❤golang 的内存管理的原理清楚吗?简述 go 内存管理机制。

golang 内存管理基本是参考 tcmalloc 来进行的。go 内存管理本质上是一个内存池,只不过内部做了很多优化:自动伸缩内存池大小,合理的切割内存块。


一些基本概念:


页 Page:一块 8K 大小的内存空间。Go 向操作系统申请和释放内存都是以页为单位的。


span : 内存块,一个或多个连续的 page 组成一个 span 。如果把 page 比喻成工人, span 可看成是小队,工人被分成若干个队伍,不同的队伍干不同的活。


sizeclass : 空间规格,每个 span 都带有一个 sizeclass ,标记着该 span 中的 page 应该如何使用。使用上面的比喻,就是 sizeclass 标志着 span 是一个什么样的队伍。


object : 对象,用来存储一个变量数据内存空间,一个 span 在初始化时,会被切割成一堆等大的 object 。假设 object 的大小是 16B , span 大小是 8K ,那么就会把 span 中的 page 就会被初始化 8K / 16B = 512 个 object 。所谓内存分配,就是分配一个 object 出去。


mheap


一开始 go 从操作系统索取一大块内存作为内存池,并放在一个叫 mheap 的内存池进行管理,mheap 将一整块内存切割为不同的区域,并将一部分内存切割为合适的大小。


编辑切换为


mheap.spans :用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个 page,已使用了多大等等。


mheap.bitmap 存储着各个 span 中对象的标记信息,比如对象是否可回收等等。


mheap.arena_start : 将要分配给应用程序使用的空间。


mcentral


用途相同的 span 会以链表的形式组织在一起存放在 mcentral 中。这里用途用 sizeclass 来表示,就是该 span 存储哪种大小的对象。


找到合适的 span 后,会从中取一个 object 返回给上层使用。


mcache


为了提高内存并发申请效率,加入缓存层 mcache。每一个 mcache 和处理器 P 对应。Go 申请内存首先从 P 的 mcache 中分配,如果没有可用的 span 再从 mcentral 中获取。


参考资料:Go 语言内存管理(二):Go 内存管理

37 ❤mutex 有几种模式?

mutex 有两种模式:normalstarvation


正常模式


所有 goroutine 按照 FIFO 的顺序进行锁获取,被唤醒的 goroutine 和新请求锁的 goroutine 同时进行锁获取,通常新请求锁的 goroutine 更容易获取锁(持续占有 cpu),被唤醒的 goroutine 则不容易获取到锁。公平性:否。


饥饿模式


所有尝试获取锁的 goroutine 进行等待排队,新请求锁的 goroutine 不会进行锁获取(禁用自旋),而是加入队列尾部等待获取锁。公平性:是。


参考链接:Go Mutex 饥饿模式GO 互斥锁(Mutex)原理

38 ❤go 如何进行调度的?GMP 中状态流转。

Go 里面 GMP 分别代表:G:goroutine,M:线程(真正在 CPU 上跑的),P:调度器 Processor


调度器是 M 和 G 之间桥梁。


go 进行调度过程:


  • 某个线程尝试创建一个新的 G,那么这个 G 就会被安排到这个线程的 G 本地队列 LRQ 中,如果 LRQ 满了,就会分配到全局队列 GRQ 中;

  • 尝试获取当前线程的 M,如果无法获取,就会从空闲的 M 列表中找一个,如果空闲列表也没有,那么就创建一个 M,然后绑定 G 与 P 运行。

  • 进入调度循环。

  • 找到一个合适的 G。

  • 执行 G,完成以后退出。

  • work stealing 机制


当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。


  • hand off 机制


当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

39 ❤Go 什么时候发生阻塞?阻塞时,调度器会怎么做。

  • 用于原子、互斥量或通道操作导致 goroutine 阻塞,调度器将把当前阻塞的 goroutine 从本地运行队列 LRQ 换出,并重新调度其它 goroutine;

  • 由于网络请求IO 导致的阻塞,Go 提供了网络轮询器(Netpoller)来处理,后台用 epoll 等技术实现 IO 多路复用。


其它回答:


  • channel 阻塞:当 goroutine 读写 channel 发生阻塞时,会调用 gopark 函数,该 G 脱离当前的 M 和 P,调度器将新的 G 放入当前 M。

  • 系统调用:当某个 G 由于系统调用陷入内核态,该 P 就会脱离当前 M,此时 P 会更新自己的状态为 Psyscall,M 与 G 相互绑定,进行系统调用。结束以后,若该 P 状态还是 Psyscall,则直接关联该 M 和 G,否则使用闲置的处理器处理该 G。

  • 系统监控:当某个 G 在 P 上运行的时间超过 10ms 时候,或者 P 处于 Psyscall 状态过长等情况就会调用 retake 函数,触发新的调度。

  • 主动让出:由于是协作式调度,该 G 会主动让出当前的 P(通过 GoSched),更新状态为 Grunnable,该 P 会调度队列中的 G 运行。


更多关于 netpoller 的内容可以参看:https://strikefreedom.top/go-netpoll-io-multiplexing-reactor

40 ❤Go 中 GMP 有哪些状态?

G 的状态:


_Gidle:刚刚被分配并且还没有被初始化,值为 0,为创建 goroutine 后的默认值


_Grunnable: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个 P 的本地队列或全局队列中(如上图)。


_Grunning: 正在执行代码的 goroutine,拥有栈的所有权(如上图)。


_Gsyscall:正在执行系统调用,拥有栈的所有权,与 P 脱离,但是与某个 M 绑定,会在调用结束后被分配到运行队列(如上图)。


_Gwaiting:被阻塞的 goroutine,阻塞在某个 channel 的发送或者接收队列(如上图)。


_Gdead: 当前 goroutine 未被使用,没有执行代码,可能有分配的栈,分布在空闲列表 gFree,可能是一个刚刚初始化的 goroutine,也可能是执行了 goexit 退出的 goroutine(如上图)。


_Gcopystac:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在系统线程上


_Gscan : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在。


P 的状态:


_Pidle :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空


_Prunning :被线程 M 持有,并且正在执行用户代码或者调度器(如上图)


_Psyscall:没有执行用户代码,当前线程陷入系统调用(如上图)


_Pgcstop :被线程 M 持有,当前处理器由于垃圾回收被停止


_Pdead :当前处理器已经不被使用


M 的状态:


自旋线程:处于运行状态但是没有可执行 goroutine 的线程,数量最多为 GOMAXPROC,若是数量大于 GOMAXPROC 就会进入休眠。


非自旋线程:处于运行状态有可执行 goroutine 的线程。

41 ❤GMP 能不能去掉 P 层?会怎么样?

P 层的作用


  • 每个 P 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。

  • 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。


参考资料:Go 面试官:GMP 模型,为什么要有 P? - 掘金

42 如果有一个 G 一直占用资源怎么办?什么是 work stealing 算法?

在 Go 语言中,Goroutine 的调度是通过 GMP 模型实现的。当一个 Goroutine 一直占用资源,导致其他 Goroutine 无法得到执行时,GMP 模型会从正常模式转变为饥饿模式,这意味着调度器会采取一些策略来解决这个问题。


其中,work stealing 算法是一种常用的策略之一。它的原理是,当一个线程处于空闲状态时,它会从其他正在忙碌的线程的本地队列中偷取(steal)一个 Goroutine 任务来执行。这样做的目的是为了充分利用系统资源,提高并发执行的效率。


具体来说,当一个线程(P)的本地队列中没有可执行的 Goroutine 时,它会尝试从其他线程(P)的本地队列中偷取一个 Goroutine 任务。这样可以避免线程空闲,提高整体的并发性能。


至于是从其他 P 的队列中偷取 Goroutine,还是从全局队列中偷取,这取决于具体的实现。在 Go 语言的调度器中,首先会尝试从其他 P 的本地队列中偷取 Goroutine 任务,如果没有可偷取的任务,则会从全局队列中获取。这样可以尽量减少全局队列的竞争,提高调度的效率。


总结起来,work stealing 算法是一种用于解决 Goroutine 饥饿问题的策略,通过从其他线程的本地队列中偷取任务来提高并发执行的效率。具体实现中,会优先从其他 P 的本地队列中偷取任务,然后再从全局队列中获取。这样的设计可以充分利用系统资源,提高并发性能。

43 goroutine 什么情况会发生内存泄漏?如何避免。

在 Go 中内存泄露分为暂时性内存泄露和永久性内存泄露。


暂时性内存泄露


  • 获取长字符串中的一段导致长字符串未释放

  • 获取长 slice 中的一段导致长 slice 未释放

  • 在长 slice 新建 slice 导致泄漏


  1. 获取长 slice 中的一段导致长 slice 未释放:

  • 当从一个长 slice 中获取一段子 slice 时,如果没有正确处理,子 slice 可能会持有对原长 slice 的引用,导致原长 slice 无法被释放。

  • 这种情况下,子 slice 会持有原长 slice 的底层数组的引用,即使子 slice 被丢弃,原长 slice 的底层数组也无法被释放,造成内存泄漏。

  • 避免内存泄漏的方法是使用copy函数将子 slice 复制到一个新的 slice 中,而不是直接引用原长 slice。

  1. 在长 slice 新建 slice 导致泄漏

  2. 当在一个长 slice 上再次使用切片操作创建一个新的 slice 时,新的 slice 会共享原长 slice 的底层数组,导致原长 slice 无法被释放。

  3. 这种情况下,新的 slice 会持有原长 slice 的底层数组的引用,即使新的 slice 被丢弃,原长 slice 的底层数组也无法被释放,造成内存泄漏。

  4. 避免内存泄漏的方法是使用copy函数将新的 slice 复制到一个新的 slice 中,而不是直接引用原长 slice。

为避免这两种情况下的内存泄漏,需要注意在处理 slice 时避免共享底层数组的引用,而是使用copy函数创建新的 slice。

确保在不需要使用的时候及时释放不再需要的内存,避免长期持有对底层数组的引用。

同时,合理使用defer语句、关闭资源、避免循环引用等方法也有助于避免内存泄漏的发生。


string 相比切片少了一个容量的 cap 字段,可以把 string 当成一个只读的切片类型。获取长 string 或者切片中的一段内容,由于新生成的对象和老的 string 或者切片共用一个内存空间,会导致老的 string 和切片资源暂时得不到释放,造成短暂的内存泄漏.


永久性内存泄露


  • goroutine 永久阻塞而导致泄漏

  • time.Ticker 未关闭导致泄漏

  • 不正确使用 Finalizer(Go 版本的析构函数)导致泄漏

44 Go GC 有几个阶段

目前的 go GC 采用三色标记法混合写屏障技术。


Go GC 有个阶段:


  • STW,开启混合写屏障,扫描栈对象;

  • 将所有对象加入白色集合,从根对象开始,将其放入灰色集合。每次从灰色集合取出一个对象标记为黑色,然后遍历其子对象,标记为灰色,放入灰色集合;

  • 如此循环直到灰色集合为空。剩余的白色对象就是需要清理的对象。

  • STW,关闭混合写屏障;

  • 在后台进行 GC(并发)。


STW:stop the world,指暂停用户业务。

45 go 竞态条件了解吗?

所谓竞态竞争,就是当两个或以上的 goroutine 访问相同资源时候,对资源进行读/写。


比如var a int = 0,有两个协程分别对 a+=1,我们发现最后 a 不一定为 2。这就是竞态竞争。


通常我们可以用go run -race xx.go来进行检测。


解决方法是,对临界区资源上锁,或者使用原子操作(atomics),原子操作的开销小于上锁。

46 如果若干个 goroutine,有一个 panic 会怎么做?

有一个 panic,那么剩余 goroutine 也会退出,程序退出。如果不想程序退出,那么必须通过调用 recover() 方法来捕获 panic 并恢复将要崩掉的程序。


参考理解:goroutine配上panic会怎样

47 defer 可以捕获 goroutine 的子 goroutine 吗?

不可以。它们处于不同的调度器 P 中。对于子 goroutine,必须通过 recover() 机制来进行恢复,然后结合日志进行打印(或者通过 channel 传递 error),下面是一个例子:


// 心跳函数func Ping(ctx context.Context) error {    ... code ...         go func() {                defer func() {                        if r := recover(); r != nil {                                log.Errorc(ctx, "ping panic: %v, stack: %v", r, string(debug.Stack()))                        }                }()         ... code ...        }()     ... code ...         return nil}
复制代码

48 gRPC 是什么?

基于 go 的远程过程调用。RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。

49 ❤channel 死锁的场景

  • 当一个 channel 中没有数据,而直接读取时,会发生死锁:


q := make(chan int,2)<-q
复制代码


解决方案是采用 select 语句,再 default 放默认处理方式:


q := make(chan int,2)select{   case val:=<-q:   default:         ...
}
复制代码


  • 当 channel 数据满了,再尝试写数据会造成死锁:


q := make(chan int,2)q<-1q<-2q<-3
复制代码


解决方法,采用 select


func main() {        q := make(chan int, 2)        q <- 1        q <- 2        select {        case q <- 3:                fmt.Println("ok")        default:                fmt.Println("wrong")        }
}
复制代码


  • 向一个关闭的 channel 写数据。


注意:一个已经关闭的 channel,只能读数据,不能写数据。


参考资料:Golang关于channel死锁情况的汇总以及解决方案

50 ❤对已经关闭的 chan 进行读写会怎么样?

  • 读已经关闭的 chan 能一直读到东西,但是读到的内容根据通道内关闭前是否有元素而不同。

  • 如果 chan 关闭前,buffer 内有元素还未读,会正确读到 chan 内的值,且返回的第二个 bool 值(是否读成功)为 true。

  • 如果 chan 关闭前,buffer 内有元素已经被读完,chan 内无值,接下来所有接收的值都会非阻塞直接成功,返回 channel 元素的零值,但是第二个 bool 值一直为 false。


写已经关闭的 chan 会 panic。

51 说说 atomic 底层怎么实现的.

atomic 源码位于sync\atomic。通过阅读源码可知,atomic 采用 CAS(CompareAndSwap)的方式实现的。所谓 CAS 就是使用了 CPU 中的原子性操作。在操作共享变量的时候,CAS 不需要对其进行加锁,而是通过类似于乐观锁的方式进行检测,总是假设被操作的值未曾改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。本质上是不断占用 CPU 资源来避免加锁的开销


参考资料:Go语言的原子操作atomic - 编程猎人

52 channel 底层实现?是否线程安全。

channel 底层实现在 src/runtime/chan.go 中


channel 内部是一个循环链表。内部包含 buf, sendx, recvx, lock ,recvq, sendq 几个部分;


buf 是有缓冲的 channel 所特有的结构,用来存储缓存数据。是个循环链表;


  • sendx 和 recvx 用于记录 buf 这个循环链表中的发送或者接收的 index;

  • lock 是个互斥锁;

  • recvq 和 sendq 分别是接收(<-channel)或者发送(channel <- xxx)的 goroutine 抽象出来的结构体(sudog)的队列。是个双向链表。


channel 是线程安全的。


参考资料:Kitou:Golang 深度剖析 -- channel的底层实现

53 map 的底层实现。

源码位于 src\runtime\map.go 中。


go 的 map 和 C++map 不一样,底层实现是哈希表,包括两个部分:hmap bucket


里面最重要的是 buckets(桶),buckets 是一个指针,最终它指向的是一个结构体:


// A bucket for a Go map.type bmap struct {    tophash [bucketCnt]uint8}
复制代码


每个 bucket 固定包含 8 个 key 和 value(可以查看源码 bucketCnt=8).实现上面是一个固定的大小连续内存块,分成四部分:每个条目的状态,8 个 key 值,8 个 value 值,指向下个 bucket 的指针。


创建哈希表使用的是 makemap 函数.map 的一个关键点在于,哈希函数的选择。在程序启动时,会检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。这是在函数 alginit() 中完成,位于路径:src/runtime/alg.go 下。


map 查找就是将 key 哈希后得到 64 位(64 位机)用最后 B 个比特位计算在哪个桶。在 bucket 中,从前往后找到第一个空位。这样,在查找某个 key 时,先找到对应的桶,再去遍历 bucket 中的 key。


关于 map 的查找和扩容可以参考map的用法到map底层实现分析

54 select 的实现原理?

select 源码位于 src\runtime\select.go,最重要的 scase 数据结构为:


type scase struct {        c    *hchan         // chan        elem unsafe.Pointer // data element}
复制代码


scase.c 为当前 case 语句所操作的 channel 指针,这也说明了一个 case 语句只能操作一个 channel。


scase.elem 表示缓冲区地址:


  • caseRecv : scase.elem 表示读出 channel 的数据存放地址;

  • caseSend : scase.elem 表示将要写入 channel 的数据存放地址;


select 的主要实现位于:select.go 函数:其主要功能如下:


  1. 锁定 scase 语句中所有的 channel

  2. 按照随机顺序检测 scase 中的 channel 是否 ready


​ 2.1 如果 case 可读,则读取 channel 中数据,解锁所有的 channel,然后返回(case index, true)


​ 2.2 如果 case 可写,则将数据写入 channel,解锁所有的 channel,然后返回(case index, false)


​ 2.3 所有 case 都未 ready,则解锁所有的 channel,然后返回(default index, false)


  1. 所有 case 都未 ready,且没有 default 语句


​ 3.1 将当前协程加入到所有 channel 的等待队列


​ 3.2 当将协程转入阻塞,等待被唤醒


  1. 唤醒后返回 channel 对应的 case index


​ 4.1 如果是读操作,解锁所有的 channel,然后返回(case index, true)


​ 4.2 如果是写操作,解锁所有的 channel,然后返回(case index, false)


参考资料:[Go select 的使用和实现原理](https://www.cnblogs.com/wuyepeng/p/13910678.html#:~:text=一、select 简介. 1.Go 的 select 语句是一种仅能用于 channl 发送和接收消息的专用语句,此语句运行期间是阻塞的;当 select 中没有 case 语句的时候,会阻塞当前 groutine。. 2.select 是 Golang 在语言层面提供的 I%2FO 多路复用的机制,其专门用来检测多个 channel 是否准备完毕:可读或可写。.,3.select 语句中除 default 外,每个 case 操作一个 channel,要么读要么写. 4.select 语句中除 default 外,各 case 执行顺序是随机的. 5.select 语句中如果没有 default 语句,则会阻塞等待任一 case. 6.select 语句中读操作要判断是否成功读取,关闭的 channel 也可以读取).

55 go 的 interface 怎么实现的?

go interface 源码在runtime\iface.go中。


go 的接口由两种类型实现 iface 和 eface。iface 是包含方法的接口,而 eface 不包含方法。


  • iface


对应的数据结构是(位于src\runtime\runtime2.go):


type iface struct {        tab  *itab        data unsafe.Pointer}
复制代码


可以简单理解为,tab 表示接口的具体结构类型,而 data 是接口的值。


  • itab


type itab struct {        inter *interfacetype //此属性用于定位到具体interface        _type *_type //此属性用于定位到具体interface        hash  uint32 // copy of _type.hash. Used for type switches.        _     [4]byte        fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.}
复制代码


属性 interfacetype 类似于_type,其作用就是 interface 的公共描述,类似的还有 maptype、arraytype、chantype…其都是各个结构的公共描述,可以理解为一种外在的表现信息。interfaetype 和 type 唯一确定了接口类型,而 hash 用于查询和类型判断。fun 表示方法集。


  • eface


与 iface 基本一致,但是用_type 直接表示类型,这样的话就无法使用方法。


type eface struct {        _type *_type        data  unsafe.Pointer}
复制代码


这里篇幅有限,深入讨论可以看:深入研究 Go interface 底层实现

56 go 的 reflect 底层实现

go reflect 源码位于 src\reflect\下面,作为一个库独立存在。反射是基于接口实现的。


Go 反射有三大法则:


  • 反射从接口映射到反射对象;

  • 反射从反射对象映射到接口值

  • 只有值可以修改(settable),才可以修改反射对象。


Go 反射基于上述三点实现。我们先从最核心的两个源文件入手 type.go 和 value.go.


type 用于获取当前值的类型。value 用于获取当前的值。


参考资料:The Laws of Reflection图解go反射实现原理

57 go GC 的原理知道吗?

如果需要从源码角度解释 GC,推荐阅读(非常详细,图文并茂):


https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/

58 go 里用过哪些设计模式 ?

我写了一篇专门讲设计模式的文章:


https://zhuanlan.zhihu.com/p/542596378

59 go 的调试/分析工具用过哪些。

go 的自带工具链相当丰富,


  • go cover : 测试代码覆盖率;

  • godoc: 用于生成 go 文档;

  • pprof:用于性能调优,针对 cpu,内存和并发;

  • race:用于竞争检测;

60 进程被 kill,如何保证所有 goroutine 顺利退出

goroutine 监听 SIGKILL 信号,一旦接收到 SIGKILL,则立刻退出。可采用 select 方法。


var wg = &sync.WaitGroup{}
func main() { wg.Add(1)
go func() { c1 := make(chan os.Signal, 1) signal.Notify(c1, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) fmt.Printf("goroutine 1 receive a signal : %v\n\n", <-c1) wg.Done() }()
wg.Wait() fmt.Printf("all groutine done!\n")}
复制代码

61 说说 context 包的作用?你用过哪些,原理知道吗?

context 可以用来在 goroutine 之间传递上下文信息,相同的 context 可以传递给运行在不同 goroutine 中的函数,上下文对于多个 goroutine 同时使用是安全的,context 包定义了上下文类型,可以使用 background、TODO 创建一个上下文,在函数调用链之间传播 context,也可以使用 WithDeadline、WithTimeout、WithCancel 或 WithValue 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context 的作用就是在不同的 goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。


关于 context 原理,可以参看:小白也能看懂的context包详解:从入门到精通

62 Go 语言支持哪些数据类型?

Go 语言支持丰富的数据类型,主要分为基本数据类型、复合数据类型和特殊数据类型三类:


  1. 基本数据类型

  2. 布尔型(bool):取值为 truefalse

  3. 整数型:包括有符号整数(intint8int16int32int64)和无符号整数(uintuint8uint16uint32uint64),其中 intuint 的宽度取决于平台(32 位或 64 位)。

  4. 浮点型float32(32 位)和 float64(64 位),后者是默认浮点类型。

  5. 复数型complex64(实部和虚部均为 float32)和 complex128(实部和虚部均为 float64)。

  6. 字符串型(string):UTF-8 编码的不可变文本,本质是字节序列。

  7. 字节型byteuint8 的别名)和 runeint32 的别名,用于存储 Unicode 字符)。

  8. 复合数据类型

  9. 数组(array):固定长度的相同类型元素序列,长度是类型的一部分(如 [5]int)。

  10. 切片(slice):可变长度的数组封装,支持动态扩容(如 []int)。

  11. 映射(map):键值对集合,键需为相同类型(如 map[string]int)。

  12. 结构体(struct):用户自定义的聚合类型,可包含不同类型字段(如 type Point struct { X, Y int })。

  13. 指针(pointer):存储变量内存地址(如 *int)。

  14. 函数(function):可作为类型传递(如 func(a int) int)。

  15. 接口(interface):定义方法集合,实现多态(如 type Reader interface { Read() })。

  16. 通道(channel):用于协程(goroutine)间通信(如 chan int)。

  17. 特殊数据类型

  18. 空值(nil):表示无值,常用于指针、切片等。

  19. 错误(error):内置接口类型,用于错误处理。

63 如何在 Go 中实现变量的类型转换?

在 Go 中,类型转换需显式操作,不支持隐式转换,主要方法包括:


  1. 显式类型转换(T(v))

  2. 语法:目标类型(表达式),例如将 int 转为 float64f := float64(42)

  3. 适用场景:数值类型间转换(如整型与浮点型),但需注意精度丢失(如 floatint 会截断小数)。

  4. 使用 strconv 包处理字符串转换

  5. 字符串转基本类型:如 strconv.Atoi("123") 将字符串转为 int,返回值和错误信息。

  6. 基本类型转字符串:如 strconv.Itoa(123)strconv.FormatFloat(3.14, 'f', 2, 64)

  7. 类型断言(针对接口类型)

  8. 语法:value, ok := interfaceVar.(具体类型),例如从空接口 interface{} 提取字符串:s, ok := i.(string)

  9. 失败时 okfalse,若忽略 ok 可能触发 panic

  10. 强制转换(需谨慎)

  11. 使用 unsafe.Pointer 实现跨类型指针转换,如 newPtr := (*T2)(unsafe.Pointer(ptr)),但可能引发安全问题。


注意事项:


  • 不兼容类型(如 float[]byte)会编译失败。

  • 字符串与 []byte 可直接互转:b := []byte(s)s := string(b)

64 constvar的区别是什么?

constvar 均用于声明标识符,但核心区别在于可变性与初始化:


  1. 可变性

  2. var 声明变量,值可修改(如 var x int = 10; x = 20)。

  3. const 声明常量,值不可修改(如 const Pi = 3.14; Pi = 3.14 会编译错误)。

  4. 初始化要求

  5. const 必须在声明时赋值,且值需为编译时可确定的表达式(如字面量或常量运算)。

  6. var 可不初始化(默认为零值,如 int 的零值为 0),或后续赋值。

  7. 类型处理

  8. const 支持无类型常量(如 const a = 42),类型由上下文推断;var 需显式或隐式指定类型。

  9. 常量可参与不同类型运算(如 const b = 1.5 + 2),而变量需显式转换。

  10. 作用域与批量声明

  11. 两者均支持批量声明(如 var (x int; y string)const (A=1; B=2))。

  12. const 常用于枚举(结合 iota),如 const (Sunday = iota; Monday)


总结:const 适用于固定值(如配置参数),var 用于需变化的场景。

65 select语句有什么用途?

select 是 Go 中处理多通道(channel)操作的控制结构,核心用途包括:


  1. 多路复用(监听多个 channel)

  2. 同时等待多个 channel 的读写操作,随机选择一个就绪的 case 执行(如 case <-ch1case ch2 <- data),避免阻塞。

  3. 超时控制

  4. 结合 time.After 实现超时机制,例如:select { case <-dataChan: ...; case <-time.After(3*time.Second): ... }

  5. 非阻塞操作

  6. 添加 default 分支实现非阻塞,当无 channel 就绪时立即执行 default(如用于检查 channel 状态)。

  7. 程序退出与错误处理

  8. 监听退出信号 channel(如 <-doneChan),实现优雅终止。

  9. 检测 channel 关闭(如 case v, ok := <-chok 标识状态)。


注意事项:


  • select 的 case 必须是 channel 操作,且随机选择就绪 case 以保证公平性。

  • default 且无 channel 就绪时,会阻塞直到至少一个就绪。


典型场景:高并发服务中协调 goroutine 通信,如 API 网关或消息队列。

66 sync.Pool有什么用途?

sync.Pool 是 Go 标准库的对象池,用于优化临时对象管理,主要用途包括:


  1. 减少内存分配与 GC 压力

  2. 复用短期对象(如缓冲区、解析器状态),避免频繁分配和垃圾回收,提升性能(尤其在高并发场景)。

  3. 实现原理

  4. 层级缓存:每个处理器(P)维护私有队列(private)和共享环形队列(shared),优先本地存取。

  5. GC 敏感性:池中对象可能在两次 GC 间被回收,防止内存泄漏。

  6. 并发安全:通过无锁设计减少竞争(如 Get()Put() 方法)。

  7. 适用场景

  8. 高频创建/销毁对象(如 JSON 解码中的临时结构体)。

  9. 资源复用(如数据库连接池),但需注意对象重置(Put() 前清除状态)。

67 Go 常用的开发工具有哪些?

Go 开发工具涵盖 IDE、编辑器、调试和依赖管理等,常用工具包括:


  1. IDE 与编辑器

  2. GoLand:JetBrains 出品,提供智能补全、调试、重构等专业功能,适合大型项目。

  3. Visual Studio Code (VS Code):轻量级,通过 Go 插件(如 Go Extension Pack)支持语法高亮、调试和测试,免费且跨平台。

  4. 包管理与构建

  5. Go Modules:官方依赖管理工具(Go 1.11+),自动处理版本和依赖冲突(如 go mod init)。

  6. Go Build/Test:内置编译和测试工具(如 go build 编译,go test 运行测试)。

  7. 调试与分析

  8. Delve:专为 Go 设计的调试器,支持断点、变量检查,可集成到 IDE。

  9. Pprof:性能分析工具,用于 CPU 和内存剖析。

  10. 代码质量工具

  11. Golint:静态代码分析,检查风格和潜在错误。

  12. GORM:ORM 库,简化数据库操作。


建议:初学者用 VS Code 快速上手,专业开发者选 GoLand;调试首选 Delve,依赖管理用 Go Modules。

68 Go 常用的测试工具有哪些?

Go 的测试工具主要分为以下几类,覆盖单元测试、覆盖率分析、性能测试和集成测试等场景:


  1. 单元测试工具:

  2. testing 包:Go 内置的标准库,提供基础断言和测试函数结构,例如 TestXxx(t *testing.T)

  3. testify/assert:第三方库,扩展断言功能(如 Equal()Nil()),支持自定义错误消息,简化测试代码。

  4. 代码覆盖率工具:

  5. go test -cover:生成代码覆盖率报告,-coverprofile 输出详细数据文件,结合 go tool cover -html 可视化分析。

  6. go-cover:第三方工具,提供更丰富的 HTML 报告和集成支持。

  7. 基准测试工具:

  8. testing/benchmark:内置基准测试框架,通过 BenchmarkXxx(b *testing.B) 测量代码性能,支持 -benchmem 分析内存分配。

  9. go-benchmark:第三方库,简化性能测试编写和结果解析。

  10. 集成测试与 Mock 工具:

  11. gocheck/echo/test:用于 HTTP 服务集成测试,模拟请求和验证响应。

  12. Mock 工具:

  13. gock:HTTP 请求模拟,录制和重放网络交互。

  14. go-sqlmock:数据库模拟,替代真实 SQL 驱动进行测试。

  15. miniredis:Redis 内存模拟,用于无依赖测试。

  16. BDD 测试框架:

  17. Ginkgo/Gomega:行为驱动开发(BDD)风格框架,提升测试可读性。


💡 面试提示:重点掌握 testingtestify 的单元测试实践,覆盖率与基准测试是高频考点。Mock 工具在微服务测试中尤为重要。

69 Go 常用的调试工具有哪些?

Go 调试工具涵盖日志、调试器和性能分析,适用于不同场景:


  1. 内置工具:

  2. fmt/log 包:通过 Printlnlog.Println 输出变量值和执行路径,适合快速定位简单问题。

  3. go test:运行单元测试并输出详细日志(-v 参数),结合 -run 过滤特定测试用例。

  4. 外部调试器:

  5. Delve (dlv):

  6. 功能:设置断点(break)、单步执行(next/step)、查看变量(print)。

  7. 使用:dlv debug main.go 启动交互会话,或集成到 CI/CD 流程。

  8. IDE 集成调试:

  9. Goland:内置调试支持,可视化断点管理和堆栈跟踪。

  10. VSCode:通过 Go 插件和 launch.json 配置调试环境,支持远程调试。

  11. 性能分析工具:

  12. pprof:分析 CPU 和内存瓶颈,生成火焰图(go test -cpuprofile)。

  13. trace:追踪协程调度和阻塞事件,优化并发性能。


⚠️ 注意:Delve 是 Go 调试首选工具,尤其在并发调试中优于 GDB。生产环境慎用调试器,优先依赖日志和测试。

70 反射机制是什么?如何使用?

反射机制允许程序在运行时动态获取类型信息、修改变量值或调用方法,通过 reflect 包实现。核心是 Type(类型描述)和 Value(值操作)两个结构。


使用方式


  1. 获取类型信息:


   t := reflect.TypeOf(variable)  // 获取类型     fmt.Println(t.Name(), t.Kind()) // 输出类型名和种类(如 struct)  
复制代码


遍历结构体字段:t.NumField()和 t.Field(i)。


  1. 操作变量值:


   v := reflect.ValueOf(&variable).Elem()  // 获取可修改的 Value     if v.Kind() == reflect.Struct {         v.FieldByName("Name").SetString("NewName") // 修改字段值     }  
复制代码


需通过指针调用 Elem(),避免值拷贝。


  1. 动态调用方法:


   method := v.MethodByName("MethodName")     args := []reflect.Value{reflect.ValueOf(arg1)}     result := method.Call(args) // 调用并获取返回值  
复制代码


适用于工厂模式或插件化系统。


应用场景


  • JSON 序列化/反序列化(如 encoding/json 包)。

  • 框架中动态处理接口类型(如 ORM 映射字段)。


⚠️ 性能提示:反射有运行时开销,频繁操作时优先考虑代码生成或接口设计。

71 类型断言是什么?如何使用?

类型断言用于从接口变量中提取具体类型值,语法为 value, ok := interfaceVar.(TargetType)


使用方式


  1. 基础用法:


   var i interface{} = "hello"     s, ok := i.(string)  // ok=true 时 s 为字符串值     if !ok {         // 处理类型不匹配     }  
复制代码


失败时 ok=false,避免直接 s := i.(string) 引发 panic。


  1. 结合 switch 类型判断:


   switch v := i.(type) {     case int:         fmt.Println("int:", v)     case string:         fmt.Println("string:", v)     default:         fmt.Println("unknown type")     }  
复制代码


简化多类型分支处理。


应用场景


  • 处理 interface{} 空接口(如 JSON 解析后 map[string]interface{} 的字段提取)。

  • 接口多态实现中获取具体类型方法(如 Shape 接口转为 Circle 调用 Circumference())。


💡 技巧:JSON 数字默认解析为 float64,需断言后转换:int(data["age"].(float64))

72 闭包是什么?

闭包是匿名函数与其外部作用域变量的绑定,即使外部函数执行结束,闭包仍能访问和修改这些变量。


特性与机制


  1. 状态封装:闭包捕获的变量生命周期延长至闭包本身销毁(存储在堆上),例如计数器:


   func counter() func() int {         count := 0         return func() int { count++; return count }     }     c := counter()     c() // 1, c() // 2 (count 状态持续)  
复制代码


  1. 词法作用域:闭包在定义时捕获变量引用(非值拷贝),循环中使用需避免共享变量问题。


主要应用场景


  • 回调与异步处理:如 HTTP 中间件(loggingMiddleware 闭包记录请求)。

  • 函数工厂:生成定制化函数(multiplier(2) 返回加倍闭包)。

  • 资源清理defer 结合闭包确保资源释放(如解锁互斥锁)。

  • 装饰器模式:扩展函数功能(如 logExecutionTime 闭包包装耗时函数)。


⚠️ 注意:闭包可能导致循环引用或内存泄漏(如大对象被闭包长期引用),并发场景需加锁保证安全。

74 Go 与 Python 的比较?

Go 和 Python 在设计和应用场景上有显著差异:


  1. 性能与执行模型:Go 是编译型语言,编译为机器码直接运行,执行速度更快(尤其在 CPU 密集型任务中),平均响应时间比 Python 快数倍。Python 是解释型语言,依赖解释器逐行执行,运行时性能较低,但开发迭代快。

  2. 并发处理:Go 内置轻量级协程(goroutine)和通道(channel),支持高并发(如百万级任务),资源开销小(每个 goroutine 约 2KB 内存)。Python 受全局解释器锁(GIL)限制,多线程效率低,需依赖多进程或异步库(如 asyncio),并发能力较弱。

  3. 内存管理:Go 的垃圾回收(GC)优化较好,暂停时间短(<1ms),适合长时间运行服务。Python 的 GC 机制简单,但内存占用高(相同任务内存消耗是 Go 的 3 倍以上),易引发性能波动。

  4. 开发效率与生态:Python 语法简洁,库丰富(如 NumPy、Django),适合快速原型开发、数据分析和 AI 领域。Go 语法严谨,标准库强大(如 HTTP、JSON 解析),但第三方生态不如 Python 成熟,更适合高性能后端和微服务。

  5. 适用场景:Python 适用于 I/O 密集型任务(如 Web 快速开发、脚本),Go 更适合高并发系统(如 API 网关、云原生应用)。

75 Go 与 Java 的比较?

Go 和 Java 在性能和架构上各有优势:


  1. 启动速度与部署:Go 静态编译为单一二进制文件,启动快(毫秒级),无外部依赖,适合容器化部署。Java 需 JVM 环境,启动慢(含类加载和 JIT 编译),部署复杂。

  2. 并发模型:Go 的 goroutine 轻量(开销仅 KB 级),基于 CSP 模型实现高效并发;Java 依赖线程池(线程开销 MB 级),需显式同步(如 synchronized),高并发下资源消耗大。

  3. 内存管理:Go 的 GC 采用并发标记清除,暂停时间可控;Java 的 GC(如 G1、ZGC)更成熟,适合长期运行的大内存应用,但配置不当易导致性能波动。

  4. 运行时性能:Go 直接执行机器码,短期任务响应快;Java 依赖 JIT 优化,长期运行后性能更优(如大数据处理)。

  5. 生态与适用性:Java 生态庞大(Spring、Hibernate),适合企业级复杂系统;Go 生态较新(如 Gin 框架),更契合微服务和云原生场景。

76 Go 与 C++的比较?

Go 和 C++在系统级开发中定位不同:


  1. 性能:C++编译后接近硬件层,执行速度最快(尤其计算密集型任务),适合游戏引擎等极致性能场景。Go 性能接近 C++,但略低(约慢 10-30%),优势在于并发和安全性。

  2. 内存安全:Go 通过 GC 自动管理内存,减少泄漏风险;C++需手动管理,易引发空指针等问题,但提供更精细控制。

  3. 开发效率:Go 语法简洁,编译快(秒级),内置工具链(如 gofmt);C++编译慢(模板实例化复杂),学习曲线陡峭。

  4. 并发支持:Go 原生支持 goroutine 和 channel,简化并发编程;C++需依赖第三方库(如 Boost)或标准线程,实现复杂。

  5. 适用场景:C++适用于操作系统、嵌入式等底层开发;Go 更适合网络服务和高并发分布式系统。

77 Go 与 Ruby 的比较?

Go 和 Ruby 在 Web 开发中差异显著:


  1. 性能:Go 是编译型语言,执行速度快(响应时间毫秒级),Ruby 解释执行速度慢(响应时间常超 10ms)。高并发下,Go 的吞吐量是 Ruby 的 5 倍以上。

  2. 并发能力:Go 内置 goroutine,轻松处理万级并发;Ruby 依赖进程/线程模型(如 Rails 线程池),效率低且资源占用高。

  3. 内存管理:Go 的 GC 高效,内存占用稳定;Ruby 的 GC 易引发停顿,高负载下需频繁重启 Worker 进程。

  4. 生态:Ruby 生态成熟(如 Rails 框架),适合快速开发中小型 Web 应用;Go 生态较新,但更适合构建高性能 API 和微服务。

  5. 开发体验:Ruby 语法灵活,开发效率高;Go 强调简洁性,适合团队协作和大型项目维护。

78 Go 与 PHP 的比较?

Go 和 PHP 在 Web 后端领域各有侧重:


  1. 性能:Go 编译执行,响应快(平均 0.8ms),PHP 解释执行速度慢(平均 2.5ms),高并发下 Go 的 QPS 是 PHP 的 3 倍以上。

  2. 并发处理:Go 原生支持高并发(goroutine 开销小),PHP 需依赖 Swoole 扩展模拟协程,效率较低。

  3. 内存占用:Go 内存管理高效(50MB/协程),PHP 进程占用高(180MB/进程),易引发资源瓶颈。

  4. 开发效率:PHP 上手快(如 Laravel 框架),适合快速迭代;Go 开发周期较长,但代码可维护性强。

  5. 适用场景:PHP 适合 CMS、中小型网站;Go 适合云原生、高并发服务(如 API 网关)。

79 Go 与 Rust 的比较?

Go 和 Rust 在系统编程中代表不同设计哲学:


  1. 内存安全:Rust 通过所有权模型在编译时保证内存安全(无 GC),避免数据竞争;Go 依赖 GC,可能引入延迟。

  2. 性能:Rust 性能接近 C++(零成本抽象),CPU 密集型任务更快;Go 性能优秀但略低于 Rust,优势在开发效率。

  3. 并发模型:Go 的 goroutine 易用,适合快速实现高并发;Rust 的异步模型(如 tokio)性能更高(延迟降低 60%),但实现复杂。

  4. 开发体验:Go 语法简单,学习曲线平缓(约 2 周);Rust 所有权机制严格,学习成本高(约 8 周)。

  5. 适用场景:Go 适合微服务和快速开发;Rust 适合嵌入式、操作系统等对安全和性能要求极高的领域。

80 数组和切片的区别?

数组(Array)和切片(Slice)是 Go 语言中两种不同的数据结构,核心区别如下:


  1. 长度固定性:

  2. 数组的长度在声明时确定,无法动态改变(如 var arr [3]int)。

  3. 切片的长度可变,支持通过 append 动态扩展(如 slice := []int{1, 2, 3})。

  4. 内存分配与传递:

  5. 数组是值类型,赋值或传参时会复制整个数据,内存开销大。

  6. 切片是引用类型,底层包含指向数组的指针、长度和容量,传递时仅复制结构体(24 字节),共享底层数组。

  7. 容量机制:

  8. 数组无容量概念,大小固定。

  9. 切片有容量(cap),表示底层数组可容纳的元素上限,支持自动扩容。

  10. 初始化方式:

  11. 数组需显式指定长度(如 [3]int{1,2,3})。

  12. 切片可通过 make、字面量或从数组截取(如 arr[1:4])创建。


面试提示:强调切片基于数组封装,但类型不兼容([3]int[]int 是不同类型),优先使用切片以提升灵活性。

81 切片的底层结构

切片的底层是一个结构体,包含三个字段:


type slice struct {    array unsafe.Pointer // 指向底层数组的指针    len   int            // 当前元素数量(长度)    cap   int            // 底层数组总容量}
复制代码

82 map 的底层结构

Map 的底层由哈希表实现,核心结构为 hmapbmap


type hmap struct {    count     int        // 元素数量    B         uint8      // 桶数量的对数(桶数 = 2^B)    buckets   unsafe.Pointer // 指向桶数组的指针    oldbuckets unsafe.Pointer // 扩容时旧桶指针    hash0     uint32     // 哈希种子    // ... 其他字段}
type bmap struct { tophash [8]uint8 // 存储哈希值高8位(快速定位) keys [8]keytype // 键数组 values [8]valuetype // 值数组(内存紧凑) overflow *bmap // 溢出桶指针(链表结构)}
复制代码

83 map 定位过程?

Map 的键值定位流程如下:


  1. 计算哈希值:对键调用哈希函数(如 hash(key)),生成 64 位哈希值。

  2. 确定桶位置:用哈希低位定位桶(桶索引 = hash & (1<<B - 1))。

  3. 匹配 tophash:在桶的 tophash 数组中匹配哈希值高 8 位:

  4. 匹配成功:比较键值是否相等(解决哈希碰撞)。

  5. 匹配失败:检查溢出桶链表。

  6. 处理冲突:若当前桶无匹配,遍历溢出桶链表直至找到或结束。


⏱️ 时间复杂度:平均 O(1),最坏 O(n)(哈希退化时退化为链表遍历)。

84 为什么 map 是无序的?

Map 的无序性由设计机制决定:


  1. 哈希表本质:元素存储位置由哈希值决定,与插入顺序无关。

  2. 随机遍历起始点:每次遍历随机选择起始桶和偏移量,避免开发者依赖顺序。

  3. 扩容扰动:扩容时元素重新哈希到新桶,顺序彻底改变。

  4. 设计意图:强制开发者不依赖顺序,防止潜在逻辑错误(如顺序敏感场景应改用切片排序)。


解决方案:需有序遍历时,提取键排序后访问(如 sort.Strings(keys))。

85 map 是并发安全的吗?

Map 非并发安全,原因与解决方案如下:


  • 风险点:并发读写可能触发 fatal error: concurrent map read and map write

  • 解决方案:

  • 互斥锁:用 sync.Mutexsync.RWMutex 包裹操作(读多写少时用读写锁)。

  • sync.Map:专为并发场景设计,适合读多写少或键值稳定的场景(如配置管理)。

  • 分区锁:对大 Map 分片加锁(如 []map + 分片锁)提升性能。


⚠️ 注意sync.MapRange 方法保证遍历时一致性,但性能略低于手动分片。

86 channel 的底层结构

Channel 的底层由 hchan 结构体表示:


type hchan struct {    buf      unsafe.Pointer // 环形缓冲区指针(有缓冲channel)    sendx    uint           // 发送索引    recvx    uint           // 接收索引    sendq    waitq          // 发送等待队列(sudog链表)    recvq    waitq          // 接收等待队列    lock     mutex          // 互斥锁    closed   uint32         // 关闭标志    // ... 其他字段}
复制代码

87 defer 的特点

Defer 的关键特性如下:


  1. 延迟执行:函数退出前按声明顺序的逆序执行(LIFO)。

  2. 参数预求值:参数在注册时计算(如 defer fmt.Println(i)i 立即求值)。

  3. 栈式管理:多个 defer 压入链表,函数返回时从链表头部依次执行。

  4. 应用场景:

  5. 资源释放(如文件关闭 defer file.Close())。

  6. 锁解锁(defer mu.Unlock())。

  7. 捕获 panic(defer func() { recover() }())。

  8. 返回值修改:若函数返回具名变量,defer 可修改返回值(通过闭包捕获变量):


   func f() (x int) {       defer func() { x++ }()       return 1 // 实际返回2   }
复制代码


性能影响:Go 1.14+ 优化后开销降低,但高频短函数中仍建议避免滥用。

88 解释 Go 语言中的指针,并描述其与 C/C++ 指针的主要区别

指针是存储变量内存地址的变量。Go 语言中的指针与 C/C++ 指针的主要区别在于 Go 没有指针运算,不能对指针进行加减等操作,这减少了指针操作的风险。

89 如何优化内存使用?

90 如何优化垃圾回收?

91 单例模式实现

使用 sync.Once 实现线程安全的单例模式。


package mainimport (  "fmt"  "sync")type Singleton struct{}var instance *Singletonvar once sync.Oncefunc GetInstance() *Singleton {  once.Do(func() {    instance = &Singleton{}  })  return instance}func main() {  a := GetInstance()  b := GetInstance()  fmt.Println(a == b) // 输出 true}
复制代码

92 读写锁实现

使用 sync.RWMutex 实现读写锁,保证数据读写的线程安全。


package mainimport (  "fmt"  "sync"  "time")type Data struct {  sync.RWMutex  value int}func (d *Data) Read() int {  d.RLock()  defer d.RUnlock()  return d.value}func (d *Data) Write(v int) {  d.Lock()  defer d.Unlock()  d.value = v}func main() {  data := &Data{}  var wg sync.WaitGroup  for i := 0; i < 10; i++ {    wg.Add(1)    go func() {      defer wg.Done()      for j := 0; j < 5; j++ {        data.Write(j)        time.Sleep(10 * time.Millisecond)      }    }()  }  for i := 0; i < 10; i++ {    wg.Add(1)    go func() {      defer wg.Done()      for j := 0; j < 5; j++ {        fmt.Println(data.Read())        time.Sleep(10 * time.Millisecond)      }    }()  }  wg.Wait()}
复制代码

93 使用 channel 实现消息队列

使用 channel 实现一个简单的消息队列,支持并发生产者和消费者。


package mainimport (  "fmt"  "sync"  "time")type MessageQueue struct {  queue chan int  wg    sync.WaitGroup}func NewMessageQueue(size int) *MessageQueue {  return &MessageQueue{    queue: make(chan int, size),  }}func (mq *MessageQueue) Produce(v int) {  mq.queue <- v}func (mq *MessageQueue) Consume() int {  return <-mq.queue}func main() {  mq := NewMessageQueue(10)  for i := 0; i < 3; i++ {    go func() {      for j := 0; j < 10; j++ {        mq.Produce(j)        time.Sleep(100 * time.Millisecond)      }    }()  }  for i := 0; i < 3; i++ {    go func() {      for j := 0; j < 10; j++ {        fmt.Println(mq.Consume())        time.Sleep(100 * time.Millisecond)      }    }()  }  time.Sleep(5 * time.Second)}
复制代码

94 使用 timer 实现定时任务

使用 timer 实现每 2 秒打印一次当前时间。


package mainimport (  "fmt"  "time")func main() {  timer := time.NewTimer(2 * time.Second)  for {    select {    case <-timer.C:      fmt.Println(time.Now().Format("2006-01-02 15:04:05"))      timer.Reset(2 * time.Second)    }  }}
复制代码

95 使用 context 实现取消机制

使用 context 实现取消机制,取消多个 goroutine 的执行。


package mainimport (  "context"  "fmt"  "time")func worker(ctx context.Context, id int) {  for {    select {    case <-ctx.Done():      fmt.Printf("worker %d: received cancel signal\n", id)      return    default:      fmt.Printf("worker %d: working\n", id)      time.Sleep(1 * time.Second)    }  }}func main() {  ctx, cancel := context.WithCancel(context.Background())  for i := 0; i < 3; i++ {    go worker(ctx, i)  }  time.Sleep(3 * time.Second)  cancel()  time.Sleep(1 * time.Second)}
复制代码

96 实现使用字符串函数名,调用函数。

思路:采用反射的 Call 方法实现。


package mainimport (        "fmt"    "reflect")
type Animal struct{ }
func (a *Animal) Eat(){ fmt.Println("Eat")}
func main(){ a := Animal{} reflect.ValueOf(&a).MethodByName("Eat").Call([]reflect.Value{})}
复制代码

97 有三个函数,分别打印"cat", "fish","dog"要求每一个函数都用一个 goroutine,按照顺序打印 100 次。

此题目考察 channel,用三个无缓冲 channel,如果一个 channel 收到信号则通知下一个。


package main
import ( "fmt" "time")
var dog = make(chan struct{})var cat = make(chan struct{})var fish = make(chan struct{})
func Dog() { <-fish fmt.Println("dog") dog <- struct{}{}}
func Cat() { <-dog fmt.Println("cat") cat <- struct{}{}}
func Fish() { <-cat fmt.Println("fish") fish <- struct{}{}}
func main() { for i := 0; i < 100; i++ { go Dog() go Cat() go Fish() } fish <- struct{}{}
time.Sleep(10 * time.Second)}
复制代码

98 两个协程交替打印 10 个字母和数字

思路:采用 channel 来协调 goroutine 之间顺序。


主线程一般要 waitGroup 等待协程退出,这里简化了一下直接 sleep。


package main
import ( "fmt" "time")
var word = make(chan struct{}, 1)var num = make(chan struct{}, 1)
func printNums() { for i := 0; i < 10; i++ { <-word fmt.Println(1) num <- struct{}{} }}func printWords() { for i := 0; i < 10; i++ { <-num fmt.Println("a") word <- struct{}{} }}
func main() { num <- struct{}{} go printNums() go printWords() time.Sleep(time.Second * 1)}
复制代码

99 启动 2 个 groutine 2 秒后取消, 第一个协程 1 秒执行完,第二个协程 3 秒执行完。

思路:采用 ctx, _ := context.WithTimeout(context.Background(), time.Second*2)实现 2s 取消。协程执行完后通过 channel 通知,是否超时。


package main
import ( "context" "fmt" "time")
func f1(in chan struct{}) {
time.Sleep(1 * time.Second) in <- struct{}{}
}
func f2(in chan struct{}) { time.Sleep(3 * time.Second) in <- struct{}{}}
func main() { ch1 := make(chan struct{}) ch2 := make(chan struct{}) ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
go func() { go f1(ch1) select { case <-ctx.Done(): fmt.Println("f1 timeout") break case <-ch1: fmt.Println("f1 done") } }()
go func() { go f2(ch2) select { case <-ctx.Done(): fmt.Println("f2 timeout") break case <-ch2: fmt.Println("f2 done") } }() time.Sleep(time.Second * 5)}
复制代码

100 当 select 监控多个 chan 同时到达就绪态时,如何先执行某个任务?

可以在子 case 再加一个 for select 语句。


func priority_select(ch1, ch2 <-chan string) {        for {                select {                case val := <-ch1:                        fmt.Println(val)                case val2 := <-ch2:                priority:                        for {                                select {                                case val1 := <-ch1:                                        fmt.Println(val1)
default: break priority } } fmt.Println(val2) } }
}
复制代码

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。


没准能让你能刷到自己意向公司的最新面试题呢。


感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

发布于: 刚刚阅读数: 4
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
2025Go面试八股(含100道答案)_Go_王中阳Go_InfoQ写作社区