Golang基础

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)

三色标记法

  1. 每次新创建的对象,默认的颜色都是标记为 “白色”
  2. 每次 GC 回收开始,会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入 “灰色” 集合
  3. 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
  4. 重复第三步 , 直到灰色中无任何对象
  5. 回收所有的白色标记表的对象。也就是回收垃圾

触发 GC 的时机

  1. 主动触发,通过调用 runtime.GC 来触发 GC,此调用阻塞式地等待当前 GC 运行完毕
  2. 被动触发,使用系统监控,当超过两分钟没有产生任何 GC 时,强制触发 GC;当所分配的堆大小达到阈值(由控制器计算的触发堆的大小)时,将会触发

堆和栈

  • 变量占有内存,内存在底层分配上有堆和栈。
  • 值类型变量的内存通常是在栈中分配
  • 引用类型变量的内存通常在堆中分配
  • 当变量是全局变量时,符合上面所说的分配规则,但当变量是局部变量时,分配在堆和栈就变的不确定了(即内存逃逸)
  • 栈空间的特点是容量小,但是要求相应速度快,因为函数调用弹出频繁使用

加入混合写屏障机制

  1. GC 开始将栈上的对象全部扫描并标记为黑色 (之后不再进行第二次重复扫描,无需 STW),
  2. GC 期间,任何在栈上创建的新对象,均为黑色。
  3. 被删除的对象标记为灰色。
  4. 被添加的对象标记为灰色。

channel 在什么情况下会引起资源泄漏

  • goroutine 操作 channel 后,发送数据到一个满状态的channel时且channel状态一直得不到改变
  • goroutine 操作 channel 后,接收数据空状态的channel时且channel状态一直得不到改变

channel 有哪些应用

  1. 任务定时 (与 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 定时任务")
      }
  }
  1. 解耦生产方和消费方(生产者将消息放入channel中,消费者直接读取channel)
  2. 控制并发数(可通过channel设置缓冲区大小来实现对应goroutine)

channel不方便及操作panic情况

  1. 不改变 channel 自身状态的情况下,无法获知一个 channel 是否关闭(即不从channel中读取或存入数据或关闭,无法知道channel是否已关闭)
  2. 向一个 closed channel 发送数据会导致 panic
  3. 关闭一个 closed channel 会导致 panic

channel操作情况总结

操作 nil channel closed channel not nil, not closed channel
close panic panic 正常关闭
<- ch 阻塞 读到对应类型的零值 阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞
ch <- 阻塞 panic 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞
redis之数据结构

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文件大,恢复速度慢