make 和 new 的区别
- make 只能用于 slice,map,channel 三种类型,返回的是初始化之后类型的值
- new 为一个 T 类型新值分配空间并将此空间初始化为 T 的零值,返回的是新值的地址,也就是 T 类型的指针 *T
slice切片的深拷贝和浅拷贝
- slice底层数据有个 Data uintptr 指向的是底层数组地址
- 浅拷贝使用赋值 a := b, a和b的指向同一个底层数组,任何一个数组元素改变,都会同时影响两个数组
- 深拷贝使用copy copy(a,b),a和b指向不同的底层数组,任何一个数组元素改变都不影响另外一个
切片容量增长
- 一般都是在向 slice 追加了元素之后,才会引起扩容。追加元素调用的是 append 函数。
- 当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。
context包相关
- Context 是安全的,可被多个 goroutine 同时使用。一个 Context 可以传给多个 goroutine,而且可以给所有这些 goroutine 发取消信号
- Done 方法返回一个 channel,当 Context 取消或到达截止时间时,该 channel 即会关闭。Err 方法返回 Context 取消的原因。
- Deadline 方法可以返回该 Context 的取消时间
- WithValue 可以用于传值
- WithTimeout 来做超时控制
接口(interface)
- 鸭子模型,一个结构体实现了某个接口的所有方法,则此结构体就实现了该接口
struct能不能比较
- struct 如果包含成员变量Slice,Map,Function则无法进行比较,会报错
- struct比较时有地址时,注意其指针值是否相同
值传递、引用传递、指针传递的区别
go语言中的值类型有:int、float、bool、array、sturct等
声明一个值类型变量时,编译器会在栈中分配一个空间,空间里存储的就是该变量的值 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数
go语言中的引用类型:slice,map,channel,interface,func,string等
声明一个引用类型的变量,编译器会把实例的内存分配在堆上其中string类型也为引用类型,但string不允许修改,底层实现struct String { byte* str; intgo len; },每次操作string只能生成新的对象,所以在看起来使用时像值类型。所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数
go语言中的指针:一个指针变量指向了一个值的内存地址,引用类型可以看作对指针的封装
defer及函数return 返回
- defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用
- 函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中
例1:
func f() (result int) {
defer func() {
result++
}()
return 0
}
可改写为
func f() (result int) {
result = 0 //return语句不是一条原子调用,return xxx其实是赋值+ret指令
func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
result++
}()
return result
}
例2:
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
可改写为
func f() (r int) {
t := 5
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t + 5
}
return //空的return指令
}
Go中map如何顺序读取
- 由于map底层实现与 slice不同, map底层使用hash表实现,插入数据位置是随机的, 所以遍历过程中新插入的数据不能保证遍历。
- 要实现对 key 排序,那么我们便可将 map 的 key 全部拿出来,放到一个数组中,然后对这个数组排序后对有序数组遍历,再间接取 map 里的值就行了。
map 中的 key 为什么是无序的
- map扩容会发生key的搬迁,这样遍历map的结果就不会是原来的顺序
- 即使map未改变,go在遍历时不会用第0个元素开始,而是随机元素开始,避免依赖map遍历顺序
map执行delete指定key后,内存会回收吗,及其扩容策略
- map中的key被删除后,内存不会立即释放,随着程序的运行,map占用的内存只会越来越大
- 可将map设置为nil 来作为回收
- map底层实现使用哈希表,同时使用链表来解决哈希冲突
- map装载因子 loadFactor := count / (2^B) (count 就是 map 的元素个数,2^B 表示 bucket 数量)
- 不扩容的话,所有的 key 都落在了同一个 bucket 里,直接退化成了链表,各种操作的效率直接降为 O(n)
- 装载因子超过6.5就会扩容,在原来的基础上 bucket 增加2倍
map并发不安全解决
- 使用sync.RWMutex 读写锁来进行操作
- 使用sync.Map来代替map操作
- 可将map划分为多个分片,单独在分片上加锁,提高效率
channel的底层实现
- buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表
- sendx和recvx用于记录buf这个循环链表中的发送或者接收的index
- lock是个互斥锁
- recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // 循环链表,存储数据
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index (当前发送的index)
recvx uint // receive index (当前读到的index)
recvq waitq // list of recv waiters(<-channel 队列)
sendq waitq // list of send waiters(channel <- 队列)
}
介绍一下大对象小对象,为什么小对象多了会造成 gc 压力
- 微对象指的是小于16字节,小对象指的是16字节~32KB,大对象指的是大于32KB
- 通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配。
内存泄漏场景
- 互斥锁未释放及死锁的产生
- 空channel被goroutine引用,处于发送或接收阻塞状态
- 定时器ticker使用忘记Stop,通常使用context来避免
内存逃逸
- 内存变量本来应该分配在栈上,结果必须分配到堆上
- 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出
- 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上
- slice 的背后数组被重新分配了,因为 append 时可能会超出其容量,切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配
GC模式
- 栈内存会随着函数的调用分配和回收;堆内存由程序申请分配,由垃圾回收器(Garbage Collector)负责回收
- 垃圾回收都是针对堆的,gc清除的垃圾也是堆上的数据
标记清除法
最初版本GC回收使用标记清除,即暂停程序业务逻辑,标记可达对象,清除未标记对象,结束清除停止暂停。 一个明显的缺点在于程序暂停,即STW(stop the world)
三色标记法
- 每次新创建的对象,默认的颜色都是标记为 “白色”
- 每次 GC 回收开始,会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入 “灰色” 集合
- 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
- 重复第三步 , 直到灰色中无任何对象
- 回收所有的白色标记表的对象。也就是回收垃圾
触发 GC 的时机
- 主动触发,通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕
- 被动触发,使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC;当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发
堆和栈
- 变量占有内存,内存在底层分配上有堆和栈。
- 值类型变量的内存通常是在栈中分配
- 引用类型变量的内存通常在堆中分配
- 当变量是全局变量时,符合上面所说的分配规则,但当变量是局部变量时,分配在堆和栈就变的不确定了(即内存逃逸)
- 栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用
加入混合写屏障机制
- GC 开始将栈上的对象全部扫描并标记为黑色 (之后不再进行第二次重复扫描,无需 STW),
- GC 期间,任何在栈上创建的新对象,均为黑色。
- 被删除的对象标记为灰色。
- 被添加的对象标记为灰色。
channel 在什么情况下会引起资源泄漏
- goroutine 操作 channel 后,发送数据到一个满状态的channel时且channel状态一直得不到改变
- goroutine 操作 channel 后,接收数据空状态的channel时且channel状态一直得不到改变
channel 有哪些应用
- 任务定时 (与 timer 结合,一般有两种玩法:实现超时控制,实现定期执行某个任务)
- 超时控制
select {
case <-time.After(100 * time.Millisecond):
case <-s.stopc:
return false
}
- 定期执行
ticker := time.Tick(1 * time.Second)
for {
select {
case <- ticker:
// 执行定时任务
fmt.Println("执行 1s 定时任务")
}
}
- 解耦生产方和消费方(生产者将消息放入channel中,消费者直接读取channel)
- 控制并发数(可通过channel设置缓冲区大小来实现对应goroutine)
channel不方便及操作panic情况
- 不改变 channel 自身状态的情况下,无法获知一个 channel 是否关闭(即不从channel中读取或存入数据或关闭,无法知道channel是否已关闭)
- 向一个 closed channel 发送数据会导致 panic
- 关闭一个 closed channel 会导致 panic
channel操作情况总结
操作 | nil channel | closed channel | not nil, not closed channel |
---|---|---|---|
close | panic | panic | 正常关闭 |
读 | <- ch | 阻塞 读到对应类型的零值 | 阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞 |
写 | ch <- | 阻塞 panic | 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞 |
string(字符串)
SET key value
- 底层通过SDS(简单动态字符串),优点在于能直接获取字符串长度并且二进制安全
list(队列)
LPUSH key value1 [value2]
- 底层实现是链表,有前置节点和后置节点的双向链表
- 常应用于异步队列(如秒杀抢购等场景)
hash(哈希)
HSET key field value
- 底层通过压缩列表或哈希表实现
- hash和string类似,实际应用适合缓存对象(key,field, value)
- 可通过hexists判断对象某字段是否存在,hincrby实现某字段计数器
- Redis 的哈希表使用链地址法来解决哈希冲突,也就是说哈希值对应的是一个链表,有一个next指针,当哈希冲突时,会采用头插法,将新值插入链表的头部。这样取数据时,计算哈希值然后再遍历链表匹配即可取出对应的值
set(集合)
sadd key member [member …]
- 底层通过哈希表或整数集合实现
- 集合特性无序、不可重复、支持并交差
- 可应用于点赞(一个用户只能点赞一次),共同关注/好友(集合的交集运算)
- 差集、并集和交集的计算复杂度较高,在数据量大的场景下容易发生阻塞
sorted set(有序集合)
zadd key score member
- 底层通过压缩列表或跳表实现
- 可以根据元素的权重来排序,如排行榜,分页等频繁更新可通过有序集合来实现
redis内存淘汰策略
- no-eviction redis默认策略,redis不再提供写操作,但可以读和删除
- volatile-lru 淘汰设置了过期时间的,并且最近最少使用的key
- volatile-ttl 优先淘汰剩余过期时间最小的key
- volatile-random 设置了过期时间的key 随机淘汰
- allkeys-lru 和volatile-lru区别在于,针对所有key
- allkeys-random 和volatile-random区别在于,针对所有key
redis缓存持久化
1.RDB(时间点快照)
- 可手动触发或自动触发,fork出一个子进程执行rdbSave进行数据的全量落盘
- 自动触发规则可设置如 save 900 1 (表示900秒 内至少有1个key的值发生变化则执行)
- 优点是恢复速度快,文件小,缺点可能导致数据丢失
2.AOF(命令追加(append)、文件写入、文件同步(sync))
- redis执行一条命令会以协议格式将命令追加到 aof_buf 的缓冲区末尾,然后将aof_buf缓冲区的文件写入AOF文件中
- 写入有三种策略 always(数据全部同步到AOF文件)、everysec(每秒同步一次)、no(不同步)
- 当AOF文件过大时,会启用 rewrite 机制对其进行压缩
- 优点是数据不会丢失,缺点在于文件体积一般比RDB文件大,恢复速度慢