golang

map

实现:哈希查找表,通过链表法解决哈希冲突

  • 随机遍历,顺序无法预测
  • 扩容特点:逐步进行,新,旧bucket
    • 遍历中出现扩容
    • 遍历前已经开始扩容

slice

底层结构:本质上是引用类型,底层结构有三个变量:len,cap,底层数组的指针

  • 空切片时,cap=len=0
  • 一般初始化方式a:=[]int{}cap=len=初始化长度
  • 使用make进行初始化:a:=make([]int,4,5):cap=5,len=4,a为[0,0,0,0]
  • var a []int默认cap=len=0

append操作

  • cap足够,直接修改len,追加
  • cap不足时…
  • cap足够时,一次追加多个和多次追加一个结果相同,cap不足时会出现不一样的结果(cap的结果不同,多次追加一个的情况可能会更大)

切片截取操作:

  • 切片进行截取时,数组容量的末尾和原切片数组末尾对齐,数组地址为截取单元的首地址,且可以超过其len的范围进行截取,当其中一个数组发生扩容时,另一个数组的地址不变

切片扩容时会改变:cap和数字指针

垃圾回收

  • 引用计数,0回收,无法处理 循环引用
  • 标记-清除: 需要STW
  • 分代收集:按照生存周期老/新不同的算法,效率高,算法设计复杂
  • 三色标记法:白-垃圾,灰-遍历到的对象,黑,遍历灰色,标记为黑色并将引用的变量标记为灰色,直到没有灰色
    • 每次GC循环时,不需要将所有对象移动到白色区域,只要将黑色和白色的颜色互换即可,更高效
  • STW:影响性能<1ms
  • 写屏障技术缩短STW
    • 引用对象丢失:黑色节点添加了指向白色结点的引用,但无法被扫描
    • 破坏以下二者之一:
      • dijistra写屏障(强三色不变性),不允许黑色节点引用白色节点,引用则将白色节点改为灰色
      • yuasa写屏障(弱三色不变性),白色节点被删除了一个引用时,认为会被黑色节点新增引用(悲观),设置为灰色

go的垃圾回收器是和主程序并行的,关键在于三色标记法能让系统的gc暂停时间能够预测

GPM调度和CSP模型

不要以共享内存的方式来通信,要以通信的方式来共享内存

  • CSP模型:以通信的方式共享内存(channel进行通信)
  • GPM含义

    • G:go协程Goroutine
    • M:工作线程,CPU数量
    • P:处理器,用来调度G,M的关联关系,M拥有P才能执行G的代码
  • Goroutine的调度策略

    • 队列轮转:P周期性地调度G到M中运行,一段时间后保存上下文切换(队列)
    • 系统调用:G0即将进入系统调用时,M0释放P,某个M1获得P运行剩下的G,G0结束后等待其他的P调度(或空闲P),此后进入缓存池睡眠

Chan原理(channel)

  type hchan struct {
 qcount   uint  // 队列中的总元素个数
 dataqsiz uint  // 环形队列大小,即可存放元素的个数
 buf      unsafe.Pointer // 环形队列指针
 elemsize uint16  //每个元素的大小
 closed   uint32  //标识关闭状态
 elemtype *_type // 元素类型
 sendx    uint   // 发送索引,元素写入时存放到队列中的位置
 recvx    uint   // 接收索引,元素从队列的该位置读出
 recvq    waitq  // 等待读消息的goroutine队列
 sendq    waitq  // 等待写消息的goroutine队列
 lock mutex  //互斥锁,chan不允许并发读写
}
  • 写数据
    • recvq不为空,缓冲区无数据/无缓冲区,则直接从recvq取出G,写入数据,并把G唤醒
    • 缓冲区有空余位置,写入缓冲区
    • 缓冲区无空余位置,将数据写入G,将当前G加入sendq,进入睡眠被唤醒
  • 读数据
    • sendq不为空,且无缓冲区,直接从sendq去除G,把G数据读走并唤醒
    • sendq不为空(缓冲区已满),从缓冲区首部读出数据,将G中的数据写入缓冲区尾部,唤醒G
    • 缓冲区有数据,从缓冲区读取数据
    • 将当前G加入recvq进入睡眠
  • 关闭channel
    • 唤醒recvq中所有的G,本该写入数据的内容为nil,sendq中的G唤醒(会出现panic)
    • panic出现的场景
      • 关闭值为nil的channel
      • 关闭已经关闭的channel
      • 向已关闭的channel写入数据

无缓冲区情况:读和写同步(会阻塞)

context上下文结构

并发安全,树状的goroutine

只定义了接口

 type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
}
  • deadline:到达ddl自动发起取消请求
  • Done:返回只读的channel,如果可以读取说明已经发出取消信号,可以清理并释放
  • err:返回被取消的原因
  • value:获取context上绑定的值,key-value

竞态与内存逃逸

  • 竞态:在程序中,同一个内存块被多个gorountine访问

    • 解决:对资源进行加锁,sync.Mutex,sync.RWMutex
    • 检测:添加-race
  • 逃逸分析:内存的分配位置由编译器决定,分配速度慢且会形成内存碎片

    以下场景

    • 指针逃逸
    • 栈空间不足逃逸
    • 动态类型逃逸
    • 闭包引用对象逃逸

零碎

安全读写共享变量方式

  • Mutex锁
  • goroutine通过channel

new和make的区别

  • make用来分配和初始化类型为slice,map,chan的数据,new任意数据并返回内存指针
  • make返回引用,即type,分配空间后进行初始,new分配的空间会被清零

对nil的slice和空silce的处理区别

  • slice:=make([]int,0):空slice,不为0
  • slice:=[]int{} 值是nil,保证返回slice的函数异常时仍可保证返回nil

协程,进程,线程的区别

  • 进程: 进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单位。 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

  • 线程: 线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

  • 协程: 协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

golang的内存模型中,为什么小对象多了会加大GC压力

  • 小对象过多会导致GC三色法消耗过多CPU

  • 解决思路:减少对象分配

channel为什么可以做到线程安全

  • 先进先出,本身用来在多任务间传递数据,通过通信共享内存,有天然优势

GC触发条件

  • 手动触发(主动):调用runtime.GC,阻塞式
  • 被动触发:
    • 系统监控,超过两分钟没有GC则触发
    • pacing(步调)算法,控制内存增长的比例,每次分配内存时检测是否以达到阈值,默认100%

goroutine的数量查看和限制

  • 查看: GOMAXPROCS控制未被阻塞的所有goroutine,通过GOMAXPROCE即可查看
  • 限制:使用channel,每次执行goroutine前向通道写入值

Channel是同步还是异步的

​ 异步的

  • channel的状态:
    • nil,初始或手动赋值,无法关闭(panic),send/recv被永久阻塞
    • closed,关闭或send会panic,recv不会阻塞
    • active,可关闭,send/recv

goroutine 和线程的区别

  • 线程可以有多个goroutine
  • 线程,进程都是同步的,协程是异步的
  • 协程可以保留上一次调用的状态
  • 协程需要线程来承载运行,不能代替线程
  • 线程是分割的CPU资源,协程是组织好的代码流程

struct能不能比较

  • 相同struct类型可以
  • 不同struct不能比较,编译不通过

go主协程如何等待其他协程

用sync.WaitGroup,内部实现计数器计数未完成的操作个数,Add()添加计数,Done()计数减一,Wait()等待所有操作结束,计数为0(立即返回)

slice扩容

append追加元素,空间不足则扩容,重新分配slice

扩容规则(只对容量)

  • 小于1024:扩容时翻倍,超过1024,增长因子变为1.25
  • 容量够用则追加,len++
  • 容量不够用,先扩容再追加

map顺序读取

(一般是随机的)

先把map中的key用sort排序后根据key读取

值接收者和指针接收者

方法的接收者

  • 值类型:可以调用值接收者的方法和指针接收者的方法
  • 指针类型:可以调用值接收者的方法和指针接收者的方法

接口实现不同:

  • 值类型接口:类型本身和该类型的指针类型都实现了该接口
  • 指针类型接口:只有对应的指针类型才被认为实现了接口

通常使用指针作为方法的接收者

  • 能够修改接收者指向的值
  • 避免每次调用方法时复制该值,更高效

发生内存泄漏的原因

  • goroutine需要维护用户代码的上下文信息,运行过程中需要消耗一定的内存来保存此类信息,如果一个程序不断产生goroutine,不结束已创建的goroutine并复用该部分内存,会造成内存泄漏

协程泄露:(某段代码卡住,陷入死循环等)应该被释放的协程没有被正确释放

如何检测内存泄漏

自带的工具:pprof,或者用Gops检测当前运行的go程占用的资源

两个nil可能不相等

接口interface是对接口值的封装,内部包含类型T和值V,一个接口为nil当且仅当T=nil,V=unset

两个接口比较时,先比较T再比较V(接口值和非接口值进行比较会将非接口值转化为接口值)

例如var p *int =nil转化为接口值T=*int,显然和值为nil的接口不相等

函数传参是值类型还是引用类型

只存在值传递(值或指针的副本),都会开辟新的空间

不要混淆值传递,引用传递和值类型,引用类型

内存对齐

CPU都是以字长访问的(32位,64位),不进行内存对齐会增加CPU访问内存的次数,内存对齐对实现变量的原子性操作有好处(并发场景下)

两个interface的比较

  • 判断类型是否一样:reflect.TypeOf(a).Kind()
  • 判断接口是否相等:reflect.DeepEqual(a,b,interface{})
  • 将interface赋值给另一个:reflect.ValueOf(a).Elem().Set(reflect.ValueOf(b))

打印%v,%+v,%#v的区别

(输出struct中的元素)

  • %v输出所有的值
  • %+v先输出字段名字,再输出该字段的值(name:value)
  • %#v先输出结构体名字,再输出结构体(name:value)

rune类型

go的字符有以下两种

  • uint8(byte),表示ASCII的字符
  • rune类型,表示UTF-8字符,等价于int32

string底层通过byte数组实现,对string求len计算了字节长度

[]rune(str) :string转化为rune类型,统计长度示例:

  • “hello 你好” string length=12(汉字3个),rune length=8

空struct{}占用的空间

使用unsafe.Sizeof(struct{})=0,不占用任何内存空间

空struct的用途

优点:不占内存空间,通常被当作占位符

  • map作为集合使用(type set map[string]struct{})
  • 不发送数据的channel,只是通知子协程执行任务或控制协程并发(make(chan struct{})
  • 有可能结构体中只包含方法,不含任何字段

变量的分配位置在堆上还是栈上

由编译器决定,如果无法判断变量作用域和大小,通常会分配到堆上(堆上的变量在函数出栈自行释放,无需gc)

select执行

select:多个可用操作:随机选一个

  • select 中只要有一个 case 能 return,则立刻执行
  • 当如果同一时间有多个 case 均能 return 则伪随机方式抽取任意一个执行
  • 如果没有一个 case 能 return 则可以执行”default” 块

array和slice的区别

  • array:固定长度,长度是数组类型的一部分,需指定大小或根据初始化值确定
  • slice:可变长度,三个属性:指针,长度,容量,通过make初始化,可以扩容

defer的作用

调用普通函数或方法前加上关键词defer即可。defer被执行时,defer后面的函数被延迟执行,直到包含该defer语句的函数执行完毕(不管return结束还是panic结束),即为在函数返回之前调用,defer是在return之前完成的

常被用于处理成对的操作,保证资源能被释放等(打开/关闭,连接/断开,加锁/释放锁)

释放资源的defer跟在请求资源的语句后

最后面的defer最先被调用(类似于栈)

return xxx并不是一条原子语句:先给返回值赋值,再调用defer语句,例如:

func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}

返回值为5

func defer_call() {
    defer func() { fmt.Println("打印前") }()
    defer func() { fmt.Println("打印中") }()
    defer func() { fmt.Println("打印后") }()

    panic("触发异常")
}

输出结果

打印后
打印中
打印前
panic:触发异常

go关键字

go func(x,y,z)只有fun(x,y,z)在新的goroutine中运行,参数的计算在原goroutine中完成。

var和:=定义变量的区别

:=只能在声明局部变量使用,var无限制

代码

type student struct {
    Name string
    Age  int
}

func pase_student() {
    m := make(map[string]*student)
    stus := []student{
        {Name: "zhou", Age: 24},
        {Name: "li", Age: 23},
        {Name: "wang", Age: 22},
    }
    for _, stu := range stus {
        m[stu.Name] = &stu
    }
}

错误:for _, stu := range stus {m[stu.Name] = &stu}一句中,stu实际上是副本,所以此处返回的均为同一个地址。

修改:

for i:=0;i<len(stus);i++  {
       m[stus[i].Name] = &stus[i]
   }
func main() {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

输出结果 A:均为输出 10,B:从 0~9 输出 (顺序不定)

defer多层嵌套

func main() {
    fmt.Println("A")
    defer func() {
       fmt.Println("B")
       defer fmt.Println("C")
       fmt.Println("D")
    }()
    
    defer fmt.Println("E")
    fmt.Println("F")
}

结果输出顺序:AFEDBC

func main() {
    for i:=0; i<5; i++ {
        defer func() {
           fmt.Println(i) 
        }()
    }
}

输出:5 5 5 5 5

func main() {
    for i:=0; i<5; i++ {
        defer func(i int) {
           fmt.Println(i) 
        }(i)
    }
}

输出:4 3 2 1 0

func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}

func main() {
    a := 1
    b := 2
    defer calc("1", a, calc("10", a, b))
    a = 0
    defer calc("2", a, calc("20", a, b))
    b = 1
}

输出

10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4