1. 内置函数
1.1 new,make的区别
1)作用类型不同:new给string,int和数组分配内存,make给slice,map,channel分配内存;
2)返回类型不同:new返回指向变量的指针,make返回变量本身;
3)初始化值不同:new 分配的空间被清零(分配的内存置为零),make 分配空间后,会进行初始化为非零值;
1.2 简述defer延迟机制
defer是一种用于注册延迟调用的机制,让函数或者语句在当前函数执行完成(包括return正常结束/panic异常结束)之后进行调用。
特性
- 延迟调用:defer在main函数return之前调用 & defer必须置于函数内部
- LIFO:后进先出,压栈执行
- 作用域:只作用于绑定的函数,若defer处于匿名函数中,会先调用匿名函数中的defer
应用场景
- 并发处理:使用sync.WaitGroup时的Add()-Done()操作
- 加锁解锁:mu.RLock() & mu.RUnLock()
- 资源释放:打开关闭文件/客户端等成对操作
2. 数据结构
2.1 Slice
1)底层结构
type slice struct {
array unsafe.Pointer //指向具体的底层数组
len int //切片长度
cap int //切片容量
}
2)简单描述底层扩容机制
1)切片较小( <= 1024):以2倍速扩容,避免频繁扩容,减少内存分配的次数和数据拷贝的代价;
- 如果期望容量大于当前容量的两倍,使用期望容量
- 如果当前切片长度小于 1024,容量翻倍
2)切片较大( > 1024):以1.25倍速扩容,避免空间浪费;
- 如果当前切片长度大于 1024,每次扩容 25% 的容量,直到新容量大于期望容量
- 在进行循环1.25倍计算时,最终容量计算值发生溢出,即超过了int的最大范围,则最终容量就是新申请的容量
3)数组与切片的异同
相同点
- 只能存储同数据类型
- 都通过下标访问元素
不同点
- 切片是指针类型,数组是值类型【传递数组是通过拷贝的方式,传递切片是通过传递引用的方式】
- 数组长度固定,切片可以动态扩容【数组是一组内存空间连续的数据,一旦初始化长度大小就不会再改变,切片的长度可以进行扩展,当切片底层的数组容量不够时,切片会创建新的底层数组】
2.2 Channel
本质是先进先出的队列,用于数据传递或数据通信;可使用goroutine + channel实现高效,线程安全的通信。
1)是否线程安全?如何保证?
结论:channel是线程安全的
原理:底层采用mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,会先获取互斥锁,然后操作channel数据。
2)底层实现
关键字段及描述
buf:缓冲区数组指针,只有缓存channel才有buf指针
sendx & recvx: 底层循环数组的发送 & 接收元素位置索引
sendq & recvq:发送和读取数据被阻塞的goroutine等待队列
向Channel发送数据流程(ch ← x)
从channel读取数据流程(x ← ch)
流程描述
sendq不为空:
- 无缓冲区: 从sendq头部取一个goroutine,将数据读取出来,并唤醒对应的goroutine,结束读取过程
- 有缓冲区: 说明缓冲区已满,则从缓冲区中首部读出数据,把sendq头部的goroutine数据 写入缓冲区尾部,goroutine唤醒,结束读取过程
sendq为空:
- 缓冲区有数据:则直接从缓冲区读取数据,结束读取过程
- 缓冲区无数据:则只能将当前的goroutine加入到recvq,并进入waiting状态,等待被写goroutine唤醒
3)channel状态和操作
操作 | nil | closed | active |
close(ch) | panic | panic | 正常关闭 |
ch ← val(写) | 永远阻塞 | panic | 成功发送/阻塞 |
val,ok = ← ch(读) | 永远阻塞 | 正常接收 | 成功接收/阻塞 |
4)应用场景
- 生产者-消费者解耦:生产者只需要往channel发送数据,消费者只管从channel中获取数据;
- 控制并发数
2.3 Map
1)底层实现
结合顺序存储数组和链式存储两种存储结构;数组作为主干,数组元素的类型为链表
2)扩容机制是怎样的?
- bmap扩容的加载因数达到6.5(元素个数/bucket),bmap就会进行扩容,将原来bucket数组数量扩充一倍,产生一个新的bucket数组。bmap中的oldbuckets属性指向旧bucket数组。
- map是渐进式扩容,首先开辟2倍的内存空间,创建一个新的bucket数组。当访问旧bucket数组时,会将旧bucket拷贝到新的bucket数组,然后去掉旧bucket的引用,等待GC回收
3)如何实现线程安全的map?
- 加读写锁(RWMutex):加锁方式简单,但并发性能差
- 分片加锁:锁粒度小,可提高访问map的吞吐,并发性能较好
- sync.Map:官方标准,但应用场景特殊,最不常用
- 场景一:只会增长的缓存系统,一个 key 值写入一次而被读很多次
- 场景二:多个 goroutine 为不相交的键读、写和重写键值对