引用类型

slice map channel interface func都是引用类型

除引用类型外的其他类型都是值类型

nil slice 与 空 slice 的处理是不一致的

var slice []int 只是声名了slice,却没有给实例话的对象

这种情况可以用于返回 slice 的函数,当函数异常时,保证函数依然会有 nil 的返回值

empty slice 是指 slice 不为 nil,但是 slice 没有值,slice 的底层空间是空的

Slice := make([]int, 0)

nil sliceempty slice是不同的东西,需要我们加以区分的

Golang 内存模型

小对象过多会造成GC压力

因为小对象过多会导致GC三色标记法消耗过多的GPU。

优化思路,减少对象分配。如使用结构体变量,减少值传递

数据竞争问题

Data Race 问题可以使用互斥锁sync.Mutex 或者也可以通过 CAS 无锁并发解决

golang 中引入了竞争检测机制,可以使用 go run -race 或者 go build -race 来进行静态检测

竞争检测器已经完全集成到 Go 工具链中,仅仅添加 -race 标志就行了

要解决数据竞争的问题,可以使用 sync.Mutex 也可以使用 channel ,使用 channel 的效率比 sync.Mutex 的效率高

Go语言 atomic 原子操作

atomic是最轻量级的锁,cpu原语

原子操作有5种,增或减 比较并交换 载入 存储 交换

atomic.AddUint32()

atomic.CompareAndSwapUint32()

atomic.LoadUint32() 当读取时,计算机中的任何 cpu 都不会进行针对此值的读写操作

atomic.SotreUint32() 当存储时,任何 cpu 都不会进行针对针这个值的读写操作

atomic.SwapUint32(*unit32,unit32) uint32

GC触发的条件

  1. 主动触发, runtime.GC 阻塞操作
  2. 被动触发 定时触发(2min)与内存分配比例触发配合完成

channel是同步还是异步

channel 是异步的,存在3种状态

nil,未初始化的状态,只进行了声明,或者手动赋值为nil

active,正常的 channel ,可读可写

closed,已关闭,千万不要认为关闭 channel 后,channel 的值是 nil

操作nilactiveclosed
关闭panic成功关闭(通知所有)panic
发送数据永久阻塞阻塞或发送成功panic
接收数据永久阻塞阻塞或接收成功永不阻塞 _,ok := <-ch ok is false

Go 的 struct 能不能比较

相同的 struct 类型可以比较

不同的 struct 类型不可以比较

Go reutn 的执行步骤

  1. 给返回值赋值(rval)
  2. 调用 defer 表达式(函数中有 defer 语句时)与 defer 有关
  3. 返回给调用函数(ret)

Go 的 select

Golang 的 select 机制,可以理解为是在语言层面(用户空间实现,没有用户态和内核态的切换)实现了和 select,poll,epoll 相似的功能。监听多个描述符的读/写事件,一旦某个描述符就绪(一般是读或写事件发生),就能够将发生的事件通知给关心的应用程序去处理该事件。golang 的 select 机制是,监听多个 channel,每一个 case 是一个事件,可以是读事件也可以是写事件,随机选择一个执行,可以设置 default,它的作用是当监听的多个事件阻塞住会执行 default 的逻辑。

Go 的 Slice 如何扩容

type slice struct {
    array unsafe.Pointer // 指向一个数组的指针
    len   int // 长度
    cap   int // 容量
}

我们在对 slice 进行 append 操作时,可能会造成 slice 的自动扩容。

  • 如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一
  • 如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组

Go 内存逃逸分析

go run -gcflags "-m -l" main.go 

在Go中逃逸分析是一种确定指针动态范围的方法,可以分析在程序的哪些地方可以访问到指针。它涉及到指针分析和形状分析。

导致内存逃逸的情况比较多,有些可能还是官方未能够实现精确的分析逃逸情况的 bug,通常来讲就是如果变量的作用域不会扩大并且其行为或者大小能够在编译的时候确定,一般情况下都是分配到栈上,否则就可能发生内存逃逸分配到堆上。

内存逃逸的五种情况:

  1. channel,由于无法在编译阶段确定其作用于与传递的路径,所以一般会逃逸到堆上分配
  2. slice 中的值包含指针,如[]*string的类型。即使切片的底层存储数组仍然可能在栈上,数据的引用也会转移到堆中
  3. slice 由于 append 操作超出其容量。在编译时,slice 初始大小已知,将会分配在栈上。如果在运行时扩容,则它将被分配到堆上
  4. 调用接口类型的方法。接口类型的方法调用时动态调用,实际使用的具体实现只能在运行时确定
  5. 尽管能够符合分配到栈的场景,但是其大小不能够在在编译时候确定的情况,也会分配到堆上

总结下来就是,编译时能够确定的属于静态内存,分配在栈上。编译时无法确定的属于动态内存,分配在堆上。运行时大小发生变化的,或运行时才能确定,分配在堆上。

Go 的对象在内存中怎么分配

  1. 栈内存的分配

    栈和堆在虚拟内存上2块不同功能的内存区域

  • 栈在高地址,从高地址向低地址增长
  • 堆在低地址,从低地址向高地址整张

    栈和堆的比较优势:

  • 栈内存比较简单,分配比堆上快

  • 栈的内存不需要回收,而堆需要。需要而外花费cpu,有stw
  • 栈上的内存有更好的局部性原理,堆内存访问的2块数据可能在不同的页上
  1. 堆内存的分配

    内存管理GC问题,主要是讨论堆内存的管理,分为3部分:

  • 分配内存块
  • 回收内存块
  • 组织内存块

Go 的内存分配类似 TCMalloc ,小对象和大对象只有大小区分,无其他区分。按大小分为67类,最大32kb。超过32kb的大对象,直接从 mheap 分配

Go 的内存分配原则:

Go 在程序启动时,会先向操作系统申请一块内存(虚拟地址空间),切成小块后自己管理

aeana 区域就是堆区,Go 动态分配的内存都在这个区域,它把内存分割成8kb大小的页,一些页组合起来成为 mspan。

bitmap 区域标识 arena 区域哪些地方保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。bitmap 中一个byte 大小的内存对应 arena 区域中4个指针大小(8B)的内存,所以 bitmap 的大小是 512GB / (4 * 8B) = 16GB

Go 内存泄漏

  • 预期能被快速释放的内存因被根对象引用而没有得到迅速释放
  • goroutine 泄漏

Go new 和 make 的区别

值类型:int float bool string struct 和 array

变量直接存储值,分配栈区的内存空间,这些变量所占据的空间在函数被调用完成后会自动释放

引用类型:slice map chan interface func 和 指针

变量存储的是一个地址(或者理解为指针),指针指向内存中真正存储数据的首地址。内存通常在堆上分配,通过 GC 回收

需要注意的是,对于引用类型的变量,我们不仅要声明变量,更重要的是,我们得手动为它分配空间

因此,new该方法的参数要求传入一个类型,而不是一个值,它会申请该类型大小的空间,并会初始化为对应的零值,返回该内存空间的一个指针

func new(Type) *Type

而make也是用于内存分配,但是和new 不同,只能用于引用对象的内存创建,它返回的类型就是类型本省,而不是它们的指针

func make(t Type, size ...IntergerType) Type
Scroll to Top