go后端面试题,你值得拥有!

2019/08/05 go

GO后端面试题

一、自我介绍

自己的过往工作经历

二、项目介绍

做了什么项目?遇到什么问题?怎么解决的

三、GO相关面试题

1、map排序如何有序?

第一种方式:
1、可以先定义key对应类型的切片,然后将map中的key存入到切片中,接着对切片排序。
2、循环切片,通过map访问切片值(key),这样就有序了

func main() {
	m := map[int]string{
		1001: "张三",
		9012: "李四",
		1010: "王武",
		1011: "订单",
		1000: "大男",
		7610: "李四",
		8910: "李四",
	}

	var s = make([]int, 0, len(m))

	for i := range m {
		s = append(s, i)
	}
	sort.Ints(s)

	for _, val := range s {
		fmt.Println(m[val])
	}

}


第二种方式:
1、定义结构体用于存储map中的key和value
2、自定义排序规则,重新排序

type Dict struct {
	Key int
	Val string
}

func main() {
	m := map[int]string{
		1001: "张三",
		9012: "李四",
		1010: "王武",
		1011: "订单",
		1000: "大男",
		7610: "李四",
		8910: "李四",
	}

	var dicts = make([]Dict, 0, len(m))
	for i := range m {
		dicts = append(dicts, Dict{
			Key: i,
			Val: m[i],
		})
	}

    // 自定义排序的规则, < 表示从小到大进行排序
	sort.Slice(dicts, func(i, j int) bool {
		
		return dicts[i].Key < dicts[j].Key
	})

	for _, v := range dicts {
		println(v.Key, v.Val)

	}

}

2、map的底层实现?

1、map底层是用hash表实现的,一个hash表有多个bucket,而每个bucket保存map中的一个或多组键值对
2、map底层结构是hmap
type hmap struct {
	count      int            //当前保存元素的个数
	B          uint           //bucket 数组大小
	buckets    unsafe.Pointer //bucket数组,数组长度为2^B
	oldbuckets unsafe.Pointer //老旧bucket数组,用于扩容
}
每个元素经过hash运算后会落到每个桶中

3、bucket数据结构
type bmap struct {
	tophash  [8]uint8 //存储hash值的高8位
	data     []byte   //key value  数据:key/key/key/key/key..../value/value/value/value
	overflow *bmap    //溢出bucket的地址
}

1)hash值相同的键存入当前桶的时候,会将hash值高8位存储在该数组中,以便后续匹配
2)data存放的是key-value数据,存放顺序是key/key/key/key/key..../value/value/value/value ,
   如此存放是为了节省字节对齐带来的空间浪费
4、hash冲突
当有两个或者以上数量的键被hash到了同一个桶时,我们称这些键发生了冲突,go使用链地址法来解决键冲突。
由于每个bucket可以存放8个键值对,所以同一个bucket存放超过8个键值对时就会再创建一个键值对,用类似
链表的方式将bucket连接起来。

5、负载因子
负载因子用于衡量一个hash表冲突的情况,公示为
负载因子 = 键数量/bucket 数量

负载因子过小或过大都不理想
负载因子过大,说明冲突严重,存储效率低

负载因子过小,可能预分配的空间空间太大,也可能是大部分元素被删除造成的。随着元素不断的添加到map中
负载因子会逐渐升高

负载因子过大,需要申请更多的bucket,并对所有的键值对重新组织,使其均匀的分布到这些bucket中,这个过程叫rehash.
go语言中map的负载因子达到6.5会触发rehash。

6、扩容条件
负载因子大于6.5
overflow的数量大于2^15

7、增量扩容
当负载因子过大时,就会新建一个bucket数组,新的bucket数组的长度是原来的2倍,然后旧的bucket数组中的数据搬迁到新的
bucket数组中。
考虑到map存储了数亿亿计的键值对,那么一次搬迁会造成较大的延迟,go采用逐步搬迁策略,每次访问map的时会触发一次搬迁,每次
搬迁两个键值对

8、等量扩容
就是重新搬迁,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存储速度。

9、增删改查

查找过程
1)根据key值计算hash值
2)取hash值低位与hmap.B取模来确定bucket的位置
3)取hash值高位,在tophash数组中查询定位到key高八位位置
4)在data数组中通过key在tophash高8位对应的位置找到对应的key
5) 比较要找的key是否在data数组中,如果存在,就取key对应的value
6) 如果没有找到,就继续从溢出的bucket中查找
7)如果当前map处于搬迁的过程,那么优先从oldbuckets 数组中查找,不再从新的buckets数组中查找
8) 如果查询不到,那么也不会返回nil,而是会返回相应类型的零值。

  

3、说说GPM?

调度模型

1、线程模型
线程可分为用户线程和内核线程,用户线程由用户创建,同步和销毁,内核线程由内核来管理,根据用户线程管理方式不同
分为三种线程模型。
一种是N:1的线程模型,也就是说N个线程模型运行在一个内核线程中,优点是用户线程上下文切换快,缺点是无法充分利用CPU的
多核算力。
另一种是1:1的线程模型,每个用户线程对应一个内核线程,充分利用cpu算力,缺点上下文切换比较慢。

Go实现的是M:N模型,也就是前两种模型的结合,M个用户线程运行在N个内核线程中


2、调度器模型
M machine:  工作线程,它由操作系统调度
P processor: 包含运行go代码的重要资源,也有调度goroutine的能力
G goroutine: GO协程,每个go关键字都会创建一个协程

全局队列(Global Queue):存放等待运行的 G。
P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。

资料

4、GC垃圾回收

1、1.3 stw 标记清除
2、1.5 三色标记
3、1.8 混合写屏障,栈无需stw

资料

5、内存管理

go语言也实现了内存分配器,原理与tcmalloc类似,简单的说维护了一块全局内存,每个处理器P维护一小块私有内存
私有内存不足的时候,再从全局申请

资料

6、关闭管道还能读取到值么?

有缓冲管道:如果数据可以读取到具体数据,没有数据,只能读取到管道类型对应零值。
无换筹管道:只能读取到管道类型对应零值。

7、内存逃逸

1、指针逃逸
我们知道Go可以返回局部变量指针,这其实是一个典型的变量逃逸案例。

type Student struct {
	Name string
}

func Say() *Student {
	stu := Student{}
	stu.Name = "张三"
	return &stu
}

2、栈空间不足逃逸
很多函数的参数为interface 类型,比如 fmt.Println(a ...interface{}),编译期间很难确定其参数的具体类型,也会产生逃逸

func slice() {
	s := make([]int, 10000, 10000)

	for index, _ := range s {
		s[index] = index
	}
}
   
3、动态类型逃逸

s := "sbc"
fmt.Println(s)



4、闭包引用对象逃逸

func bibao() func() int {
	a, b := 100, 10

	return func() int {
		return a + b
	}
}

该函数返回一个闭包,闭包引用了函数的局部变量a和b,使用时通过该函数获取闭包


小结:
栈上分配内存比在堆中分配内存有更高的效率;
栈上分配的内存不需要GC处理;
逃逸分析的目的是决定分配地址是栈还是堆;
逃逸分析在编译阶段完成;

8、切片扩容原理


9、如何降低GC


10、字符串拼接,哪种性能最好?


11、互斥锁的正常模式和饥饿模式


12、defer的使用介绍

defer不仅可以用于资源释放,也可以用于流程控制和异常处理,但defer关键字只能用于函数或函数调用。
defer关键字后接一个匿名函数
defer func(){
    fmt.Println("hello world!")
}

defer关键字后接一个函数调用:
file,err :=os.open(name)
if err != nil {

   return nil,err
}
defer file.Close()

1、使用场景
1)释放资源
m.mutex.Lock()
defer m.mutex.Unlock()
defer 常用于文件句柄、数据库连接,停止定时器 Ticker 以及关闭管道等资源清理场景 。

2)流程控制
var wg wait.group
defer wg.Wait()
defer 也用于控制函数的执行顺序,比如配合wait.Group实现等待携程的退出。 

3)异常处理
defer 常用于处理异常,与recover配合可以消除panic,另外recover只能用于defer函数。


2、行为规则
1)规则一:延迟函数的参数在defer语句出现的时候就已经确定了
官方给出的例子如下图所示:
func f(){
    i :=0
    defer fmt.Println(i)
    i++
    return
}

defer语句中的fmt.Println()参数i值在defer出现时就已经确定了,实际上是复制了一份。后面对变量i的修改
不会影响fmt.Println()函数的执行,仍然打印0。

2) 规则二:延迟函数按照先进后出的顺序执行,也就是说在函数中,最先出现的defer最后执行。


3)规则三:延迟函数可能影响主函数的返回值
定义defer的函数可能有名字的返回值,也可能是没名字的返回值!延迟函数可能会影响到返回值。

那如果想要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回就够了。
(1)有个事实必须要清楚,关键字return不是一个原子操作,实际上return只代表汇编指令ret,即跳转程序的执行。
    比如return i,实际上是分两步执行,即先将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机介于返回值
    入栈与跳转之间。这意味着defer是有机会操作返回值的。
    
    那举个例子:
    func deferResult() (res int){
        i :=1
        
        defer func(){
            res++
        }
    
        return i    
    }
    该函数的return语句可以拆分成下面两行:
    result = i
    return
    前面说过延迟函数在return之前执行
    result = i //1
    result++ //2
    return
    
    所以上面的返回值是 result++ 的值
    
   (2)主函数有匿名函数返回值,则返回字面值。
    一个主函数拥有一个匿名返回值,比如说1,2,3,"hello"等,那么defer是无法操作返回值的
    比如下面一个返回字面值的函数:
    func f() int{
        var i int
        defer func(){
            i++
        }
        return 1
    }
    
    上面的return语句直接把1写入栈中作为返回值,延迟函数无法操作该返回值,索引就无法影响到该返回值。
    
   (3)主函数拥有匿名返回值,返回变量。
    一个主函数拥有一个匿名返回值,返回本地变量或者局部变量,这种情况下defer可以引用返回值,但不会改变返回值。
    一个返回本地变量的函数如下:
    func f() int{
      var i int
      defer func (){
        i++
      }
      return i
    
    }
    
    上面的函数返回一个局部变量,同时defer函数也会操作这个局部变量。对于一个匿名返回值来说,可以假定一个匿名变量
    来存储该返回值,那么上面的返回值可以拆分以下步骤:
    
    anony = i
    i++
    return
    
    由于i是整数,会将值复制给anony,所以defer语句中的修改不会函数返回值造成影响

13、sync.map底层实现?

四、Mysql相关面试题

1、B+tree索引为什么快?


2、索引的优化?


3、分库分表?


五、Redis相关面试题

用户签到场景设计?


六、算法相关面试题

1、跳跃表实现

2、成员分组问题

题目一:公司有n个组,>=5,每组人数相同,>=2人,需要进行随机的组队吃饭。

要求:

1. 两两一队或三人一队,不能落单

2. 两人队、三人队各自的队伍数均不得少于2

3. 一个人只出现一次

4. 队伍中所有人不能来自相同组

5. 随机组队,重复执行程序得到的结果不一样,总队伍数也不能一样

6. 注释注释注释

注:要同时满足条件1-6,

七、网络相关面试题

1、epoll,select有什么区别



八、用户认证相关

1、基于cookie,session认证


2、jwt认证


3、oauth2.0认证


九、项目相关问题

1、如何设计一个秒杀系统


2、mysql,redis双写一致性如何保证?


3、大转盘抽奖如何设计?


Search

    Table of Contents