1、Go语言的数据类型
基本数据类型
1 整形
int8 int16 int32 int64 uint8 uint16 uint32 uint64
int uint uintptr int和uint在32位机器上4字节,在64位机器上8字节
math.MaxInt32 math.MaxInt64 math.MinInt32 math.MinInt64
math.MaxInt math.MinInt
2 浮点型 float32 float64
3 复数 (1+2i) complex32 complex64
4 布尔值 (1字节)bool (true false)
5 字符
byte uint8 一个ASCII码字符
rune int32 一个UTF-8字符
6 字符串 string
注意:只有整形和浮点型,字符串和切片能够强制转换
其他类型
1 数组
2 slice
3 map
4 channel
5 struct
6 func()
7 interface
2、GMP模型?进程、线程、协程的区别?
进程和线程是操作系统创建的,而协程是用户创建的。
协程是用户态的线程,比线程更加的轻量级,操作系统对其没有感知,
之所以没有感知是由于协程处于线程的用户栈能感知的范围,
是由用户创建的而非操作系统。
G:Goroutine,每个goroutine都有自己的栈空间,定时器,初始化的栈空间在2k左右,空间会随着需求增长。
M:内核线程,记录内核线程栈信息,当goroutine调度到线程时,使用该goroutine自己的栈信息。
P:处理器,负责调度goroutine,维护一个本地goroutine队列,M从P上获得goroutine并执行。
M代表一个内核线程,在M上有一个P和G,P是绑定到M上的,G是通过P的调度获取的,在某一时刻,一个M上只有一个G。
在P上拥有一个G队列,里面是已经就绪的G,是可以被调度到线程栈上执行的协程,称为运行队列。
P有2种类型的队列:
本地队列:本地的队列是无锁的,没有数据竞争问题,处理速度比较高。
全局队列:是用来平衡不同的P的任务数量,所有的M共享P的全局队列。
调度策略是为了尽可能地复用线程,避免频繁地创建,销毁线程。
当M没有运行的G时,从其他P的队列上获得G
当M阻塞时,将P转移到其他空闲的M。
3 、了解Golang的GC吗?
常见的GC
引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时,被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。
标记-清除:首先标记所有的可达对象为”被引用”,标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。
1.3 以前,使用的是Mark-Sweep 算法,即标记-清理算法
1.3 版本进行了一下改进,把 Sweep 改为了并行操作。
1.5 版本进行了较大改进,使用了三色标记算法。
4、channel
channel是Go语言在语言级别提供的goroutine间的通信方式
是一种进程内的通信方式。
4.1、有缓存和无缓存channel的区别?
管道分为无缓冲管道和有缓冲管道
无缓存channel,只能接受一个数据,有数据时存则阻塞,无数据时取也阻塞
有缓存channel,在满之前存和非空时取不会阻塞,在满时存和空时取会阻塞
4.2、channel的实现原理?
channel 内部就是一个带锁的环形队列。
关键变量:buf、sendx、recvx、lock
buf 指向一个环形队列。
sendx 和 recvx 分别用来记录发送、接收的位置。
然后用一个 lock 互斥锁来确保无数据竞争。
4.3、channel关闭以后,再往其发送或接收,会发生什么?
channel关闭后,发送数据会报错,读取数据不报错
有数据时读数据能正常读取,且读取操作返回true,
无数据时读取则读取到该类型的0值,并且读取返回false,
4.4、哪些操作会使channel发生panic?
情况1:往一个已经关闭的channel写数据
情况2:关闭一个nil的channel
情况3:关闭一个已经关闭的channel
4.5、channel发送和接收元素的本质是什么?
值的拷贝
5、Go slice
5.1 slice内部有什么?
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作可能影响到其他 slice 。
var s []int 创建一个 nil slice。它的长度和容量都为0。和nil比较的结果为true。
var s = make([]int, 0)创建一个empty slice
它的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 ,
空切片和 nil 比较的结果为false。
使用make函数时需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。
5.2 slice 和 array 区别?
1 slice 的底层数据是数组,
slice 是对数组的封装,它描述一个数组的片段。
两者都可以通过下标来访问单个元素。
2 数组是定长的,长度定义好之后,不能再更改。
数组长度是类型的一部分,比如 [3]int 和 [4]int 就是不同的类型。
而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。
3 作为参数传递时,参数是数组时传递的是数组的拷贝,函数内修改不影响外部
参数是切片时传递的是切片的指针的拷贝,函数内修改影响外部
5.3 slice的扩容?
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;
一旦元素个数超过1024个元素,则乘以1.25,即每次增加原来容量的四分之一。
切片的一个特点是,被截取后的数组仍然指向原始切片的底层数据。
slice的拷贝属于引用拷贝。拷贝后的slice如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,
如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
在go语言中,切片的复制其实也是值复制,但底层指针仍然指向相同的底层数据地址,因此可以理解为数据进行了引用传递。
如果没有触及原数组的容量,那么操作都是对底层数组进行操作
a:= []int{1,2,3,4,5,6,7,8,9}
b := append(a[:3], a[6:]...)
底层数组变为
1 2 3 6 7 8 9 8 9
即前面操作的地方被修改了,后面保持不变
深拷贝有两种方式
1 copy函数
2 append函数容量达到上限,触发翻倍扩容
6、Go map
1、创建
data := make(map[string]string)
data := map[string]string{"123":"123", "1232":"24424"}
2、获取元素个数
len(data)
cap(data) // 报错, map的容量是自动分配的
3、插入和修改
data["123"] = "aaa"
4、删除
delete(data, "123")
5、查找
elem, ok := data["123"] //查找不存在不会添加该key
6、赋值
data2 := data // data2 和 data 指向同一块地址
// 切片扩容会导致切片不指向同一个地址
// map扩容后仍指向同一个地址
6.1 map底层
type hmap struct {
count int // 字典的键值对个数,即len函数得到的长度
B uint8 // 实际buckets 的长度为 2 ^ B
buckets unsafe.Pointer // 存储 buckets 的地方
oldbuckets unsafe.Pointer // 迁移时oldbuckets中存放部分buckets 的数据
...
}
// buckets数组就是bmap数组
type bmap struct {
tophash [8]uint8 // 存储hash值的高8位
keys [8]keytype // 存储key
values [8]valuetype // 存储value
overflow *bmap // 溢出桶,一个桶装不下,继续向右查找
}
golang的map是hash map,是使用数组+链表的形式实现的,使用拉链法消除hash冲突。
hmap中有一个指向bmap数组的指针
bmap存放键值对,每个bmap可以存8个键值对,如果一个桶满了,
会采用链地址法解决hash 的冲突,链接到下一个溢出桶。
根据键生成64位hash值
hash值的最低B位确定放到那个桶
将tophash、key、value分别写入到tophash、keys、values数组
如果桶已满,则通过溢出桶
查找
先根据hash的低B位找到对应的桶,再根据tophash和key查找对应的元素,
如果当前桶没找到,就根据overflow继续在溢出桶查找
删除
把对应位置置为空,但并不删除空间
扩容和迁移
1、当负载因子大于6.5时,采用翻倍扩容,即使用2^(B+1)个桶
迁移时,采用低B位判断应该分配到那个桶
刚好是原来一个桶的数据分到两个桶中,原来是低B位,现在是低B+1位
多出的那一位是0时,存到原来位置,是1时,刚好是新增加的hash桶中的一个
2、当溢出桶个数过多时,采用等量扩容,不增加新桶,
而是把原始的桶中的数据更紧凑的存放,减少所用的溢出桶的个数,
因为溢出桶中有很多空间没有被利用
B<=15,已使用的溢出桶个数>=2的B次方时,引发等量扩容。
B>15,已使用的溢出桶个数>=2的15次方时,引发等量扩容。
6.2 扩容机制
map不能使用cap,实际大小与申请大小不一定一样,
会根据申请的大小自动分配,一个桶放8个数据,自动分配合适的桶个数
6.3 go的map是线程安全的吗?有没有什么线程安全的办法?
go的map不是线程安全的,
线程安全的方法:
1、加锁,即 map + Mutex 或 map + RWMutex
2、使用sync.Map
7、new和make有什么区别?
new和make都用于申请分配内存
1 new只接受一个参数,即类型,返回一个指向该类型内存地址的指针
同时 new 函数会把分配的内存置为零,也就是类型的零值。
而make只能为slice、map、channel分配内存,返回的是原始类型,
也就是slice, map和chan。
2 new 分配的空间被清零。
make 分配空间后,会进行初始化;
3 如果make用于slice类型,make函数的第2个参数表示slice的长度,这个参数必须给值。
8、函数传指针和传值有什么区别?
go语言中只有值传递,没有引用传递,传指针和传值都会拷贝
指针的拷贝和原始指针指向同一片空间,可以通过指针对内容进行修改
而对拷贝的值本身进行修改不影响外界变量的值
9、什么是内存逃逸,在什么情况下发生,原理是什么?
Go程序变量会携带一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。
如果变量通过这些校验,它就可以在栈上分配。否则就说它逃逸了,必须在堆上分配。
编译器在编译阶段进行逃逸分析,决定某个变量需要分配在栈上,还是堆上。
如果一个变量不能在栈上分配,只能在堆上分配,就说这个变量内存逃逸了。
1、在方法内把局部变量指针返回。局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
2、发送指针或带有指针的值到 channel 中。在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
3、在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。
4、因为切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。
5、在 interface 类型上调用方法。在 interface 类型上调用方法都是动态调度的,方法的真正实现只能在运行时知道。
一个经验是指针指向的数据都是在堆上分配的。
10、闭包
闭包指一个函数与其相关的引用环境组合的一个整体
AddUpper()内部内容就是一个闭包,匿名函数和n组成闭包
func AddUpper() func (int) int {
var n int = 10
return func (x int) int {
n = n + x
return n
}
}
func main() {
f := AddUpper()
fmt.Println(f(1)) // 11
fmt.Println(f(2)) // 13
fmt.Println(f(3)) // 13
}
11、go语言的并发问题
func main() {
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
}
上述代码什么都不会输出,因为主协程已经结束了,其它子协程也要跟着退出
func main() {
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
time.Sleep(time.Second)
}
上述代码会随机输出0~9,顺序是不确定的
func main() {
wait := sync.WaitGroup{}
wait.Add(10)
flag := [11]int{}
flag[0] = 1
for i := 0; i < 10; i++ {
go func(j int){
for {
if flag[j] == 1 {
fmt.Println(j)
flag[j + 1] = 1
wait.Done()
break
} else {
time.Sleep(time.Millisecond * 2)
}
}
}(i)
}
wait.Wait()
}
上述代码可以控制顺序输出0~9
func main() {
wait := sync.WaitGroup{}
wait.Add(2)
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
go func(){
defer wait.Done()
for i:=0; i<=10; i++{
<- ch1
fmt.Println(2*i)
ch2 <- 1
}
}()
go func(){
defer wait.Done()
for i:=0; i<10; i++{
<- ch2
fmt.Println(2*i+1)
ch1 <- 1
}
}()
wait.Wait()
}
上述代码两个协程交替输出0到20
十 技术栈
语言方面,Go,C++
数据库方面,mysql,redis
技能,make,docker,shell,git
三 有什么要问的
一面面试官给的忠告:不会就直接说不会
1、业务方向,技术栈
2、实习生怎么培养
3、我这次的面试表现怎么样,如果有幸进入下一轮的话,
有哪些方面需要去准备,或者需要去改进的;