当前位置: 首页 > news >正文

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

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

01 = 和 := 的区别?

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

02 指针的作用

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

  • 获取变量的值
 import fmtfunc main(){a := 1p := &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 mainimport ("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.Sprintfret := fmt.Sprintf("%s%s%s", a[0],a[1],a[2])//方式3:strings.Buildervar sb strings.Buildersb.WriteString(a[0])sb.WriteString(a[1])sb.WriteString(a[2])ret := sb.String()//方式4:bytes.Bufferbuf := new(bytes.Buffer)buf.Write(a[0])buf.Write(a[1])buf.Write(a[2])ret := buf.String()//方式5:strings.Joinret := 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]int
if _, 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 := 0for _, num := range nums {total += num}return total
}

10 defer 的执行顺序

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

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

func test() int {i := 0defer func() {fmt.Println("defer1")}()defer func() {i += 1fmt.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 = 0defer func() {i += 1fmt.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 reflect
type 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).Names, _ := 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 MiBGiBTiBPiBEiB
)

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)) // 3if _, 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) // falsefmt.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.WaitGroup
ch := 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 = 1
var b uint = 2
fmt.Println(a - b)

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

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

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

func init() {...
}

init函数非常特殊:

  • 初始化不能采用初始化表达式初始化的变量;
  • 程序运行前执行注册
  • 实现sync.Once功能
  • 不能被其它函数调用
  • init函数没有入口参数和返回值:
  • 每个包可以有多个init函数,每个源文件也可以有多个init函数
  • 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
  • 不同包的init函数按照包导入的依赖关系决定执行顺序。

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

type GobCodec struct{conn io.ReadWriteCloserbuf *bufio.Writerdec *gob.Decoderenc *gob.Encoder
}type Codec interface {io.CloserReadHeader(*Header) errorReadBody(interface{})  errorWrite(*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导致泄漏
    1. 当在一个长slice上再次使用切片操作创建一个新的slice时,新的slice会共享原长slice的底层数组,导致原长slice无法被释放。
    2. 这种情况下,新的slice会持有原长slice的底层数组的引用,即使新的slice被丢弃,原长slice的底层数组也无法被释放,造成内存泄漏。
    3. 避免内存泄漏的方法是使用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<-1
q<-2
q<-3

解决方法,采用select

func main() {q := make(chan int, 2)q <- 1q <- 2select {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不一样,底层实现是哈希表,包括两个部分:hmapbucket

里面最重要的是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         // chanelem 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  *itabdata unsafe.Pointer
}

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

  • itab
type itab struct {inter *interfacetype //此属性用于定位到具体interface_type *_type //此属性用于定位到具体interfacehash  uint32 // copy of _type.hash. Used for type switches._     [4]bytefun   [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 *_typedata  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. 基本数据类型
    • 布尔型(bool):取值为 truefalse
    • 整数型:包括有符号整数(intint8int16int32int64)和无符号整数(uintuint8uint16uint32uint64),其中 intuint 的宽度取决于平台(32位或64位)。
    • 浮点型float32(32位)和 float64(64位),后者是默认浮点类型。
    • 复数型complex64(实部和虚部均为 float32)和 complex128(实部和虚部均为 float64)。
    • 字符串型(string):UTF-8编码的不可变文本,本质是字节序列。
    • 字节型byteuint8 的别名)和 runeint32 的别名,用于存储Unicode字符)。
  2. 复合数据类型
    • 数组(array):固定长度的相同类型元素序列,长度是类型的一部分(如 [5]int)。
    • 切片(slice):可变长度的数组封装,支持动态扩容(如 []int)。
    • 映射(map):键值对集合,键需为相同类型(如 map[string]int)。
    • 结构体(struct):用户自定义的聚合类型,可包含不同类型字段(如 type Point struct { X, Y int })。
    • 指针(pointer):存储变量内存地址(如 *int)。
    • 函数(function):可作为类型传递(如 func(a int) int)。
    • 接口(interface):定义方法集合,实现多态(如 type Reader interface { Read() })。
    • 通道(channel):用于协程(goroutine)间通信(如 chan int)。
  3. 特殊数据类型
    • 空值(nil):表示无值,常用于指针、切片等。
    • 错误(error):内置接口类型,用于错误处理。

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

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

  1. 显式类型转换(T(v))
    • 语法:目标类型(表达式),例如将 int 转为 float64f := float64(42)
    • 适用场景:数值类型间转换(如整型与浮点型),但需注意精度丢失(如 floatint 会截断小数)。
  2. 使用 strconv 包处理字符串转换
    • 字符串转基本类型:如 strconv.Atoi("123") 将字符串转为 int,返回值和错误信息。
    • 基本类型转字符串:如 strconv.Itoa(123)strconv.FormatFloat(3.14, 'f', 2, 64)
  3. 类型断言(针对接口类型)
    • 语法:value, ok := interfaceVar.(具体类型),例如从空接口 interface{} 提取字符串:s, ok := i.(string)
    • 失败时 okfalse,若忽略 ok 可能触发 panic
  4. 强制转换(需谨慎)
    • 使用 unsafe.Pointer 实现跨类型指针转换,如 newPtr := (*T2)(unsafe.Pointer(ptr)),但可能引发安全问题。

注意事项:

  • 不兼容类型(如 float[]byte)会编译失败。
  • 字符串与 []byte 可直接互转:b := []byte(s)s := string(b)

64 constvar的区别是什么?

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

  1. 可变性
    • var 声明变量,值可修改(如 var x int = 10; x = 20)。
    • const 声明常量,值不可修改(如 const Pi = 3.14; Pi = 3.14 会编译错误)。
  2. 初始化要求
    • const 必须在声明时赋值,且值需为编译时可确定的表达式(如字面量或常量运算)。
    • var 可不初始化(默认为零值,如 int 的零值为 0),或后续赋值。
  3. 类型处理
    • const 支持无类型常量(如 const a = 42),类型由上下文推断;var 需显式或隐式指定类型。
    • 常量可参与不同类型运算(如 const b = 1.5 + 2),而变量需显式转换。
  4. 作用域与批量声明
    • 两者均支持批量声明(如 var (x int; y string)const (A=1; B=2))。
    • const 常用于枚举(结合 iota),如 const (Sunday = iota; Monday)

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

65 select语句有什么用途?

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

  1. 多路复用(监听多个channel)
    • 同时等待多个channel的读写操作,随机选择一个就绪的case执行(如 case <-ch1case ch2 <- data),避免阻塞。
  2. 超时控制
    • 结合 time.After 实现超时机制,例如:select { case <-dataChan: ...; case <-time.After(3*time.Second): ... }
  3. 非阻塞操作
    • 添加 default 分支实现非阻塞,当无channel就绪时立即执行 default(如用于检查channel状态)。
  4. 程序退出与错误处理
    • 监听退出信号channel(如 <-doneChan),实现优雅终止。
    • 检测channel关闭(如 case v, ok := <-chok 标识状态)。

注意事项:

  • select 的case必须是channel操作,且随机选择就绪case以保证公平性。
  • default 且无channel就绪时,会阻塞直到至少一个就绪。

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

66 sync.Pool有什么用途?

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

  1. 减少内存分配与GC压力
    • 复用短期对象(如缓冲区、解析器状态),避免频繁分配和垃圾回收,提升性能(尤其在高并发场景)。
  2. 实现原理
    • 层级缓存:每个处理器(P)维护私有队列(private)和共享环形队列(shared),优先本地存取。
    • GC敏感性:池中对象可能在两次GC间被回收,防止内存泄漏。
    • 并发安全:通过无锁设计减少竞争(如 Get()Put() 方法)。
  3. 适用场景
    • 高频创建/销毁对象(如JSON解码中的临时结构体)。
    • 资源复用(如数据库连接池),但需注意对象重置(Put() 前清除状态)。

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

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

  1. IDE与编辑器
    • GoLand:JetBrains出品,提供智能补全、调试、重构等专业功能,适合大型项目。
    • Visual Studio Code (VS Code):轻量级,通过Go插件(如Go Extension Pack)支持语法高亮、调试和测试,免费且跨平台。
  2. 包管理与构建
    • Go Modules:官方依赖管理工具(Go 1.11+),自动处理版本和依赖冲突(如 go mod init)。
    • Go Build/Test:内置编译和测试工具(如 go build 编译,go test 运行测试)。
  3. 调试与分析
    • Delve:专为Go设计的调试器,支持断点、变量检查,可集成到IDE。
    • Pprof:性能分析工具,用于CPU和内存剖析。
  4. 代码质量工具
    • Golint:静态代码分析,检查风格和潜在错误。
    • GORM:ORM库,简化数据库操作。

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

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

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

  1. 单元测试工具:
    • testing 包:Go 内置的标准库,提供基础断言和测试函数结构,例如 TestXxx(t *testing.T)
    • testify/assert:第三方库,扩展断言功能(如 Equal()Nil()),支持自定义错误消息,简化测试代码。
  2. 代码覆盖率工具:
    • go test -cover:生成代码覆盖率报告,-coverprofile 输出详细数据文件,结合 go tool cover -html 可视化分析。
    • go-cover:第三方工具,提供更丰富的 HTML 报告和集成支持。
  3. 基准测试工具:
    • testing/benchmark:内置基准测试框架,通过 BenchmarkXxx(b *testing.B) 测量代码性能,支持 -benchmem 分析内存分配。
    • go-benchmark:第三方库,简化性能测试编写和结果解析。
  4. 集成测试与 Mock 工具:
    • gocheck/echo/test:用于 HTTP 服务集成测试,模拟请求和验证响应。
    • Mock 工具:
      • gock:HTTP 请求模拟,录制和重放网络交互。
      • go-sqlmock:数据库模拟,替代真实 SQL 驱动进行测试。
      • miniredis:Redis 内存模拟,用于无依赖测试。
  5. BDD 测试框架:
    • Ginkgo/Gomega:行为驱动开发(BDD)风格框架,提升测试可读性。

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

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

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

  1. 内置工具:
    • fmt/log 包:通过 Printlnlog.Println 输出变量值和执行路径,适合快速定位简单问题。
    • go test:运行单元测试并输出详细日志(-v 参数),结合 -run 过滤特定测试用例。
  2. 外部调试器:
    • Delve (dlv):
      • 功能:设置断点(break)、单步执行(next/step)、查看变量(print)。
      • 使用:dlv debug main.go 启动交互会话,或集成到 CI/CD 流程。
  3. IDE 集成调试:
    • Goland:内置调试支持,可视化断点管理和堆栈跟踪。
    • VSCode:通过 Go 插件和 launch.json 配置调试环境,支持远程调试。
  4. 性能分析工具:
    • pprof:分析 CPU 和内存瓶颈,生成火焰图(go test -cpuprofile)。
    • 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)。

  2. 操作变量值:

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

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

  3. 动态调用方法:

    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。

  2. 结合 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 状态持续)  
    
  2. 词法作用域:闭包在定义时捕获变量引用(非值拷贝),循环中使用需避免共享变量问题。

主要应用场景

  • 回调与异步处理:如 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. 长度固定性:
    • 数组的长度在声明时确定,无法动态改变(如 var arr [3]int)。
    • 切片的长度可变,支持通过 append 动态扩展(如 slice := []int{1, 2, 3})。
  2. 内存分配与传递:
    • 数组是值类型,赋值或传参时会复制整个数据,内存开销大。
    • 切片是引用类型,底层包含指向数组的指针、长度和容量,传递时仅复制结构体(24字节),共享底层数组。
  3. 容量机制:
    • 数组无容量概念,大小固定。
    • 切片有容量(cap),表示底层数组可容纳的元素上限,支持自动扩容。
  4. 初始化方式:
    • 数组需显式指定长度(如 [3]int{1,2,3})。
    • 切片可通过 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. 处理冲突:若当前桶无匹配,遍历溢出桶链表直至找到或结束。

⏱️ 时间复杂度:平均 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
  • 解决方案:
    1. 互斥锁:用 sync.Mutexsync.RWMutex 包裹操作(读多写少时用读写锁)。
    2. sync.Map:专为并发场景设计,适合读多写少或键值稳定的场景(如配置管理)。
    3. 分区锁:对大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. 应用场景:

    • 资源释放(如文件关闭 defer file.Close())。
    • 锁解锁(defer mu.Unlock())。
    • 捕获panic(defer func() { recover() }())。
  5. 返回值修改:若函数返回具名变量,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 main
import ("fmt""sync"
)
type Singleton struct{}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {once.Do(func() {instance = &Singleton{}})return instance
}
func main() {a := GetInstance()b := GetInstance()fmt.Println(a == b) // 输出 true
}

92 读写锁实现

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

package main
import ("fmt""sync""time"
)
type Data struct {sync.RWMutexvalue 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.WaitGroupfor 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 main
import ("fmt""sync""time"
)
type MessageQueue struct {queue chan intwg    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 main
import ("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 main
import ("context""fmt""time"
)
func worker(ctx context.Context, id int) {for {select {case <-ctx.Done():fmt.Printf("worker %d: received cancel signal\n", id)returndefault: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 main
import ("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 mainimport ("fmt""time"
)var dog = make(chan struct{})
var cat = make(chan struct{})
var fish = make(chan struct{})func Dog() {<-fishfmt.Println("dog")dog <- struct{}{}
}func Cat() {<-dogfmt.Println("cat")cat <- struct{}{}
}func Fish() {<-catfmt.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 mainimport ("fmt""time"
)var word = make(chan struct{}, 1)
var num = make(chan struct{}, 1)func printNums() {for i := 0; i < 10; i++ {<-wordfmt.Println(1)num <- struct{}{}}
}
func printWords() {for i := 0; i < 10; i++ {<-numfmt.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 mainimport ("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")breakcase <-ch1:fmt.Println("f1 done")}}()go func() {go f2(ch2)select {case <-ctx.Done():fmt.Println("f2 timeout")breakcase <-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)}}}

欢迎关注 ❤

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

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

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

相关文章:

  • 【Spark征服之路-2.2-安装部署Spark(二)】
  • 41道Django高频题整理(附答案背诵版)
  • 泊松融合的介绍和OpenCV教程
  • Kaggle-Predicting Optimal Fertilizers-(多分类+xgboost)
  • 数学运算在 OpenCV 中的核心作用与视觉效果演示
  • 【个人笔记】数据库原理(西电)
  • 半监督学习:低密度分离假设 (Low-Density Separation Assumption)
  • Devops系列---python基础篇二
  • STM32 智能小车项目 两路红外循迹模块原理与实战应用详解
  • 厂区能源监控系统:网关赋能下的高效能源管理与环保监测
  • 「Java EE开发指南」如何使用MyEclipse在Web项目中用Web Fragments?
  • 数学复习笔记 27
  • 基于RK3568的多网多串电力能源1U机箱解决方案,支持B码,4G等
  • 【Elasticsearch】Elasticsearch 核心技术(二):映射
  • DeepSeek 赋能智能养老:情感陪伴机器人的温暖革新
  • Python数据类型与运算符全解析-Python数据挖掘可视化分析
  • AI系统负载均衡与动态路由
  • bootstrap:点击回到顶部 超简单
  • halcon c# 自带examples报错 Matching
  • Dubbo Logback 远程调用携带traceid
  • 照明公司网站制作/网站如何快速收录
  • 临安农家乐做网站/百度seo引流怎么做
  • 广州网站建设网站/外包公司有哪些
  • 有九类商标可以做网站名吗/做网站需要什么条件
  • 电影网站开发/seo排名点击软件推荐
  • 免费做网站. 优帮云/seo推广公司排名