使用go语言的好处: go语言的设计是务实的, go在针对并发上进行了优化, 并且支持大规模高并发, 又由于单一的码格式, 相比于其他语言更具有可读性, 在垃圾回收上比java和Python更有效, 因为他是和程序同时执行的.
1. 进程, 线程, 协程的区别, 协程的优势
进程是操作系统资源调度的基本单位,线程是程序执行的基本单位, 线程是不拥有系统资源的, 但是可以访问进程内的资源,一个进程内可以运行多个线程, 在创建, 撤销和切换操作上, 进程要比线程开销要大, 下面来说协程, 刚刚说的进程和线程是同步机制的, 而协程是异步机制的, 所以协程在这个异步机制下, 协程就会比进程和线程效率要高, 进程和线程都可以有多个协程, 在系统管理上, 协程不受内核管理,完全是由程序来管理, 所以协程的执行需要依靠进程或者线程.
2. 讲一下GMP模型(重点)
首先G是goroutine, M是线程(个数由cpu核数来决定), P是逻辑处理器(GOMAXPROCS决定),每一个P中有一个本地队列, 当绑定P的局部队列满了之后会放到全局队列, 每个p会和一个m绑定,p通过调用m来执行本地队列的线程,当p的本地队列为空的时候会尝试从其他本地队列和全局队列中偷取g,当m执行的g发生堵塞时,m会自动与p解绑, 让p去创建或者引用其他空闲的m来继续执行自己本地队列的g.
3. Go的GC, 混合写屏障(重点)
GODEBUG=gctrace=1: 观察GC
go tool trace : 图形化界面查看GC
go采用的是三色标记清理法, 属于追踪式GC, 首先把所有的对象放到白色的集合中, 第二是从根节点遍历白色对象, 遍历到的对象放到灰色对象中, 第三遍历灰色集合中的对象, 把灰色对象中引用的白色对象放到黑色集合中, 然后一直循环第三步骤, 直到灰色集合中没有对象, 最后把白色集合中的对象当做垃圾清理掉.
首先我说一下写屏障的作用: 因为go的垃圾回收和程序是同时执行的, 写屏障就是为了防止对象的错误丢失而诞生的, 他只存在于并发类型的GC中, 在并发GC中, 会出现两种情况会导致对象的错误着色:1. 赋值器修改对象图, 导致黑色对象用白色对象, 2是灰色对象到达白色的未访问过的路径被破坏,导致错误回收,只要可以成功避免一个就可以使得回收期回收成功, 其中插入写屏障是为了避免条件一的发生, 删除写屏障是避免了条件二的发生, 混合写屏障是插入写屏障和删除写屏障的混合使用, GO团队使用他的目的是简化GC的流程, 基本思想是对正在被覆盖的对象进行着色, 如果当前栈并未扫描完, 则同样会对指针进行着色, 但此时这种做法使着色成本为之前的双倍, 所以在之后的版本中, 又实现了批量写屏障机制, 基本思想是将需要着色的指针写入一个缓存, 缓存满的时候统一批量着色.
4. go的Slice和数组的区别, slice的扩容原理(重点)
在底层结构上: 数组是固定长度的, 长度是数组类型的一部分, 长度不同, 数组类型也不相同, slice的底层是一个结构体, 包含了三个变量值长度, 容量和指向底层数组的指针, 长度可变,在传参方式上: 数组是值传递, 切片是引用传递, 在扩容方面: 数组长度固定不会扩容, 切片长度小于1024的时候,是以大概2倍的速度增长, 超过1024的时候, 会以1.25倍左右增长, 之所以是大概不是固定速率的原因是还有内存对齐这一机制, 简单来说就是在切片扩容过程中, go语言会为切片申请预先分配好并且大小最接近的内存块, 比如8,16,32 48,64等保证内存不被浪费.
5. 讲一下channel,实现原理(重点)
在缓存有无方面: channel分为有缓存和无缓存两种类型, 在使用类型方面: 可以分为接收类型, 发送类型, 和可收可发类型,在数据类型上: 无论发送和接收, 都是进行值的拷贝,资源泄露的原因: goroutine在操作channel后, 会处于发送和接收状态, 如果此时channel出于满或空的状态, 会一直得不到改变,同时, 垃圾回收器也不会回收此类资源, 并且导致goroutine会一直出于等待队列中, 之后在发送或者资源, 会产生资源泄露. 应用方面: 停止信号, 任务计时, 控制并发数
6. 讲一下Go的Map的实现原理, 是否线程安全, 如何实现安全(重点)
map存储kv键值对, slice, map, functions类型不能做k, 而value支持所有类型为什么事无序的, 分两种情况: 当map扩容时, 原来落在同一个bucket中的部分key会被重新其他bucket, 导致位置变化,如果不扩容的话, 按道理来说是这样的, 但go实现的是,每次遍历的时候都会随机选取一个起始位置, 以至于无序
map不是线程安全的, 在增删改查的时候, 都会检查map中的写标志, 一旦发现是1, 则立即会报panic.线程安全用锁实现, read map, dirty map, misses(穿透次数)
go的map的底层实现是hash查找表, 使用链表方式来解决冲突, 代码实现的话主要分为两部分, hmap, bmap,hmap里面主要存储了map的主要数据结构和相关信息, bmap指向的是一个结构体, 里面存储的是是一个数组, 在分配key的内存地址的时候, 首先map会对key进行hash计算, 成为64位的hash地址, 根据hash值的后几位来决定该key存储在哪个桶里, 具体位数是桶数量的对数,hash值高八位决定该key在桶中的位置
map的扩容受装载因子的作用, 当超过6.5的时候会引发扩容机制, 主要控制两种情况, 第一种是: 元素太多, 且几乎把所有已经分配过bucket的占满, 第二种是经历了太多的bucket增加然后删除, 导致bucket分配过多但是元素很少, 就相当于在很多房子里面找一个人, 这样也会降低速率,对于第一种情况来说, map采用了直接将B+1, 创建一个新map, 容量为原来的两倍, 但是需要搬迁, 考虑到一次性搬迁完容易损耗性能, 然后采用了渐进式搬迁, 一次只搬迁两个, 直到全部搬迁完.
遍历: 遍历所有的 bucket 以及它后面挂的 overflow bucket,然后挨个遍历 bucket 中的所有 cell。每个 bucket 中包含 8 个 cell,从有 key 的 cell 中取出 key 和 value,这个过程就完成了
7. new 和 make 的区别
使用new, 会返回一个类型的指针, make返回的是一个实例, make只能用在切片, channel 和 map上, new都可以
8. 说一下内存逃逸
内存逃逸分析是编译器在进行编译优化时, 用来决定变量应该是分配到堆上还是栈上的工具, 我们手抖分析的时候可以 go build -gcflags="-m"
9. 函数传指针和传值有什么区别
传指针的话, 方法内部可以对外部的变量做修改, 传值则只是将外部变量的值拷贝到方法内部, 方法内部做修改影响不了外部的变量
10. goroutine之间的通信方式
channel
11. 测试是怎么做的(单元测试, 压力测试)
go test
go-wrk
-H="User-Agent: go-wrk 0.1 bechmark\nContent-Type: text/html;": 由'\n'分隔的请求头
-c=100: 使用的最大连接数
-k=true: 是否禁用keep-alives
-i=false: if TLS security checks are disabled
-m="GET": HTTP请求方法
-n=1000: 请求总数
-t=1: 使用的线程数
-b="" HTTP请求体
-s="" 如果指定,它将计算响应中包含搜索到的字符串s的频率
12. 堆和栈的区别
栈内存是用来存储函数出入参和局部变量的,这种分配方式有极高的效率。
栈内存的分配与释放由编译器决定,堆内存的分配与释放则完全由开发人员决定。