【Go面试常备】
Go语言基础
1. 什么是协程
- 携程是一种用户态轻量级线程,是线程调度的基本单位
- 通常在函数前加关键字
go
就可以实现 - 一个Goroutine会以一个很小的栈启动(2KB或4KB),当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个goroutine同时启动
2. 高效拼接字符串 💖
拼接字符串的方式有:+
、fmt.Sprintf
、strings.Builder
、bytes.Buffer
、strings.Join
+
使用+
对字符串进行拼接时,需要对字符串进行遍历,计算并开辟一个新的空间来存储新的字符串fmt.Sprintf
采用了接口参数,必须要用反射获取值,有一定的性能损耗strings.Builder
使用WriteString
进行拼接,内部实现是:指针+切片,同时使用String()
返回拼接后的字符串。它是直接把[]byte
转化成string
,从而避免了变量拷贝bytes.Buffer
bytes.Buffer
是一个缓冲byte
的缓冲器。bytes.Buffer
的底层也是一个[]byte
strings.Join
strings.Join
也是基于strings.builder
来实现的,并且可以自定义分隔符,在 join 方法内调用了 b.Grow(n) 方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。
性能比较:strings.Join
≈ strings.Builder
> bytes.Buffer
> +
> fmt.Sprintf
五种拼接方式代码实现:
1 | func main(){ |
3. Go 是否支持默认参数/可选参数?
Go 语言不支持默认参数/可选参数,但是可以通过结构体参数,或者传入参数切片数组来实现。
1 | # 可以传入任意数量的整型参数,会被当作一个整型切片处理 |
1 | **结构体参数是值传递**,如果需要修改结构体参数中的值,则应该传入结构体指针! |
4. defer 执行顺序
defer
执行顺序与调用顺序相反,类似于栈的先入后出(FILO)defer
会在 return
语句后执行,但是在函数退出之前,defer
可以修改返回值
在无名返回中,go 会创建临时变量保存返回值,在所有 defer
执行完毕后将临时变量保存的返回值返回
在有名返回中,执行 return
语句时,并不会再创建临时变量保存,因此,defer 语句修改了 i,即对返回值产生了影响。
1 | // 无名返回 |
1 | // 有名返回 |
5. Go 语言中的 tag
tag 用处:
tag 可以为结构体成员提供属性。常见的:
- json:序列化或反序列化时字段的名称
- db:sqlx模块中对应的数据库字段名
- form:gin框架中对应的前端的数据字段名
- binding:搭配 form 使用,默认如果没查找到结构体中的某个字段则不报错值为空,binding 为 required 代表如果没找到则返回错误给前端
如何获取一个结构体中的 tag:
1 | // 利用反射机制 |
reflect.StructField:
6. 如何判断两个字符串切片是否相等
可以使用 reflect.DeepEqual(x, y any)
来判断两个字符串数组([]string
) 是否相等
但是由于使用了反射,效率会很底
7. 格式化输出结构体
1 | au := Author{ |
%v
:输出结构体各成员的值{jack 20}
%+v
:输出结构体各成员的名称和值{Name:jack Age:20}
%#v
:输出结构体全称和结构体各成员的名称和值test.Author{Name:"jack", Age:20}
8. 空结构体的作用
在 Go 中,我们可以使用 unsafe.Sizeof
计算出一个数据类型实例需要占用的字节数
1 | fmt.Println(unsafe.Sizeof(struct{}{})) // 0 |
我们不难发现一个空结构体实例不占用内存空间
因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。一是节省资源,二是空结构体本身就具备很强的语义,即这里不需要任何值,仅作为占位符。
- ==实现 Set==
Go 语言中本身没有 Set,一般来说我们可以使用map[string]bool
来实现一个 Set,但是事实上我们只需要集合的键而不需要集合的值,即使是将值设置为 bool 类型,也会多占据 1 个字节的空间,假设 map 中有一百万条数据,就会浪费 1MB 的空间。
因此呢,将 map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。
type Set map[string]struct{}
- ==不发送数据的通道==
有时候使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine) 执行任务,或只用来控制协程并发度。这种情况下,使用空结构体作为占位符就非常合适了。1
2
3
4
5
6
7
8
9func main() {
ch := make(chan struct{}, 1)
go func() {
<-ch
// do something
}()
ch <- struct{}{}
// ...
} - ==仅有方法的结构体==
在部分场景下,结构体只包含方法,不包含任何的字段。例如上面例子中的Door
,在这种情况下,Door
事实上可以用任何的数据结构替代。1
2
3
4
5
6
7
8
9type Door struct{}
func (d Door) Open() {
fmt.Println("Open the door")
}
func (d Door) Close() {
fmt.Println("Close the door")
}
9. go里面的int和int32是同一个概念吗?
go 语言中的 int 不能和 int32 混为一谈,int32 的大小固定为 4 字节,而 int 的大小与操作系统位数有关,如果是 32 位操作系统则 int 占 4 字节,如果是64 位操作系统则 int 占 8 字节
类型 | 大小 |
---|---|
int8 | 1 字节 |
int16 | 2 字节 |
int32 | 4 字节 |
int64 | 8 字节 |
int | 取决于操作系统位数 |
实现原理
1. init() 是什么时候执行的?
结论:在 mian()
函数执行之前执行
init()
函数是 go 初始化的一部分,由 runtime 初始化每个导入的包,初始化的顺序不是从上到下的, 而是按照解析的依赖关系,没有依赖的包最先初始化。
每个包首先初始化包作用域的变量和常量(常量优先于变量),然后执行包的 init()
函数。同一个包、同一个源文件都可以有多个 init()
函数。init()
函数没有参数和返回值,同一个包内多个 init()
函数的执行顺序没有保证。
执行顺序:**import
–> const
–> var
–>init()
–>main()
**
2. 如何知道一个对象是分配在堆上还是在栈上的?💖
Go 和 C++ 不同,Go 的局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。
如何判断是否发生了逃逸?
go build -gcflags=m xxx.go
关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。
3. 两个 interface 可以进行比较吗
在 Go 中,interface 内部实现包含了两个字段:类型 T 和值 V,interface 可以使用 ==
和 !=
进行比较,满足以下情况时两个 interface 相等:
- 两个 interface 均为 nil 时(T、V 此时均处于 unset 状态)
- 两个 interface T、V 均相同时
1 | type Stu struct { |
4. 两个 nil 可能不相等吗
有可能,interface 在运行时绑定值,只有值为 nil 的接口才是 nil,但此时与指针的 nil 不相等。
1 | var p *int = nil |
总结:两个 nil 只有在类型相同时才相等。
5. Go 垃圾回收💖
摘自:
Go常见面试题【由浅入深】2022版
参考:
Golang垃圾回收(GC)介绍 | Random walk to my blog
图解Golang垃圾回收机制! | Go 技术论坛 - LearnKu
垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障。
标记清除法
分为两个阶段:标记和清除标记阶段:从根对象出发寻找并标记所有存活的对象。
清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。
缺点是需要暂停程序STW。
三色标记法
将对象标记为白色,灰色或黑色。白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了写屏障技术,当对象新增或者更新会将其着色为灰色。
一次完整的GC分为四个阶段:
- 准备标记(需要STW),开启写屏障。
- 开始标记
- 标记结束(STW),关闭写屏障
- 清理(并发)
三色标记法+混合写屏障
基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:- GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW);
- GC期间,任何栈上创建的新对象均为黑色
- 被删除引用的对象标记为灰色
- 被添加引用的对象标记为灰色
总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得GC时间从 2s降低到2us。
6. 函数返回局部变量的指针是否安全?
和C++不同,在Go里面返回局部变量的指针是安全的。因为Go会进行逃逸分析,如果发现局部变量的作用域超过该函数则会把指针分配到堆区,避免内存泄漏。
7. 非接口的任意类型 T 都能够调用 *T
的方法吗?反过来呢?
一个 T 类型的值可以调用 *T
类型声明的方法,当且仅当 T 是可寻址的
反之,*T
可以调用 T 的方法,因为指针可以解引用
8. Go slice扩容💖
- Go <= 1.17:
如果当前容量小于 1024,则判断所需容量是否大于原来容量的两倍,如果大于,则当前容量加上所需容量,如果小于,则当前容量乘二 - Go >= 1.18:
引入了新的扩容规则:浅谈 Go 1.18.1的切片扩容机制
对于容量小的切片,按照 2 倍的速率扩容
对于容量大的切片,按照 1.25 倍的速率扩容
公式为以上两个原则提供了平滑的过度!