1 go的数据类型
基本数据类型bool、float、int 、字符、错误
复合数据类型:ptr、array、slice、map、channel、interface、func()
2 UTF-8 和 Unicode
3 如何高效地拼接字符串
拼接字符串的方式有:+ fmt.Sprintf, strings.Builder, bytes.Buffer, strings.Join
- 使用+操作符进行拼接时,会对字符串进 行遍历,计算并开辟一个新的空间来存储原来的两个字符串。
- fmt.Sprintf由于采用了接口参数,必须要用反射获取值,因此有性能损耗。
- strings.Builder用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。
- bytes.Bufferbytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte, bytes.buffer底层也是一个[]byte切片。
- strings.joinstrings.join也是基于strings.builder来实现的,并且可以自定义分隔符,在join方法内调用了b.Grow(n)方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的slice的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。
在Go语言中,strings.Builder的性能通常比bytes.Buffer好,主要有以下几个原因:
- 零拷贝:strings.Builder在内部使用了可变长度的[]byte切片来存储字符串,而bytes.Buffer使用了固定长度的[]byte切片。当进行字符串拼接时,strings.Builder可以直接修改切片中的内容,而不需要进行额外的内存分配和拷贝操作,从而避免了不必要的性能开销。
- 预分配内存:strings.Builder在初始化时会预分配一定大小的内存空间,避免了频繁的内存分配和释放操作。这样可以减少内存分配的次数,提高性能。
- 字符串连接优化:strings.Builder提供了WriteString方法,可以直接将字符串追加到内部的[]byte切片中,而不需要进行类型转换和拷贝操作。这样可以减少不必要的中间步骤,提高字符串连接的效率。需要注意的是,strings.Builder和bytes.Buffer都是用于字符串拼接和缓冲的类型,选择使用哪个取决于具体的需求和场景。如果需要频繁进行字符串拼接操作,尤其是在循环中,strings.Builder通常会更高效。而如果只是简单的缓冲操作,bytes.Buffer也可以满足需求。总结来说,strings.Builder的性能比bytes.Buffer好,主要是因为它采用了零拷贝、预分配内存和字符串连接优化等技术,避免了不必要的内存分配和拷贝操作,提高了字符串拼接的效率。
4 go里面的int和int32是同一个概念吗?
不是一个概念。go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统,int类型的大小就是4字节。如果是64位操作系统,int类型的大小就是8个字节。除此之外uint也与操作系统有关,占字节与int类型情况一致。而int后有数字的话,占用空间大小是固定的:
int8占1个字节,int16占2个字节,int32占4个字节,int64占8个字节。
5 闭包
所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
闭包=函数+引用环境,这里可以简单的理解为闭包是函数内部的匿名函数
func main() {
// 创建一个玩家生成器
generator := playerGen("码神")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp)
}
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
6 数组和切片的区别
- 切片的长度可能在执行期间发生变化 ,而数组的长度不能变化,可以把切片看成一个长度可变的数组。
- 数组作为函数参数是进行值传递的,函数内部改变传入的数组元素值不会影响函数外部数组的元素值; 切片作为函数的参数是进行的指针传递,函数内部改变切片的值会影响函数外部的切片元素值。
- 数组可以比较,切片不能比较 (对底层数组的引用)。
7 new和make的区别?
- new只用于分配内存,返回一个指向地址的指针。它为每个新类型分配一片内存,初始化为0且返回类型*T的内存地址,它相当于&T{}
- make只可用于slice,map,channel的初始化,返回的是引用。
8 如何判断 2 个字符串切片(slice) 是相等的?
reflect.DeepEqual(),但反射非常影响性能。
9 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;
- 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
- 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
- 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。
10 Go面向对象是如何实现的?
Go实现面向对象的两个关键是struct和interface。
封装:对于同一个包,对象对包内的文件可见;对不同的包,需要将对象以大写开头才是可见的。
继承:继承是编译时特征,在struct内加入所需要继承的类即可:
type A struct{}
type B struct{
A
}
多态:多态是运行时特征,Go多态通过interface来实现。类型和接口是松耦合的,某个类型的实例可以赋给它所实现的任意接口类型的变量。
11 空 struct{} 的用途
- 用map模拟一个set,那么就要把值置为struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。
- 有时候给通道发送一个空结构体,
channel<-struct{}{}
,也是节省了空间。 - 仅有方法的结构体
12 如何知道一个对象是分配在栈上还是堆上?
Go的逃逸分析是在编译器完成的;go局部变量会进行逃逸分析。如果变量离开作用域后没有被引用,则优先分配到栈上,否则分配到堆上。
go build -gcflags ‘-m -m -l’ xxx.go.
关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。
如果变量内存占用较大时,优先放在堆上;
如果函数外部没有引用,优先放在栈中;
如果变量在函数外部存在引用;必定在堆中。
13 golang的内存管理
对于微对象的分配流程:
(1)从 P 专属 mcache 的 tiny 分配器取内存(无锁)
(2)根据所属的 spanClass,从 P 专属 mcache 缓存的 mspan 中取内存(无锁)
(3)根据所属的 spanClass 从对应的 mcentral 中取 mspan 填充到 mcache,然后从 mspan 中取内存(spanClass 粒度锁)
(4)根据所属的 spanClass,从 mheap 的页分配器 pageAlloc 取得足够数量空闲页组装成 mspan 填充到 mcache,然后从 mspan 中取内存(全局锁)
(5)mheap 向操作系统申请内存,更新页分配器的索引信息,然后重复(4).
对于小对象的分配流程是跳过(1)步,执行上述流程的(2)-(5)步;
对于大对象的分配流程是跳过(1)-(3)步,执行上述流程的(4)-(5)步.
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中获取。
14 GoGC工作原理
垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障。
标记清除法
分为两个阶段:标记和清除
标记阶段:从根对象出发寻找并标记所有存活的对象。
清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。
缺点是需要暂停程序STW(stop the world)。
三色标记法
- 1.只要是创建了新对象就标记为白色
- 2.gc开始就从根节点遍历所有直接一次可达的对象,将其从白色移到灰色
- 3.遍历灰色集合,将灰色对象引用的对象从白色集合中放入灰色,同时将此次遍历的灰色对象标记为黑色
- 4.不断重复3过程,直到灰色中无对象,即只有白黑色
- 5.回收白色对象
所以三色标记最不愿发生的事:
- 一个白色对象被黑色对象引用(白挂在黑下)
- 灰色对象与它之间可达的白色对象遭到破坏(灰同时放弃了白)
同时发生时,就导致了某个对象的丢失!!!所以需要破坏上述两个条件之一!!!
- 强三色不变式:对于条件1:白挂黑下->强制性的不允许黑色应用白色对象
- 弱三色不变式:对于条件2:灰放弃白->黑引用白条件为必须存在其他灰色同时引用了白色,或者可达它的链路上游存在灰色对象
上述就就是解决办法,之后就诞生了屏障
屏障机制
屏障:额外的判断机制
- 插入写屏障 对象被引用时
- 删除写屏障 对象被删除时
插入写屏障(强三色)(在堆上启用,栈上禁用)
- 要求:在A对象引用B对象时,B对象被标记为灰色(B挂在A下,B就必须是灰色)
- 作用:上述操作保证了堆内强三色,但不会保证栈内强三色,所以在GC前对栈进行一次STW
- 不足:结束需要STW
删除写屏障(弱三色)(快照stw)
- 要求:被删除的对象,如果自身为灰色或者白色,那么会被标记为灰色(保护灰白不断)
- 作用:保证了灰色对象引用的白色对象被删除时不会被本轮gc
- 不足:回收精度低,真正的垃圾也可以活过本轮gc
三色标记和混合写屏障
- 1.GC开始优先将栈上对象全部扫描为黑色(之后不进行二次扫描,也不STW)
- 2.GC期间,任何在栈上创建的新对象均为黑色
- 3.删除对象标记为灰色
- 4.被添加的对象标记灰色
15 为什么Go语言中的字符串是不可变的?
- 内存安全:字符串的不变性也有助于防止内存安全问题,如缓冲区溢出。由于字符串内容不能被修改,因此无需担心修改操作导致的内存越界问题。
- 性能优化:字符串在Go语言中是通过字节数组实现的,且存储在只读内存段中。如果字符串是可变的,那么每次修改字符串都需要重新分配内存和复制数据,这将导致性能下降。而不可变性则避免了这种情况,使得字符串操作更加高效。
16 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 转移给其他空闲的线程执行。
17 map的key不可以是什么类型?map为什么无序?
key需要比较所以只能是基本数据类型,不可以是引用类型
map的底层数据结构机制导致他是无序的,在遍历时会做随即播种,及不确定本次遍历开始于第几个桶的第几个哈希值。
18 map的底层
type hmap struct {
count int // map中key、value总数
flags uint8 // map 是否被 goroutine 并发读写
B uint8 // 桶数组长度的指数
noverflow uint16 // map 中溢出桶的数量
hash0 uint32 //
buckets unsafe.Pointer // 桶数组
oldbuckets unsafe.Pointer // 扩容过程中老的桶数组
nevacuate uintptr // 扩容时的进度标识,index 小于 nevacuate 的桶都已经由老桶转移到新桶中
extra *mapextra // 预申请的溢出桶
}
type mapextra struct {
overflow *[]*bmap // 供桶数组 buckets 使用的溢出桶
oldoverflow *[]*bmap // 扩容流程中,供老桶数组 oldBuckets 使用的溢出桶
nextOverflow *bmap // 下一个可用的溢出桶
}
type bmap struct {
tophash [bucketCnt]uint8
}
type bmap struct {
tophash [bucketCnt]uint8 // 高八位
keys [bucketCnt]T
values [bucketCnt]T
overflow uint8
}
map扩容机制
map 扩容机制的核心点包括:
(1)扩容分为增量扩容和等量扩容;
(2)当桶内 key-value 总数/桶数组长度 > 6.5 时发生增量扩容,桶数组长度增长为原值的两倍;
(3)当桶内溢出桶数量大于等于 2^B 时( B 为桶数组长度的指数,B 最大取 15),发生等量扩容,桶的长度保持为原值;
(4)采用渐进扩容的方式,当桶被实际操作到时,由使用者负责完成数据迁移,避免因为一次性的全量数据迁移引发性能抖动.
map扩容流程
map 的扩容类型分为两类,一类叫做增量扩容,一类叫做等量扩容.
(1)增量扩容
表现:扩容后,桶数组的长度增长为原长度的 2 倍;
目的:降低每个桶中 key-value 对的数量,优化 map 操作的时间复杂度.
(2)等量扩容
表现:扩容后,桶数组的长度和之前保持一致;但是溢出桶的数量会下降.
目的:提高桶主体结构的数据填充率,减少溢出桶数量,避免发生内存泄漏.
map读流程
map 读流程主要分为以下几步:
(1)根据 key 取 hash 值;
(2)根据 hash 值对桶数组取模,确定所在的桶;
(3)沿着桶链表依次遍历各个桶内的 key-value 对;
(4)命中相同的 key,则返回 value;倘若 key 不存在,则返回零值.
map 写流程
(1)根据 key 取 hash 值;
(2)根据 hash 值对桶数组取模,确定所在的桶;
(3)倘若 map 处于扩容,则迁移命中的桶,帮助推进渐进式扩容;
(4)沿着桶链表依次遍历各个桶内的 key-value 对;
(5)倘若命中相同的 key,则对 value 中进行更新;
(6)倘若 key 不存在,则插入 key-value 对;
(7)倘若发现 map 达成扩容条件,则会开启扩容模式,并重新返回第(2)步.
19 channel底层
type hchan struct {
qcount uint // 当前 channel 中存在多少个元素
dataqsiz uint // 当前 channel 能存放的元素容量
buf unsafe.Pointer // channel 中用于存放元素的环形缓冲区
elemsize uint16
closed uint32
elemtype *_type // channel 元素类型
sendx uint // 发送元素进入环形缓冲区的 index
recvx uint // 接收元素所处的环形缓冲区的 index
recvq waitq // 因接收而陷入阻塞的协程队列
sendq waitq // 因发送而陷入阻塞的协程队列
lock mutex
}
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // 读取/写入 channel 的数据的容器
isSelect bool // 标识当前协程是否处在 select 多路复用的流程中
c *hchan // 标识与当前 sudog 交互的 chan
}
- • 判断申请内存空间大小是否越界,mem 大小为 element 类型大小与 element 个数相乘后得到,仅当无缓冲型 channel 时,因个数为 0 导致大小为 0;
- • 根据类型,初始 channel,分为 无缓冲型、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;
- • 倘若为无缓冲型,则仅申请一个大小为默认值 96 的空间;
- • 如若有缓冲的 struct 型,则一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;
- • 倘若为有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续;
- • 对 channel 的其余字段进行初始化,包括元素类型大小、元素类型、容量以及锁的初始化.
01 实现使用字符串函数名,调用函数。
思路:采用反射的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{})
}
02 有三个函数,分别打印”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)
}
03 两个协程交替打印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)
}
04 启动 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)
}
05 当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)
}
}
}