Golang questions

利用通信,也就是封装内部实现,提供接口的方式来进行相应的操作

make 和new的区别

分配内存的类型,返回值来回答
make专门给引用类型分配内存+初始化,返回的是一个对象的引用
new用来为其他类型分配内存,返回的是对象的指针(也可以new一个map,返回的指针指向全是0值的map对象

map数据结构是什么,底层是基于哈希表实现的,碰撞冲突用的是开链法

  1. hmap存的是哈希结构,里面包含一个bmap,B,extra字段,bmap存的时bucket,B用于扩容,extra存溢出的bucket指针
  2. bucket的数量是通过B计算的 = 2^B,每个bucket包含8个kv对,和一个overflow指针,用于开链解决冲突,
  3. bmap存的是topbits[8], keys[8] values[8] overflow指针(移动到hmap的extra字段)

查一个kv的时候,怎么查的

  1. myMap[“key0”]
  2. 对key0做哈希函数;,这个时候落到某个桶中,再查找hashcode高8位,也就是在bmap中查找topbits[8]来查找在桶的哪个位置

map扩容是怎么做的

增量扩容时机是插入时,计算这个map的装载因子,大于6.5时把bucket的2^B +1,才进行扩容。

  1. loadfactor = count / 2^B 大于6.5。发生增量扩容,B+1,然后rehash重新分配kv到不同bucket

等量扩容是overflow的bucket过多是发生
当1. B小于15时,overflow的bucket超过2^B个;
当2. B大于15时,超过2^15个,hasgrowth()函数进行判断,在mapassign的时候进行逐步搬迁

  1. 等量扩容,重新计算一次hashcode进行等量扩容
  2. 扩容时,会对bucket的内存进行搬迁,先把现有bucket挂到oldbucket字段,然后在插入动作(mapassign)中执行

    为什么负载因子是6.5

    至于装载因子为什么选择6.5,以下是go源码中对不同装载因子的测试,其中有四个重要的指标
    1
    2
    3
    4
    %overflow : hmap中拥有溢出桶的bucket数量
    bytes/entry:平均每对key/elem使用的内存数量
    hitprobe:查找一个存在的keys所需要的检查的kv数量
    missprobe:查找一个不存在的key需要检查的kv数量

可以看到,当负载因子过大时会导致查找性能急速下降,但是负载因子太小时又会导致有大量内存被浪费,所以go team最终选择了6.5做负载因子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Picking loadFactor: too large and we have lots of overflow

// buckets, too small and we waste a lot of space. I wrote

// a simple program to check some stats for different loads:

// (64-bit, 8 byte keys and elems)

// loadFactor %overflow bytes/entry hitprobe missprobe

// 4.00 2.13 20.77 3.00 4.00

// 4.50 4.05 17.30 3.25 4.50

// 5.00 6.85 14.77 3.50 5.00

// 5.50 10.55 12.94 3.75 5.50

// 6.00 15.27 11.67 4.00 6.00

// 6.50 20.90 10.79 4.25 6.50

// 7.00 27.14 10.15 4.50 7.00

// 7.50 34.03 9.73 4.75 7.50

// 8.00 41.10 9.40 5.00 8.00

//

// %overflow = percentage of buckets which have an overflow bucket

// bytes/entry = overhead bytes used per key/elem pair

// hitprobe = # of entries to check when looking up a present key

// missprobe = # of entries to check when looking up an absent key

map遍历

讲讲go程序的执行过程:预处理,编译、链接、运行

词法分析、语法分析、类型检查、代码生成、编译器优化、链接

  • go run做了什么:编译、链接、运行

channel和共享内存有什么优劣

channel

  • 隐式同步,减少锁的使用
  • 不适合大量数据传输
    共享内存
  • 显式用锁,性能高
  • 适合大量数据,但难以调试,存在复杂的同步机制

context原理和场景【并发安全的】

【withtimeout超时取消,withvalue传递共享数据】
确切的说它是 goroutine 的上下文,包含了 goroutine 的运行状态、环境、现场等信息。
作用的话一般是用取消信号,控制超时
context用于多个goroutine之间进行通信和控制的官方库,实现并发控制,包括取消信号、控制超时时间
注意点

  1. 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
  2. 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
  3. 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响

原理,Context.Value查找

Context 指向它的父节点,链表则指向下一个节点。通过 WithValue 函数,可以创建层层的 valueCtx,存储 goroutine 间可以共享的变量。
取值的过程,实际上是一个递归查找的过程,它会顺着链路一直往上找,比较当前节点的 key是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。
父节点没法获取子节点存储的值,子节点却可以获取父节点的值。

waitgroup的原理和场景【多个groutine的等待结束/并发控制】

  • 内部维护了一个计数器,初始化为0。
  • 调用 Add 方法时,计数器会增加;
  • 每当调用 Done 方法时,计数器会减少;
  • 调用 Wait 方法时,如果计数器不为零,则会阻塞当前 Goroutine,直到计数器减至零。

string和byte的转换发生内存拷贝吗【会】为什么?

string底层是一个不可变的字符数组,执行[]byte(str),拷贝完,之前分配的空间被gc

为什么go协程堵掉不会阻塞,C++的线程堵掉

【C++的线程pthread是内核态,Go的调度时runtime系统管理的,相当于套了一层壳,运行在用户态,但是有内核态的速度,然后goroutine堵掉会通过GPM模型的handoff机制移交给其他的M去执行

除了mutex还有什么方法实现并发安全

atomic包和channel

mutex和RWmutex的区别

  1. 可重入性:mutex不是可重入锁,Mutex 不会记录持有锁的协程的信息,所以它也无法区分是不是重入这种场景。
  2. 读写锁的读锁可重入,写锁不可以

    锁的正常和饥饿状态

    这个runtime系统解释一下

    golang 的 runtime 在 golang 中的地位类似于 Java 的虚拟机。
    包括
  3. GPM模型
  4. GC机制
  5. 内存分配:Go 程序在启动时,会首先向系统申请一块内存(虚拟地址空间),然后自己切成小块进行管理. 将申请的内存,分成 3 个区域,spans、bitmap、arena
    1
    2
    3
    arena: 就是堆区,go runtime 在动态分配的内存都在这个区域,并且将内存块分成 8kb 的页,一些组合起来的称为 **mspan,**成为 go 中内存管理的基本单元,这种连续的页一般是操作系统的内存页几倍大小.
    bitmap: 顾名思义,用来标记堆区使用的映射表,它记录了哪些区域保存了对象,对象是否包含指针,以及 GC 的标记信息.
    spans: 存放 mspan 的指针,根据 spans 区域的信息可以很容易找到 mspan. 它可以在 GC 时更快速的找到的大块的内存 mspan.

1.go的数组和切片

  1. 数组是固定长度的,切片是可以变化的
  2. 切片实际是对数组的封装,切片底层是由指向数组的指针,切片长度,切片容量三个参数组成。指向底层数组的指针就标志着切片的开始
  3. 切片是对底层数组的一个引用,不同的切片可以指向同一个底层数组,操纵同一个底层数组。

需要注意的几点

  1. 传递切片作为函数参数,其实拷贝的是切片这个结构体,会产生一个新的切片结构体实例,指向同一个底层数组。虽然也会改变底层数组得值,但是对于原来的切片来说,是没有任何变化的,只是对应的底层数组中某些元素的值变了。

  2. 切片进行append扩容的时候,会产生新的切片地址,所以要将append函数返回的值重新赋给切片

  3. Go1.18不再以1024为临界点,而是设定了一个值为256的threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3256)/4;

  • 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
  • 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
  • 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3threshold)/4。
  1. 需要注意切片是对数组的引用, 所以当切片被赋值给别的切片变量时, 改变新的切片变量中的值, 会连带改变原切片值

2.读已经关闭的channel发生什么

  1. 读已关闭的channel 读已经关闭的channel无影响。

    • 如果在关闭前,通道内部有元素,会正确读到元素的值;

    • 如果关闭前通道无元素,则会读取到通道内元素类型对应的零值。

    • 若遍历通道,如果通道未关闭,读完元素后,会报死锁的错误。会引发fatal error: all goroutines are asleep - deadlock!

  2. 写已关闭的通道
    会引发panic: send on closed channel

  3. 关闭已关闭的通道
    会引发panic: close of closed channel

需要注意的几点:对于一个已初始化,但并未关闭的通道来说,收发操作一定不会引发 panic。但是通道一旦关闭,再对它进行发送操作,就会引发 panic。如果我们试图关闭一个已经关闭了的通道,也会引发 panic。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//1. 读一个已经关闭的通道
func main() {
channel := make(chan int, 10)
channel <- 2
close(channel)
x := <-channel
fmt.Println(x)
}
/*[Output]: 不会报错,输出2*/

// 遍历读关闭通道
func main() {
channel := make(chan int, 10)
channel <- 2
channel <- 3
close(channel) //若不关闭通道,则会报死锁错误
for num := range channel {
fmt.Println(num)
}
}
/*[Output]: 不会报错,输出2 3*/

//2. 写一个已经关闭的通道
func main() {
channel := make(chan int, 10)
close(channel)
channel <- 1
}
/*[Output]: panic: send on closed channel*/

//3. 关闭一个已经关闭的管道
func main() {
channel := make(chan int, 10)
close(channel)
close(channel)
}
/*[Output]: panic: close of closed channel */

3.Go是如何实现继承的

通过struct的组合实现的继承

13.map取一个key,修改这个key,原map会更改吗【会,是引用类型】

从map中取出一个值并对其进行修改时,原始的map也会受到影响,因为map是引用类型,它们在底层是指向相同的数据结构的指针

  • 引用类型:map slice channel interface

  • 【*不可以边遍历边修改元素】

16. struct能否==比较【可以】,成员里有struct呢?【可以】

  1. 但是只能1.相同类型结构体,2.成员结构相同并且都是可比较类型
  2. 但切片、映射和函数等类型无法比较

    25. go的深浅拷贝

  3. 浅拷贝: person1 := person2 ,拷贝后的数据是原来数据的引用,更改后原来的也会改
  4. 深拷贝: person3 := Person{name : person1.name},独立的对象

    29. 如何判断channel是否关闭

  5. _, ok := <- mych

30. make 和 new 的区别

  • make创建、初始化引用类型
  • new返回的是一个类型的指针,只有创建没有初始化,可以用于任何数据

    31. Slice的append

    扩容,每次达到阈值会扩容大改 1/4

    44. goroutine获取不到锁会一直等待吗【当然会】

    58. 空结构体用来干嘛【占位符,某个集合的存在性检查】

    60. defer用来干什么【释放锁,关协程,关channel,文件,recover panic】

    61. context包的作用【并发安全】

处理网络请求和并发任务时常用,处理请求范围内的值传递、取消和超时等问题,

  1. 并发安全,可以在多个goroutine中共享
  2. 传递信号给goroutine,进行管理
  3. 传递请求范围内的值

64. panic如何恢复

  1. 当程序遇到一个 panic,它会立即停止当前函数的执行,并沿着调用栈一直向上传播,直到到达 recover 所在的延迟函数。
  2. 如果在defer 中调用了 recover,它会停止 panic 的传播并返回 panic 的值。
  3. 如果没有发生 panic,recover 会返回 nil
    1
    2
    3
    4
    5
    6
    7
    8
    9
    func recoverExample() {
    defer func() {
    if r := recover(); r != nil {
    fmt.Println("Recovered:", r)
    }
    }()
    // 引发 panic
    panic("something went wrong")
    }

【多协程】

  1. 子协程A的panic,B能否捕获到?【不可以,但是对其他协程不影响】
  2. 主协程能捕获子协程吗【也不能,因为只有协程自己内部的 recover 才能捕获自己抛出的 panic】

70. go的init函数何时执行的

  • 导包初始化的时候执行,多个init时,都会执行
  • 一个文件里有多个init时,根据包的导入关系决定
  • 在包内有多个init,init执行顺序,golang没有明确定义,字典序?

    72. Gin的路由如何实现【压缩版的前缀树路由,httprouter库】

74. struct的传递场景:大struct避免复制,用浅拷贝

79. sync.Pool 对象池用来干嘛的,应用场景如何

  • 它用于存储和复用临时对象,以减少内存分配和垃圾回收的开销。
  • 适用于需要频繁创建和销毁对象的场景
    • 一些高并发场景,频繁创建和销毁一些对象
    • 协程池,数据库连接池,http连接池
    • 临时缓冲区
      1
      2
      3
      4
      5
      6
      7
      8
      原理如下
      1. 每个 sync.Pool 实例内部维护了两个 interface{} 类型的字段,一个用于存储临时对象(私有私有的 local 对象池),另一个用于存储共享对象(共享的 shared 对象池)。
      2. 当你调用 pool.Get() 方法时,sync.Pool 会首先尝试从当前 Goroutine 的私有对象池 local 中获取一个对象。
      3. 如果 local 中没有可用的对象,它会转而尝试从共享对象池 shared 中获取一个对象。
      4. 如果 shared 中也没有可用的对象,它会调用 New 函数创建一个新的对象。
      5. 当你调用 pool.Put(obj) 方法时,对象会被放回到当前 Goroutine 的私有对象池 local 中。
      6. 如果私有对象池 local 已满,或者对象过期,那么该对象会被丢弃。
      这个机制保证了对象会在同一个 Goroutine 中被复用,从而减少了对象的创建和垃圾回收的开销。

sync.pool怎么实现的

私有对象池和共享对象池+互斥锁保证线程安全,通过get和put

1
2
3
4
5
type poolLocal struct {
private interface{}
shared []interface{}
M sync.Mutex
}

83. 变量申请类型是为了做什么

类型就是根据不同的数据类型可以存储不同的数据,所以需要申请对应类型地址
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请

88. Go的GC机制介绍一下

84. Go和Java的GC机制有什么区别

  • 目前主流的Java虚拟机实现都采用了分代垃圾回收的思想,堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收,年老代主要使用标记-整理垃圾回收算法
  • 然后go的垃圾回收是混合写屏障机制,stw的时间更短,1.3用的标记清除,1.5改为用三色标记,但是还需要stw扫描栈所以就演进为混合写屏障,栈上新增对象都为黑色,暂时活过这一轮,然后删除的对象为灰色或者白色,节点都会变为灰色,黑色节点下新增节点都为灰色
  • java语言中选择了可达性分析进行对象存活判断,而不是引用计数,主要也是因为java中软引用、弱引用、虚引用等多种引用方式使用引用计数并不能进行有效的存活判断,同时为了避免循环引用的问题,所以java选择了可达性分析的方式进行对象存活判断。
  • 在java中触发垃圾回收的条件是:
    • cpu空闲的时候;
    • 在堆栈满了的时候;
    • 主动调用 System.gc() 后尝试进行回收;

Go的gc最佳应用场景是自身的分配行为不容易导致碎片堆积,并且程序分配新对象的速度不太高的情况,这种情况下go的垃圾回收比java更高效。相反的,当对象分配速度高时,java的gc的优势就会明显体现

102. 什么时候触发线程切换

  1. 阻塞
  2. 时间片用完
  3. 显式调用 runtime.Gosched():主动让出当前 Goroutine 的执行权限,让调度器选择另一个可运行的 Goroutine 执行
  4. 互斥锁
  5. 等待组 sync.WaitGroup()

    107. http库的设计原理是什么?为什么不池化?

    采用的是连接池:http 包会自动维护一个连接池,用于复用 TCP 连接,从而提升性能。
  • 不池化的原因:处理的对象不一样,场景也不一样
    • 对象池管理创建、销毁常用对象,减少gc压力
    • 连接池是复用连接,减少资源分配的开销,连接池中的资源通常会被长时间地重复使用,而对象池中的对象可能在短时间内就会被释放

110. 关闭一个已关闭的channel会发生什么?【panic】

110/238. 有缓存channel和没缓存channel的区别是什么?

无缓冲的与有缓冲 channel 有着重大差别,那就是一个是同步的 一个是非同步的

116/138. 类型断言

  • t := i.(T),这个表达式可以断言一个接口对象(i)里不是 nil,并且接口对象(i)存储的值的类型是 T,如果断言成功,就会返回值给 t,如果断言失败,就会触发 panic
  • t, ok:= i.(T),这个表达式也是可以断言一个接口对象(i)里不是 nil,并且接口对象(i)存储的值的类型是 T,如果断言成功,就会返回其类型给 t,并且此时 ok 的值 为 true,表示断言成功。这个不会触发 panic,而是将 ok 的值设为 false ,表示断言失败,此时t 为 T 的零值。

    8.channel的实现方式/原理/概念/底层实现

117. 实现一种等待或者监听的机制【使用select channel,或者time.sleep】

118. sleep的底层实现?slice的append返回一个新切片会发生什么

  • append不超threshold,底层引用的数组还是原来的地址
  • 超过了的话,原来的和新的都会指向新数组

120/137/291. interface的底层实现

  • 带有方法的interface,一种是不带方法的interface
  • 任何一个interface变量都是占用16个byte的内存空间
  • 第一个字段 _type指针,指向数据类型,runtime中的每个数据类型都包含一个这样的字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 没有方法的interface
type eface struct {
_type *_type //重要字段,记录着某种数据类型的一些基本特征,比如这个数据类型占用的内存大小(size字段),数据类型的名称(nameOff字段)等等
// 每种数据类型都存在一个与之对应的_type结构体
data unsafe.Pointer
}
// 有方法的interface
type iface struct {
tab *itab
data unsafe.Pointer
}
// 记录着Go语言中某个数据类型的基本特征
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
type itab struct {
inter *interfacetype
_type *_type //重要
link *itab
hash uint32
bad bool
inhash bool
unused [2]byte
fun [1]uintptr
}
// interface数据类型对应的type
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}

404. defer的底层实现

160. string的底层实现

121. STW 在 go 的哪些阶段发生?1.8的改进是什么【混合写】

标记和清理阶段都会发生

132. 如何避免panic

133. 结构体对齐优化

内存对齐:CPU访问内存时,通过字来访问,一个字在32位cpu中4个字节,所以对于

1
2
3
4
5
6
7
8
9
10
11
struct demo{
a int8 //1
b int32//4
c int16 //2
}
会变成 1+3 + 2+2 + 4 字节,而下面会变成 3+1 + 4
struct demo{
a int8 //1
b int16//2
c int32//4
}

244. go实现func自定义参数

type myFunc func(int) int

252. copy是操作符还是内置函数【内置函数,深拷贝】

290. 解释一下Go的通信机制

是通过channel实现的,chan定义实现了环形队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序,这一点和管道是一样的;chan在实现时定义了:

  • 指针
  • 环形队列
  • (阻塞)协程链表
    来控制通信,当chan满足条件时,通过指针sendx 、recvx 进行读写数据。

296. slice函数传参,先赋值再append与先append再赋值,哪个发生了改变【先赋值的改变了,因为先append的地址发生了改变】

1
2
3
4
5
6
7
8
9
10
11
12
func appendThenAssign(arr []int) {
arr = append(arr, 3)
arr[0] = 99
}
func assignThenAppend(arr []int) {
arr[0] = 99
arr = append(arr, 3)
}
appendThenAssign(arr)
//print arr: 1 2
assignThenAppend(arr)
//print arr: 99 2

297. 有没有什么线程安全的办法?

互斥锁
信号量,条件变量
原子操作
线程安全的库

golang http包的内存泄漏情况

  1. 忘记关闭response的 body
  2. 忘记释放连接,或者一直创建连接,没有有效复用的情况?

306. go map的时间复杂度

307. go由源码变二进制代码的整个流程

309. select poll epoll

314. make底层原理

315. string 强转 []byte 发生了什么

底层的不可变字符数组拷贝到byte类型的内存,然后原来的内存被gc掉

335/411. go的包管理工具除了go mod还有什么

  • go mod能下载和管理指定版本的库,实现高效的模块化开发,和管理依赖关系的功能
  • go sum干什么的:提供了安全机制
    • 块路径:列出了项目中所使用的所有直接依赖模块的路径。
    • 版本号:对应每个模块的版本号。
    • 哈希值:对应每个模块版本的哈希值,用于确保模块的代码完整性

      356. 介绍一下go的反射

      typeof和valueof来实现,每个类型,包括符合类型都维护了一个type和value区域

396. go的 oop 面向对象与传统面向对象的区别

go是用结构体定义对象,然后通过组合实现继承,对于多态来说,go强调接口的使用

397. go里面interface对于java的接口和c++的虚函数区别在哪

  • Go 接口是隐式实现的,一个类型只要实现了接口中的所有方法,就被认为是实现了该接口,无需显式声明。这种方式让 Go 具有了更大的灵活性。
  • Go 倾向于使用接口和返回错误值的方式来处理错误,而不是像 Java 或者 C++ 中那样使用异常

402. Go runtime的程序计数器,为什么是私有的(为什么程序猿不能操作)

  • 在 Go 的 runtime 中,程序计数器用于跟踪当前 Goroutine 正在执行的代码位置,从而支持 Goroutine 的并发执行。

  • 在单线程情况下,程序计数器会指向当前 Goroutine 执行的代码块。当发生 Goroutine 切换时,程序计数器的值会保存到当前 Goroutine 的上下文中,然后加载新 Goroutine 的上下文中的程序计数器值,以便从上次中断的地方继续执行。

  • 避免混乱,保护Groutine在并发环境下的完整性

423. interface和nil可以比较吗【可以】

但是必须要类型和值都相同

  • 果接口变量的动态值和动态类型同时都为 nil,那么接口变量将与 nil 比较相等。
  • 如果类型和值有一个不为nil,那么就是不相等

447. struct组合与java继承有什么区别

  • 相同点是,都是静态语言,在编译期实现
  • go组合支持多继承,java需要extends 父类来继承,只能继承一个

448. go的强制类型转换与隐式类型转换

Go 支持两种类型转换:

  • var a int = 10,var b float64 = float64(a) 将整数 a 转换为浮点数。
  • 在算术表达式中,如果操作符两侧的类型不一致,Go 会自动将其中一个值转换为与另一个值相同的类型。

451. 多个interface间可以存在什么关系

组合,嵌套都可以

  • 一个接口可以嵌套在另一个接口内部,这种情况下,外部接口会继承内部接口的所有方法
  • 一个接口可以由多个其他接口组合而成,组合后的接口将具有所有组成接口的方法

    510. go方法和函数的区别

    方法是定义了 Receiver 的函数,分为receiver
  • Value Receiver,不会修改receiver的内容
  • Pointer Receiver,会修改receiver的内容

512. Go函数返回局部变量的指针是否安全【安全,发生内存逃逸,监测到没使用的时候就GC掉】

5.Go的GMP模型

6.Go和Java相比

9.同一个goroutine里面,对无缓冲channel同时发送和接收数据有什么问题

导致死锁

10.channel和锁的对比

11.channel的应用场景

12.

227. go实现一个链表

289. 写一个将字符串json转成一个可用的map的函数,json的value类型可能不定

Slice专题

455. 内置cap函数可以用于?【arrary slice channel】的capability计算

为什么map不能用cap来计算:

  • map因为有bucket,在内存存放的大小可能不和make出来的大小一致。是编译器计算后的结果,

463. 切片扩容机制

扩容是为切片分配新的内存空间并复制原切片中元素的过程。
先确定新的切片大致容量而分配内存空间,根据该切片当前容量选择不同的策略:
【旧】

  • 如果期望容量大于当前容量的两倍,就会使用期望容量
  • 如果当前切片的长度小于 1024,容量就会翻倍
  • 如果当前切片的长达大于 1024,每次扩容 25% 的容量,直到新容量大于期望容量。

roundupsize 函数来确定待申请的内存,该函数会从一个数组中获取整数,使用这个数组中的元素可以提高内存分配效率并减少碎片,这个数组叫做 NumSizeClasses 。

520. Slice为什么不是线程安全的

因为他是引用类型,其他指针可以同时指向底层数组,而且没有同步的措施

443. slice底层,内存泄漏分析

1)发生场景:截取长slice中的一段导致长slice未释放。

由于底层都是数组,如果截图长slice的一段,其实相当于引用了底层数组中的一小段。只要还有引用,golang的gc就不能回收数组。这种情况导致未使用的数组空间,未及时回收。

解决方案:新建一个长度为0的slice,将需要的一小段slice使用append方法添加到新的slice。再将原来的slice置为nil。

2)发生场景:没有重置丢失的子切片元素中的指针

没有及时将不再使用的slice置为nil

解决方案:如果slice中包含很多元素,再只有一小部分元素需要使用的情况下。建议重新分配一个slice将需要保留的元素加入其中,将原来的长slice整个置为nil。

Map专题

32. 如何实现一个线程安全的map

https://github.com/guowei-gong/go-demo/blob/main/mutex/demo.go

  1. 加读写锁
  2. 分片加锁
  3. sync.Map(很少用)
    • 场景一:只会增长的缓存系统,一个 key 值写入一次而被读很多次;
    • 场景二:多个 goroutine 为不相交的键读、写和重写键值对。
  4. channel做串行访问:通过将 map 的读写操作发送到一个单独的 Goroutine 中,使得对 map 的访问变成串行的,从而避免了竞态条件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package main

    var m = make(map[int]int)
    var ch = make(chan func())

    func read(key int) int {
    var result int
    ch <- func() {
    result = m[key]
    }
    return result
    }

    func write(key, val int) {
    ch <- func() {
    m[key] = val
    }
    }

    func main() {
    write(1, 10)
    value := read(1)
    println(value) // 输出 10
    }

34. Map的底层实现

使用Hash表和搜索树作为底层实现,底层是一个hmap和一个bmap

  • bmap被称之为“桶”。一个桶里面会最多装 8 个 key,key 经过哈希计算后,哈希结果是“一类”的将会落入到同一个桶中。在桶内,会根据key计算出来的hash值的高 8 位来决定key到底落入桶内的哪个位置。
  • 这也是为什么map无法使用cap()来求容量的关键原因:map的容量是编译器进行计算后得出的一个结果,由于桶的存在,map在内存中实际存放的大小不一定同make出来后的map的大小一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type hmap struct {
count int //元素个数,调用len(map)时直接返回
flags uint8 //标志map当前状态,正在删除元素、添加元素.....
B uint8 //单元(buckets)的对数 B=5表示能容纳32个元素
noverflow uint16 //单元(buckets)溢出数量,如果一个单元能存8个key,此时存储了9个,溢出了,就需要再增加一个单元
hash0 uint32 //哈希种子
buckets unsafe.Pointer //指向单元(buckets)数组,大小为2^B,可以为nil
oldbuckets unsafe.Pointer //扩容的时候,buckets长度会是oldbuckets的两倍
nevacute uintptr //指示扩容进度,小于此buckets迁移完成
extra *mapextra //与gc相关 可选字段
}
// A bucket for a Go map.
type bmap struct {
tophash [bucketCnt]uint8
}

//实际上编译期间会生成一个新的数据结构
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}

36. map的key可以是哪些类型,可以是nil吗?nil不可以,嵌套不可以

  • 可以作为 map 的键的类型必须满足相等性比较的条件,包括基本数据类型和一些自定义类型,string必然可以
  • 不可以做key的类型:切片,函数,包含切片和函数的符合类型

36. struct{} interface{} nil可以做map的key吗

  • nil不可以,其他的可以
  • struct{} 以值的字面量形式去比较
  • interface{} 以动态类型去比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
m := make(map[interface{}]int)

m[1] = 10
m["string"] = 20
m[3.14] = 30
fmt.Println(m[1]) // 输出 10
fmt.Println(m["string"]) // 输出 20
fmt.Println(m[3.14]) // 输出 30
//
m := make(map[struct{}]int)

key1 := struct{}{}
key2 := struct{}{}
m[key1] = 10
m[key2] = 20
fmt.Println(m[key1]) // 输出 10
fmt.Println(m[key2]) // 输出 20

251. sync.Map 怎么解决线程安全问题?源码看过吗

支持并发读写,采取了 “空间换时间” 的机制,冗余了两个数据结构,分别是:read 和 dirty.

  • 优点是读多写少场景下使用,比如只会增长的缓存

  • 缺点是写多场景下,导致 read map 缓存失效,需要加锁,冲突变多,性能急剧下降

  • 和原始map+RWLock的实现并发的方式相比,减少了加锁对性能的影响。它做了一些优化:可以无锁访问read map,而且会优先操作read map,倘若只操作read map就可以满足要求,那就不用去操作write map(dirty),所以在某些特定场景中它发生锁竞争的频率会远远小于map+RWLock的实现方式

1
2
3
4
5
6
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}

275 map的分段锁拆了几个分片?

379. 如果一个map没申请空间,去向里面取值【发生panic】

一般用的时候就给他make一个stuct{}{}空类型

407. map取一个key,然后修改这个值,原来的数据会发生变化吗?【会】引用类型

526. map的负载因子是多少【6.5】为什么?

默认当 map 中的元素个数达到总容量的 65% 时,会触发扩容操作。
为什么?

channel

9.同一个协程里面,对无缓冲channel同时进行读写会发生什么问题

原则上不可以这样写,会导致死锁。

对于一个无缓冲的channel而言,只有不同的协程之间一方发送数据一方接受数据才不会阻塞。channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。

14.向为nil的channel发送数据会怎样【发生panic】为什么

连续两次close(ch)会发生panic: runtime err

51. channel 线程安全吗【安全】里面有互斥锁

互斥锁是如何起作用的

在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据。

【重点】分布式session如何实现

https://juejin.cn/post/6965665934165950495

golang是用redis实现的,启动redis-trib.rb

spring cloud可以用原生的分布式session支持

98/99. 分布式锁有哪些?如何用channel实现?

基于数据库的分布式锁

使用数据库的事务特性来实现分布式锁,通过在数据库中创建一个唯一索引或者唯一约束来保证锁的唯一性。

基于Redis的分布式锁,非阻塞的trylock

使用 Redis 提供的 SETNX(SET if Not eXists)指令,可以在 Redis 中创建一个分布式锁。

基于etcd的分布式锁,阻塞的等待lock

使用 etcd 提供的分布式锁实现,可以实现分布式系统中的互斥访问。

基于ZooKeeper的分布式锁

使用 ZooKeeper 提供的临时有序节点和监视机制,可以实现分布式锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import "fmt"

var (
lockCh = make(chan struct{}, 1) // 带缓冲的 channel,容量为1表示只能同时有一个 Goroutine 获取到锁
locked = false // 标记是否已经获取到锁
)
func acquireLock() bool {
select {
case lockCh <- struct{}{}:
locked = true
return true
default:
return false
}
}
func releaseLock() {
if locked {
<-lockCh
locked = false
}
}
func main() {
if acquireLock() {
defer releaseLock()
// 临界区代码
fmt.Println("Lock acquired!")
} else {
fmt.Println("Failed to acquire lock")
}
}

174. go 里的 sync的Lock 和 channel 的性能区别

134. channel实现一个排序算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import "fmt"

func bubbleSort(nums []int, ch chan int) {
n := len(nums)
for i := 0; i < n-1; i++ {
swapped := false
for j := 0; j < n-i-1; j++ {
if nums[j] > nums[j+1] {
nums[j], nums[j+1] = nums[j+1], nums[j]
swapped = true
}
}
if !swapped {
break
}
}
ch <- 1 // 排序完成,向通道发送信号
}
func main() {
nums := []int{4, 3, 1, 5, 2}

ch := make(chan int)

go bubbleSort(nums[:len(nums)/2], ch) // 在一个 Goroutine 中排序前半部分
go bubbleSort(nums[len(nums)/2:], ch) // 在另一个 Goroutine 中排序后半部分

// 等待两个 Goroutine 完成
<-ch
<-ch

// 合并两个有序数组
merged := merge(nums[:len(nums)/2], nums[len(nums)/2:])
fmt.Println(merged)
}
func merge(left, right []int) []int {
result := make([]int, len(left)+len(right))
i, j, k := 0, 0, 0

for i < len(left) && j < len(right) {
if left[i] <= right[j] {
result[k] = left[i]
i++
} else {
result[k] = right[j]
j++
}
k++
}
for i < len(left) {
result[k] = left[i]
i++
k++
}
for j < len(right) {
result[k] = right[j]
j++
k++
}
return result
}

202. channel 实现一个限流器

在每次循环中,我们先向 limiter channel 发送当前时间,如果 channel 已满(达到了最大并发数),则会阻塞等待。接着,启动一个新的协程来执行操作,当操作执行完毕后,通过匿名函数中的 defer 语句从 channel 中接收一个值,表示释放一个协程的位置。

473. channel的ring buffer

适用FIFO,recvx 指向最早被读取的数 据,sendx 指向再次写入时插入的位置

go的同步库

15. sync.waitgroup的坑

① Add一个负数

如果计数器的值小于0会直接panic

② Add在Wait之后调用

比如一些子协程开头调用Add结束调用Wait,这些 Wait无法阻塞子协程。正确做法是在开启子协程之前先Add特定的值。

③ 未置为0就重用

WaitGroup可以完成一次编排任务,计数值降为0后可以继续被其他任务所用,但是不要在还没使用完的时候就用于其他任务,这样由于带着计数值,很可能出问题。

④ 复制waitgroup

WaitGroup有nocopy字段,不能被复制。也意味着WaitGroup不能作为函数的参数。

18. 读写锁怎么实现的

  1. 读写锁内部是通过互斥锁实现的,主要应用于写操作少,读操作多的场景。
  2. goroutine获得写锁时,其他读写都会阻塞,读锁相互之间不会阻塞,当所有读锁释放后,才可以获取写锁
    读锁可重入,写锁不可重入
    原理:
    1
    2
    1. 基于一个计数器和两个队列
    2. RLOCK WLOCK

114. mutex 如何处理正常和饥饿状态?rwmutex中写操作如何处理写操作阻止读操作

正常模式和饥饿状态:

  • 正常状态下,Mutex 的锁是公平的,当一个 Goroutine 尝试获取锁时,如果锁已经被其他 Goroutine 持有,那么它将被放入一个等待队列中,直到锁被释放。
  • 饥饿状态指的是某些 Goroutine 一直无法获得锁,而其他 Goroutine 不断获得锁的情况。在 Go 中,sync.Mutex 并没有专门的机制来处理饥饿状态。如果出现饥饿状态,通常是由于程序逻辑设计不合理导致的,可能需要重新考虑并发结构和资源的设计。

读写操作

  • 如果一个 Goroutine 持有写锁,那么其他 Goroutine 将无法获取读锁,直到写锁被释放。这种机制保证了在写操作进行时,不会有其他 Goroutine 进行并发的读取操作,从而避免了数据的并发写入。

33. go的锁是可重入的吗【不是】

可重入锁(也称为递归锁)是指允许同一个线程或 Goroutine 多次获取同一个锁,而不会发生死锁的情况。这在一些场景下是很有用的,比如在一个函数中多次调用其他需要锁保护的函数

但是go的sync.Mutex不是可重入。

210. 如何检测死锁的?

  • go vet 进行静态分析
  • go run/build -race 可以检测死锁,在编译好静态文件后

211. 怎么处理锁分段(Lock Sharding)

  1. 锁分段是将一个大锁拆分成多个小锁,每个小锁只保护一部分共享资源,从而减小锁的粒度,提高并发度。
  2. 使用锁分段的优点在于它可以将大锁拆分成多个小锁,提高并发性能,特别是在高并发的情况下。然而,需要注意的是在设计分段锁时,需要考虑到资源的访问模式和分段的粒度,以避免出现性能瓶颈或竞争条件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    type LockShard struct {
    locks []sync.Mutex
    }
    //初始化锁分段init lock shard
    func NewLockShard(numShards int) *LockShard {
    shard := &LockShard{
    locks: make([]sync.Mutex, numShards),
    }
    return shard
    }
    //确定分段索引
    func (ls *LockShard) GetShardIndex(key string) int {
    // 根据 key 计算出分段索引
    // ...
    return shardIndex
    }
    // 取和释放分段锁
    func (ls *LockShard) Lock(key string) {
    shardIndex := ls.GetShardIndex(key)
    ls.locks[shardIndex].Lock()
    }

    func (ls *LockShard) Unlock(key string) {
    shardIndex := ls.GetShardIndex(key)
    ls.locks[shardIndex].Unlock()
    }

226. sync.mutex的底层实现(Linux)

  • 实现可能因操作系统和硬件平台而异
  • 使用 pthreads 库(POSIX 线程库)中的互斥锁实现。
  • 这是一个用户态的锁,它会使用操作系统提供的系统调用来进行加锁和解锁
  • mutex维护一个state,类型是int32

提供了两种锁定方式:阻塞锁和自旋锁

  • 阻塞锁:当一个 Goroutine 尝试获取一个被其他 Goroutine 持有的锁时,它会被阻塞,直到锁被释放。
  • 自旋锁:自旋锁是一种非阻塞的锁机制,在尝试获取锁时,如果锁已经被其他 Goroutine 持有,它会在一段时间内快速尝试获取锁,而不是被阻塞。如果在一定时间内无法获取到锁,那么它会转为阻塞模式。

mutex允许自旋的条件是什么【执行状态的M个数< mapprocs】

开发者可以使用 runtime.GOMAXPROCS() 函数来设置 Goroutine 的最大并发数,从而影响自旋锁的行为。

goroutine使用

20. 两个goroutine交替打印字母和数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
)

func main() {
limit := 26

numChan := make(chan int, 1)
charChan := make(chan int, 1)
mainChan := make(chan int, 1)
charChan <- 1

go func() {
for i := 0; i < limit; i++ {
<-charChan
fmt.Printf("%c\n", 'a'+i)
numChan <- 1

}
}()
go func() {
for i := 0; i < limit; i++ {
<-numChan
fmt.Println(i)
charChan <- 1

}
mainChan <- 1
}()
<-mainChan
close(charChan)
close(numChan)
close(mainChan)
}

26. 为什么不要大量使用goroutine

  • 上下文切换,开销变大
  • 可能会存在内存泄漏的问题,得
    要根据具体的情况来评估并发的需求,避免不必要的并发,以免引入不必要的复杂性和潜在的问题。

协程池如何实现 worker

40. for 循环多次执行goroutine 有什么坑?

  • go支持闭包, 如果用了循环的这个i,里面的变量就可能出错,用临时变量的副本比较好。
  • Goroutine 是异步执行的,它们可能会在循环变量发生变化之后才开始执行,导致不确定的结果

48. 如果要等待所有goroutine结束,怎么做?【使用waitgroup】

55. goroutine为什么轻量

独立的栈空间 每个 Goroutine 都有自己独立的栈空间,相对于传统的线程来说,Goroutines 的栈空间通常会小很多。这使得创建和销毁 Goroutines 更加快速和节省内存。

灵活的调度器 Go 的运行时(runtime)包含了一个高效的调度器,它可以在多个操作系统线程上调度 Goroutines,以便充分利用多核处理器的优势。这使得在单个程序中可以同时执行大量的 Goroutines,而不会导致线程过度切换和资源浪费。

快速的启动和停止 相对于传统的线程,创建和销毁 Goroutines 更加快速。这使得在需要短暂执行某些任务时,使用 Goroutines 更为合适。

共享的堆空间 所有 Goroutines 共享相同的堆空间,这意味着它们可以相对容易地共享数据,而不需要显式的同步机制。

通信通过通道 Goroutines 之间的通信主要依赖于通道(channel),它们提供了一种安全且高效的方式来传递数据。通过通道,可以实现 Goroutines 之间的同步和数据传递,而无需显式的锁。

自动的垃圾回收 Go 具有垃圾回收机制,它会自动管理内存的分配和释放,使得开发者无需手动管理内存,降低了并发程序中内存泄漏的风险。

85. 使用两个channel实现a+b

92. goroutine的实现方式

100. 并行goroutine如何实现

111. 父 goroutine 退出,如何使得子goroutine也退出【waitgroup 用channel ,defer】

  • 父 Goroutine 退出时,只要main不退出,所有的子 Goroutines 不会强制关闭

Go的GC机制

24. go的gc什么是否触发

主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。

被动触发,分为两种方式:

  • 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC。
  • 使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。

148. Go 语言什么时候垃圾回收,写代码时,如何减少对象分配

对象池:可以使用 sync.Pool 或者自定义对象池来重用对象,避免频繁分配和释放。

必要时使用数组而不是切片:如果你知道元素数量固定,可以使用数组而不是切片,因为切片底层数组可能会导致对象分配。

避免逃逸:逃逸发生在编译器无法确定一个变量的生命周期时,变量将会在堆上分配。尽量避免函数内部的变量逃逸到堆上

使用内置函数:Go 提供了一些内置函数(如 append、copy)来处理切片,它们会在底层做一些优化,避免不必要的分配。

176. Golang 内存分配和管理

Go语言内置运行时(就是runtime),抛弃了传统的内存分配方式,改为自主管理。这样可以自主地实现更好的内存使用模式,比如内存池、预分配等等。这样,不会每次内存分配都需要进行系统调用。

Golang运行时的内存分配算法主要源自 Google 为 C 语言开发的TCMalloc算法,全称Thread-Caching Malloc。

核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。

管理如何管理?

Go自带GC,可以自动回收垃圾,对比C语言不用malloc申请内存及free释放,Go的GC采取三色标记法动态;

Go自动分配内存,开发者可以不用关注堆、栈,Go在编译阶段会做变量的生命周期分析做逃逸分析,自动将变量分配在堆或栈上。

354. go的内存分配机制

Go 的内存分配借鉴了 Google 的 TCMalloc 分配算法,其核心思想是内存池 + 多级对象管理。内存池主要是预先分配内存,减少向系统申请的频率;多级对象有:mheap、mspan、arenas、mcentral、mcache。
它们以 mspan 作为基本分配单位。具体的分配逻辑如下:

  • 当要分配大于 32K 的对象时,从 mheap 分配。
  • 当要分配的对象小于等于 32K 大于 16B 时,从 P 上的 mcache 分配,如果 mcache 没有内存,则从 mcentral 获取,如果 mcentral 也没有,则向 mheap 申请,如果 mheap 也没有,则从操作系统申请内存。
  • 当要分配的对象小于等于 16B 时,从 mcache 上的微型分配器上分配。

    324. go的内存分配机制中,有mcentral为什么要mcache

177. 如何避免内存逃逸【合理用指针,设定slice长度】

  1. 不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。
  2. 预先设定好slice长度,避免频繁超出容量,重新分配。
  3. 一个经验是,指针指向的数据大部分在堆上分配的,请注意。

出现内存逃逸的情况有:

  1. 发送指针或带有指针的值到channel,因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放。

  2. 在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。

  3. 切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。

  4. 调用接口类型时,接口类型的方法调用是动态调度,实际使用的具体实现只能在运行时确定,如一个接口类型为io.Reader的变量r,对r.Read(b)的调用将导致r的值和字节片b的后续转义并因此分配到堆上。

  5. 在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,导致内存溢出。

    237. gc和 delete free 有什么区别,优势?

  • delete free是手动释放的,一有忘记的就可能导致内存泄漏,产生内存碎片
  • gc的话是自动释放堆的内存,能有效避免内存泄漏和内存碎片,没有垃圾回收的情况下,程序员可能需要手动释放不再使用的内存,以避免内存碎片的问题
    • 内存碎片是指分配在堆上的内存块中,由于频繁的分配和释放操作,导致堆中的可用内存呈现出碎片化的状态,使得大块的连续内存难以分配。

355. go的性能调优是怎么做的

内存优化

A、将小对象合并成结构体一次分配,减少内存分配次数

Go runtime底层采用内存池机制,每个span大小为4k,同时维护一个cache。cache有一个0到n的list数组,list数组的每个单元挂载的是一个链表,链表的每个节点就是一块可用的内存块,同一链表中的所有节点内存块都是大小相等的;但是不同链表的内存大小是不等的,即list数组的一个单元存储的是一类固定大小的内存块,不同单元里存储的内存块大小是不等的。cache缓存的是不同类大小的内存对象,申请的内存大小最接近于哪类缓存内存块时,就分配哪类内存块。当cache不够时再向spanalloc中分配。

B、缓存区内容一次分配足够大小空间,并适当复用

在协议编解码时,需要频繁地操作[]byte,可以使用bytes.Buffer或其它byte缓存区对象。
bytes.Buffer等通过预先分配足够大的内存,避免当增长时动态申请内存,减少内存分配次数。对于byte缓存区对象需要考虑适当地复用。
C、slice和map采make创建时,预估大小指定容量
slice和map与数组不一样,不存在固定空间大小,可以根据增加元素来动态扩容。
slice初始会指定一个数组,当对slice进行append等操作时,当容量不够时,会自动扩容:
如果新的大小是当前大小2倍以上,则容量增涨为新的大小;
否则循环以下操作:如果当前容量小于1024,按2倍增加;否则每次按当前容量1/4增涨,直到增涨的容量超过或等新大小。
map的扩容比较复杂,每次扩容会增加到上次容量的2倍。map的结构体中有一个buckets和oldbuckets,用于实现增量扩容:
正常情况下,直接使用buckets,oldbuckets为空;
如果正在扩容,则oldbuckets不为空,buckets是oldbuckets的2倍,
因此,建议初始化时预估大小指定容量

D、长调用栈避免申请较多的临时对象

Goroutine的调用栈默认大小是4K(1.7修改为2K),采用连续栈机制,当栈空间不够时,Go runtime会自动扩容:
当栈空间不够时,按2倍增加,原有栈的变量会直接copy到新的栈空间,变量指针指向新的空间地址;
退栈会释放栈空间的占用,GC时发现栈空间占用不到1/4时,则栈空间减少一半。
比如栈的最终大小2M,则极端情况下,就会有10次的扩栈操作,会带来性能下降。
因此,建议控制调用栈和函数的复杂度,不要在一个goroutine做完所有逻辑;如的确需要长调用栈,而考虑goroutine池化,避免频繁创建goroutine带来栈空间的变化。

E、避免频繁创建临时对象

Go在GC时会引发stop the world,即整个情况暂停。Go1.8最坏情况下GC为100us。但暂停时间还是取决于临时对象的个数,临时对象数量越多,暂停时间可能越长,并消耗CPU。
因此,建议GC优化方式是尽可能地减少临时对象的个数:尽量使用局部变量;所多个局部变量合并一个大的结构体或数组,减少扫描对象的次数,一次回尽可能多的内存。

并发优化

A、高并发的任务处理使用goroutine池
Goroutine虽然轻量,但对于高并发的轻量任务处理,频繁来创建goroutine来执行,执行效率并不会太高,因为:过多的goroutine创建,会影响go runtime对goroutine调度,以及GC消耗;高并发时若出现调用异常阻塞积压,大量的goroutine短时间积压可能导致程序崩溃。
B、避免高并发调用同步系统接口
goroutine的实现,是通过同步来模拟异步操作。
网络IO、锁、channel、Time.sleep、基于底层系统异步调用的Syscall操作并不会阻塞go runtime的线程调度。
本地IO调用、基于底层系统同步调用的Syscall、CGo方式调用C语言动态库中的调用IO或其它阻塞会创建新的调度线程。
网络IO可以基于epoll的异步机制(或kqueue等异步机制),但对于一些系统函数并没有提供异步机制。例如常见的posix api中,对文件的操作就是同步操作。虽有开源的fileepoll来模拟异步文件操作。但Go的Syscall还是依赖底层的操作系统的API。系统API没有异步,Go也做不了异步化处理。
因此,建议:把涉及到同步调用的goroutine,隔离到可控的goroutine中,而不是直接高并的goroutine调用。
C、高并发时避免共享对象互斥
传统多线程编程时,当并发冲突在4~8线程时,性能可能会出现拐点。Go推荐不通过共享内存来通信,Go创建goroutine非常容易,当大量goroutine共享同一互斥对象时,也会在某一数量的goroutine出在拐点。
因此,建议:goroutine尽量独立,无冲突地执行;若goroutine间存在冲突,则可以采分区来控制goroutine的并发个数,减少同一互斥对象冲突并发数。

其它优化

A、避免使用CGO或者减少CGO调用次数
GO可以调用C库函数,但Go带有垃圾收集器且Go的栈动态增涨,无法与C无缝地对接。Go的环境转入C代码执行前,必须为C创建一个新的调用栈,把栈变量赋值给C调用栈,调用结束现拷贝回来。调用开销较大,需要维护Go与C的调用上下文,两者调用栈的映射。相比直接的GO调用栈,单纯的调用栈可能有2个甚至3个数量级以上。
因此,建议:尽量避免使用CGO,无法避免时,要减少跨CGO的调用次数。
B、减少[]byte与string之间转换,尽量采用[]byte来字符串处理
GO里面的string类型是一个不可变类型,GO中[]byte与string底层是两个不同的结构,转换存在实实在在的值对象拷贝,所以尽量减少不必要的转化。
因此,建议:存在字符串拼接等处理,尽量采用[]byte。
C、字符串的拼接优先考虑bytes.Buffer
string类型是一个不可变类型,但拼接会创建新的string。GO中字符串拼接常见有如下几种方式:
string + 操作 :导致多次对象的分配与值拷贝
fmt.Sprintf :会动态解析参数,效率好不哪去
strings.Join :内部是[]byte的append
bytes.Buffer :可以预先分配大小,减少对象分配与拷贝
因此,建议:对于高性能要求,优先考虑bytes.Buffer,预先分配大小。fmt.Sprintf可以简化不同类型转换与拼

500. 写屏障-插入写屏障-删除写屏障-混合写屏障

  • 混合写屏障继承了插入写屏障的优点,起始无需 STW 打快照,直接并发扫 描垃圾即可;
  • 混合写屏障继承了删除写屏障的优点,赋值器是黑色赋值器,GC 期间,任 何在栈上创建的新对象,均为黑色。扫描过一次就不需要扫描了,这样就 消除了插入写屏障时期最后 STW 的重新扫描栈;
  • 混合写屏障扫描精度继承了删除写屏障,比插入写屏障更低,随着带来的 是 GC 过程全程无 STW;
  • 混合写屏障扫描栈虽然没有 STW,但是扫描某一个具体的栈的时候,还是 要停止这个 goroutine 赋值器的工作(针对一个 goroutine 栈来说,是 暂停扫的,要么全灰,要么全黑哈,原子状态切换)。

505. gc流程

GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障

STWGCMark 扫描标记阶段,与赋值器并发执行,写屏障开启并发

GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障

GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭

GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭。

506. GC是如何调优的

Go 内存分配机制?

Go 内存逃逸机制?

Go 内存对齐机制

563. waitgroup的底层实现

1
2
3
4
5
// A WaitGroup must not be copied after first use.
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}

564. cond实现原理

run -race能用于什么【排查逃逸,死锁,数据竞争等】

go其他

17. 不重启实现热更新

根据系统的 SIGHUP 信号量,以此信号量触发进程重启,达到热更新的效果。

热部署我们需要考虑几个能力:

  • 新进程启动成功,老进程不会有资源残留
  • 新进程初始化的过程中,服务不会中断
  • 新进程初始化失败,老进程仍然继续工作
  • 同一时间,只能有一个更新动作执行
    监听信号量的方法的环境是在 类 UNIX 系统中,在现在的 UNIX 内核中,允许多个进程同时监听一个端口。在收到 SIGHUP 信号量时,先 fork 出一个新的进程监听端口,同时等待旧进程处理完已经进来的连接,最后杀掉旧进程。

示例代码,仓库地址:https://github.com/guowei-gong/tablefilp-example, 如果你希望动手来加深印象可以打开看看。

157. 日志框架logrus

go实现stack 和 set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Stack struct {
data []interface{}
}
func (s *Stack) Push(item interface{}) {
s.data = append(s.data, item)
}

func (s *Stack) Pop() interface{} {
if len(s.data) == 0 {
return nil
}
item := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return item
}
1
2
3
4
5
6
7
8
9
10
11
12
13
type Set map[interface{}]struct{}
func (s Set) Add(item interface{}) {
s[item] = struct{}{}
}

func (s Set) Remove(item interface{}) {
delete(s, item)
}

func (s Set) Contains(item interface{}) bool {
_, exists := s[item]
return exists
}

200. 项目上线了,但是发现协程/内存泄漏,如何处理

1.goroutine泄漏。
2.有一些全局的数据结构意外的挂住了本该释放的对象,虽然goroutine已经退出了,但是这些对象并没有从这类数据结构中删除,导致对象一直被引用,无法被收回。
所以发现有内存泄漏的话,具体问题具体分析。

RPC基础

讲一下RPC基础:

RPC的概念
RPC(Romote Procedure Call,远程过程调用),作为分布式系统中不同节点之间的通信方式,是分布式系统的基石之一,RPC不是具体的方法,而是一种解决不同服务之间调用的设计。

基于RPC开发的框架可以称为RPC框架,典型的有谷歌的gRPC、阿里的Dubbo、Facebook的Thrift等,当然成熟的RPC框架还会有服务注册与发现、服务治理、负载均衡等功能。

RPC的四个要素
Client
服务调用的发起方

Client Stub
用于存储要调用的服务器地址、以及将要请求的数据信息打包,通过网络请求发送给Server Stub,然后阻塞,直到接受到返回的数据,然后进行解析。

Server
Server,包含要调用的方法

Server Stub
用于接受Client Stub发送的请求数据包并进行解析,完成功能调用,最后将结果进行打包并返回给Client Stub。在没有接受到请求数据包时则处于阻塞状态。

封装了Client Stub和Server Stub后,从Client的角度来看,似乎和本地调用一样。从Server的角度看,似乎就是客户直接调用。

RPC的具体通信步骤
Client以类似本地调用的方式调Client Stub
Client Stub序列化生成消息,然后调用本地操作系统的通信模块, Stub阻塞
本地操作系统与远程Server进行通信,消息传输到远程操作系统
远程操作系统将消息传递给Server Stub
Server Stub进行反序列化,然后调用Server的对应方法
Server程序执行方法,将结果传递给Server Stub
Server Stub将结果进行序列化,然后传递给Server操作系统
Server操作系统将结果传递给Client
Client操作系统将其交给Client Stub, Stub从阻塞状态恢复
Client Stub对结果进行反序列化,并将值返回给Client程序
Client程序获得返回结果

乐观锁

乐观锁的概念其实很简单,就是在操作一个共享变量时,我们先认为多个线程之间没有冲突

CAS

CAS是乐观锁的一种实现,CAS全称是比较和替换,CAS的操作主要由以下几个步骤组成:

  1. 先查询原始值
  2. 操作时比较原始值是否修改
  3. 如果修改,则操作失败,禁止更新操作,如果没有发生修改,则更新为新值

    CAS的缺点

    CAS虽然在低并发量的情况下可以减少系统的开销,但是CAS也有一些问题:
  4. CPU开销过大问题
  5. ABA问题
  6. 只能针对一个共享变量

    CPU开销过大

    在我们使用CAS时,如果并发量过大,我们的程序有可能会一直自旋,长时间占用CPU资源。

    ABA问题

    假设有个共享变量J,原始值为1。
  7. 线程A读取变量J,值为1
  8. 线程B读取变量J,值为1
  9. 线程A变量J+1,CAS成功从1修改为2
  10. 线程C读取变量J,值为2
  11. 线程C将变量J-1,CAS成功从2修改为1
  12. 线程A通过CAS比较和替换,依然可以改为自己想修改的值
    上述过程,线程B和C已经将变量J的值已经改变了,但是线程A无法发现,依然可以修改共享变量,这就产生了ABA问题。

    共享变量单一

    CAS操作单个共享变量的时候可以保证原子的操作,无法操作多个变量。
    但是在JDK5之后,AtomicReference可以用来保证对象之间的原子性,我们可以把多个对象放入CAS中操作。

如何防止CAS的ABA

四个字:加标志位(version)。
至于标志位可以是自增的数字,也可以是时间戳。通过标志位我们可以精确的知道每次修改。

go python java的协程区别

python协程特点

  1. 单线程内切换,适用于IO密集型程序中,能够最大化IO多路复用的效果。
  2. 没法利用多核。
  3. 协程间彻底同步,不会并行。不须要考虑数据安全。
  4. 关键词yield

java协程特点

go协程特点

  1. 协程间须要保证数据安全,好比经过channel或锁。
  2. 能够利用多核并行执行。
  3. 协程间不彻底同步,能够并行运行,具体要看channel的设计。
  4. 抢占式调度,可能没法实现公平。

    三者区别

  5. java引入了一个虚拟线程的东西,然后结合Thread库的VirtualThread来创建,用JVM管理,用共享内存的方式实现的通信,本质上还是原生thread那一套,golang是原生支持goroutine机制来支持并发的,runtime/GMP模型管理的,python用一个asyncio库实现并发的,awaitable对象实现的通信

性能排查

  1. 使用性能分析工具(如pprof)来获取详细的性能数据,了解哪些函数或代码段消耗了大量的时间。
  2. 检查缓存、连接、数据库等
  3. 日志里也可能有信息
    ##
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    func Goroutine5() {
    //ctx := context.Background()
    //quere := make(chan int,1000)
    q1 := make(chan int)
    q2 := make(chan int)
    q3 := make(chan int)
    q4 := make(chan int)
    q5 := make(chan int)
    ch1 := make(chan struct{})
    ch2 := make(chan struct{})
    ch3 := make(chan struct{})
    ch4 := make(chan struct{})
    ch5 := make(chan struct{})
    suc := make(chan struct{})

    oper := func(sort int, in, out chan struct{}, q chan int) {
    //defer wg.Done()
    //fmt.Println(sort)
    for i := range q {
    <-in
    fmt.Println(sort, " print : ", i)
    //_, ok := <-q
    //if ok {
    //fmt.Println("sl ", sort)
    out <- struct{}{}
    //}

    }
    //_, ok := <-out
    //if ok {
    // fmt.Println("close --------", sort)
    // close(out)
    //}
    //close(in)
    fmt.Println("close ", sort)
    }
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
    //defer wg.Done()
    go func() {
    ch1 <- struct{}{}
    }()
    for i := 1; i <= 10; i++ {
    //fmt.Println(i, "==========")
    switch i % 5 {
    case 1:
    q1 <- i
    case 2:
    q2 <- i
    case 3:
    q3 <- i
    case 4:
    q4 <- i
    case 0:
    q5 <- i
    }
    //fmt.Println("s")
    }
    close(q1)
    close(q2)
    close(q3)
    close(q4)
    close(q5)
    suc <- struct{}{}

    close(suc)
    //close(ch1)
    fmt.Println("ws")
    }()
    go func() {
    defer wg.Done()
    for s := range suc {

    fmt.Println(s)
    for i := range ch1 {
    fmt.Println(i)
    close(ch1)
    }
    }
    }()
    go oper(1, ch1, ch2, q1)
    go oper(2, ch2, ch3, q2)
    go oper(3, ch3, ch4, q3)
    go oper(4, ch4, ch5, q4)
    go oper(5, ch5, ch1, q5)
    wg.Wait()

    fmt.Println("suc", <-ch1)