GO面试一定要看看这些面试题

本文深入探讨Go语言的核心特性,包括goroutine、channel、defer、切片、垃圾回收机制等,并详细讲解了并发编程的runtime、调度、线程与协程的区别。同时,还涵盖了一些面试常见问题,如Go语言的垃圾回收工作原理、面向对象实现、Channel和map的底层实现等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Go核心特性

1.goroutine

协程是用户态轻量级线程,它是线程调度的基本单位

使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作。这种机制在 Go语言中被称为 goroutine(协程)。

2.channel

Don’t communicate by sharing memory, share memory by communicating.

channel是go语言协程中数据通信的双向通道。但是在实际应用中,为了代码的简单和易懂,一般使用的channel是单向的。

img

缓冲机制

有缓冲

package main

import "fmt"

/*有缓冲*/
func main() {
	ch := make(chan int, 3)//缓冲区大小为3,消息个数小于等于3都不会阻塞goroutine
	ch <- 1
	fmt.Println(<-ch)//输出1
	ch <- 2
	fmt.Println(<-ch)//输出2
	ch <- 3
	ch <- 4
	fmt.Println(len(ch))//输出2,表示是channel有两个消息
	fmt.Println(cap(ch))//输出3,表示缓存大小总量为3
}

无缓冲

package main

import "fmt"

/*无缓冲*/
func main() {
	ch := make(chan int, 3)
	ch <- 2
	ch <- 1
	ch <- 3
	elem := <-ch
	fmt.Println("The first element received from channel ch:%v\n", elem)
}

3.defer

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

defer一般用于资源的释放和异常的捕捉, 作为Go语言的特性之一.

defer 语句会将其后面跟随的语句进行延迟处理. 意思就是说 跟在defer后面的语言 将会在程序进行最后的return之后再执行.

在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

4.切片

5.垃圾回收机制

6.int与int32

go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关。

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

7.interface代替继承多态

8.字母大小设置可见性

9.没有模板泛型

10.所有类型初始化默认为0

11.反射

底层实现

切片(slice)和数组的区别

切片数组
长度可以不断增加但不会减少初始化确定数组的长度
长度可变长度固定、值传递
底层实现数组每个数据的引用、指针,自身是结构体函数参数传递需要制定数组长度

make与new的区别

makenew
make只用于给slice、map、channel类(引用类型)分配内存,返回对应的类型,并非指针new用于各种类型的内存分配,返回指针,指向对应类型的零值 类型的指针需要分配内存才能赋值,与c中一样

并发编程

runtime

go运行的基础设施

  • 协调多线程调度、内存分配、GC
  • 操作系统、CPU级别操作的封装(信号处理,系统调用、寄存器操作、原子操作)、CGO
  • 性能分析等,pprof、trace、race
  • 反射实现

与python、java的runtime不同的是,在运行时与用户代码没有明显的界限,一起打包

最主要的功能是两个方面:调度、GC

调度

众所周知,操作系统内部有着线程、进程调度器,当触发阻塞、时间片用尽、硬件中断的时候,都会涉及到切换的问题

Go在此基础上实现了自己的调度器(调度Goroutine,Go内部最基本的执行单元)

线程与协程

线程:共享堆,不共享栈,其切换由操作系统控制

协程:共享堆,不共享栈,切换由程序员显式控制,可以避免上下文切换的额外消耗,可以运行在一个或多个线程上

协程的切换调度在用户空间完成,不涉及到用户空间到内核空间的切换(寄存器切换、内存数据切换、栈切换、安全检查),线程调度里面的taskstructure除了CPU信息之外,还会保存线程的私有栈以及寄存器,上下文会多一点,在POSIX中线程获得了许多进程拥有的功能,这些功能在go的调度中都是用不到的,同时也增加了开销

由于线程拥有协程,而一个线程只在一个CPU上执行,导致协程没有办法利用多核

Goroutine与协程类似,且可以实现并行

Go实现了调度器之后

  • 可以自己管理上下文切换等,减少切换成本(协程切换)
  • GC的时候需要暂停所有运行的goroutine,使得内存状态一致的时候进行垃圾回收

面试题:

=和:=的区别

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

指针的作用

个指针可以指向任意变量的地址,它所指向的地址在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(){}

持久化是怎么做的

所谓持久化就是将要保存的字符串写到硬盘等设备。

  • 最简单的方式就是采用ioutil的WriteFile()方法将字符串写到磁盘上,这种方法面临格式化方面的问题。

  • 更好的做法是将数据按照固定协议进行组织再进行读写,比如JSON,XML,Gob,csv等。

  • 如果要考虑高并发高可用,必须把数据放入到数据库中,比如MySQL,PostgreDB,MongoDB等。

    磁盘——》固定协议——》数据库(高并发高可用)

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

标记清楚法

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

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

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

缺点是需要暂停程序STW。

三色标记法

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

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

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

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

三色标记法+混合写屏障

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

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

GMP模型

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

go进行调度过程:

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

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

  • 进入调度循环:

    • 找到一个合适的G
    • 执行G,完成以后退出

Go面向对象是如何实现的

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

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

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

type A struct{}
type B struct{
A
}

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

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

Channel底层实现

map的底层实现

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

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

hmap结构体如图:

type hmap struct {
    count     int //map元素的个数,调用len()直接返回此值
    
    // map标记:
    // 1. key和value是否包指针
    // 2. 是否正在扩容
    // 3. 是否是同样大小的扩容
    // 4. 是否正在 `range`方式访问当前的buckets
    // 5. 是否有 `range`方式访问旧的bucket
    flags     uint8 
    
    B         uint8  // buckets 的对数 log_2, buckets 数组的长度就是 2^B
    noverflow uint16 // overflow 的 bucket 近似数
    hash0     uint32 // hash种子 计算 key 的哈希的时候会传入哈希函数
    buckets   unsafe.Pointer // 指向 buckets 数组,大小为 2^B 如果元素个数为0,就为 nil
    
    // 扩容的时候,buckets 长度会是 oldbuckets 的两倍
    oldbuckets unsafe.Pointer // bucket slice指针,仅当在扩容的时候不为nil
    
    nevacuate  uintptr // 扩容时已经移到新的map中的bucket数量
    extra *mapextra // optional fields
}

里面最重要的是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。

GO interface的实现

go的接口由两种类型实现ifaceeface。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的公共描述,类似的还有maptypearraytypechantype…其都是各个结构的公共描述,可以理解为一种外在的表现信息。interfaetype和type唯一确定了接口类型,而hash用于查询和类型判断。fun表示方法集。

  • eface

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

type eface struct {
	_type *_type
	data  unsafe.Pointer
}

Go的调试和分析工具有哪些

go cover 测试代码覆盖率

go doc 用于生成go文档

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

race 用于竞争检测

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值