Golang

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、我这次的面试表现怎么样,如果有幸进入下一轮的话,
有哪些方面需要去准备,或者需要去改进的;