Golang面经
一、基础
1.make与new的区别
相同点:都是给变量分配内存
不同点:
- 作用变量类型不同,new给string,int,bool等分配内存,make给切片,map,channel分配内存;
- 返回类型不一样,new返回指向变量的指针,make返回变量本身;
- new 分配的空间被初始化为零值。make 分配空间后会进行初始化(切片会被初始化为空切片、map会被初始化为空map、channel会被初始化为带有指定容量的空channel)。
2.go 多个 defer 的顺序,defer 在什么时机会修改返回值?
作用:defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic;
避坑指南:defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。
执行顺序:多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中
defer,return,return value(函数返回值) 执行顺序:先进行返回值赋值,再执行 defer,最后执行 return 指令。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针
3.讲讲 Go 的 defer 底层数据结构和一些特性?
每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单连表,保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。
defer 的规则总结:
- 延迟函数的参数是 defer 语句出现的时候就已经确定了的。
- 延迟函数执行按照后进先出的顺序执行,即先出现的 defer 最后执行。
- 延迟函数可能操作主函数的返回值。
- 申请资源后立即使用 defer 关闭资源是个好习惯。
4.for range 的时候它的地址会发生变化么?
在 for a,b := range c 遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程。解决办法:在每次循环时,创建一个临时变量。
5.uint 类型溢出问题
如果超过这些最大值最小值,会“回绕”并从 0 或最大值(依据操作系统)继续计数
6.能介绍下 rune 类型吗?
- 相当int32,golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8
- byte 等同于int8,常用来处理ascii字符
- rune 等同于int32,常用来处理unicode或utf-8字符
7.golang 中解析 tag 是怎么实现的?反射原理是什么?
Go 中解析的 tag 是通过反射实现的,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力或动态知道给定数据对象的类型和结构,并有机会修改它。反射将接口变量转换成反射对象 Type 和 Value;反射可以通过反射对象 Value 还原成原先的接口变量;反射可以用来修改一个变量的值,前提是这个值可以被修改;tag是啥:结构体支持标记,name string json:name-field
就是 json:name-field
这部分
8.讲讲 Go 的 select 底层数据结构和一些特性
go 的 select 为 golang 提供了 IO 多路复用机制,用于检测是否有读写事件是否 ready。linux 的系统 IO 模型有 select,poll,epoll,go 的 select 和 linux 系统 select 非常相似。
select 结构组成主要是由 case 语句和执行的函数组成 select 实现的多路复用是:每个线程或者进程都先到注册和接受的 channel(装置)注册,然后阻塞,然后只有一个线程在运输,当注册的线程和进程准备好数据后,装置会根据注册的信息得到相应的数据。
select 的特性
- select 操作至少要有一个 case 语句,出现读写 nil 的 channel 该分支会忽略,在 nil 的
channel上操作则会报错 - select 仅支持管道,而且是单协程操作
- 每个 case 语句仅能处理一个管道,要么读要么写
- 多个 case 语句的执行顺序是随机的
- 存在 default 语句,select 将不会阻塞,但是存在 default 会影响性能。
9.单引号,双引号,反引号的区别?
- 单引号,表示byte类型或rune类型,对应 uint8和int32类型,默认是 rune 类型。byte用来强调数据是rawdata,而不是数字;而rune用来表示Unicode的code point。
- 双引号,才是字符串,实际上是字符数组。可以用索引号访问某字节,也可以用len()函数来获取字符串所占的字节长度。
- 反引号,表示字符串字面量,但不支持任何转义序列。字面量 raw literal string的意思是,你定义时写的啥样,它就啥样,你有换行,它就换行。你写转义字符,它也就展示转义字符。
10.context 结构是什么样的?context 使用场景和用途?
Go 的 Context 的数据结构包含 Deadline,Done,Err,Value,Deadline 方法返回一个time.Time,表示当前 Context 应该结束的时间,ok 则表示有结束时间,Done 方法当 Context 被取消或者超时时候返回的一个 close 的 channel,告诉给 context 相关的函数要停止当前工作然后返回了,Err 表示 context 被取消的原因,Value 方法表示 context 实现共享数据存储的地方,是协程安全的。context 在业务中是经常被使用的,
应用 :
1:上下文控制,2:多个 goroutine 之间的数据交互等,3:超时控制:到某个时间点超时,过多久超时。
11.泛型
泛型使得Go语言更加灵活和强大,通过参数化类型,你可以编写更加通用和可复用的代码。泛型函数和泛型类型是泛型在Go中的两种主要应用方式,而类型约束则用来限制类型参数的范围,确保类型安全。掌握泛型将大大提高你的Go编程效率和代码质量。
type Number interface {int | int64 | float64
}func Add[T Number](a, b T) T {return a + b
}
二、切片与数组
1.数组和切片的区别
相同点:
- 只能存储一组相同类型的数据结构
- 都是通过下标来访问,并且有容量长度,len 获取长度, cap 获取容量
不同点:
- 数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩容
- 数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变
2.切片底层结构
切片(Slice)在 Go 语言中,有一个很常用的数据结构,切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。并发不安全。
切片是一种引用类型,它有三个属性:指针,长度和容量
type slice struct {array unsafe.Pointerlen intcap int
}
1.指针: 指向 slice 可以访问到的第一个元素。
2.长度: slice 中元素个数。
3.容量: slice 起始元素到底层数组最后一个元素间的元素个数。
3.切片扩容机制
扩容时机:append时
1.18版本以前
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
1.18版本以后
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
- 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是 newcap + 3*threshold (threshold=256),直到新容量大于期望容量;
- 并不是严格按照策略,还会进行内存对齐