Golang模块

2023-06-03T14:11:02+08:00 | 7分钟阅读 | 更新于 2023-06-03T14:11:02+08:00

@

Golang基础

Slice

1. 组成

slice主要由三部分组成

image-20250420125214667

2. 声明

1️⃣ 字面量:var ints [ ]int

变量ints就要存在一段连续的内存中,实际上就是个数组,data就是这个底层数组的起始地址, 但是目前只分配了这个切片结构,还没有分配底层数组

image-20250420125902601

2️⃣ make: var ints [ ]int = make ( [ ]int,2,5 )

使用make后会不仅会分配这三个结构,还会开辟的一段内存,作为它的底层数组,这里make会为ints开辟一段可容纳5个整型元素的内存,还会把它们初始化为整形的默认值0

image-20250420130624171

追加一个元素,append (ints, 1)

已经有了两个元素,在后面添加一个,长度变为3

image-20250420131027882

修改一个元素 ints[1] = 2

修改后元素变为1,但是不能修改超过数组长度的,比如int[4],就会发生越界访问,会发生panic

image-20250420131137387

3️⃣ new : ps:=new ( []string )

只分配三个结构,data=nil,len=0,cap=0,不负责分配底层数组,new的返回值就是slice结构的起始地址, (*ps)[0]=“aa”,这样的操作是不被允许的

image-20250420132104928

添加元素 : *ps = append ( *ps , “abc”)

append会分配底层数组

image-20250420135754077

3. slice与array

image-20250420140141660

append: 分别追加

s2扩容超过原数组长度,需要新建数组,并扩容

image-20250420140344023

4. 扩容规则

image-20250420141110982

64位下,每个元素占16个字节

image-20250420141448654

Golang原理

GMP

image-20250424145527180

组件介绍:

  • G(Goroutine):代表 Go 协程,是轻量级的用户态线程。与传统线程相比,Goroutine 的创建和销毁开销极小,并且上下文切换也非常快速。一个 Go 程序中可以创建成千上万个 Goroutine。
  • M(Machine):代表操作系统线程,每个 M 都对应一个内核线程。M 负责执行 Goroutine,它是真正在操作系统上运行的实体。
  • P(Processor):代表处理器,它是连接 G 和 M 的桥梁。P 维护着一个本地的 Goroutine 队列,同时也可以从全局 Goroutine 队列中获取 Goroutine。每个 M 在运行时都需要绑定一个 P,才能执行 Goroutine。
  • GOMAXPROCS(n) : 在程序中经常设置的进程数其实就是设置的GOMAXPROCS(n),它指定了调度器的个数

工作原理:

  1. 创建 Goroutine:当在 Go 程序中使用go关键字创建一个新的 Goroutine 时,这个 Goroutine 会被放入当前 P 的本地队列中。如果本地队列已满,Goroutine 会被放入全局队列中。
  2. M 执行 Goroutine:每个 M 会绑定一个 P,然后从 P 的本地队列中获取 Goroutine 来执行。如果本地队列为空,M 会从全局队列中获取一批 Goroutine 到本地队列中。如果全局队列也为空,M 会尝试从其他 P 的本地队列中 “偷取” 一半的 Goroutine 到自己的本地队列中,这种机制称为 “工作窃取” work-stealing。
  3. 上下文切换:当一个 Goroutine 遇到阻塞操作(如 IO 操作)时,M 会将当前 Goroutine 挂起,然后从 P 的本地队列中获取另一个 Goroutine 继续执行。当阻塞操作完成后,挂起的 Goroutine 会被重新放入 P 的本地队列中等待执行。

GC三色标记

Golang启动时发生了什么?

image-20250420120600691

image-20250420120924510

  1. 开始:程序启动的起始点。
  2. 保存命令行参数到栈中:将程序启动时传入的命令行参数存储到栈空间 ,方便后续使用,为后续初始化等操作提供参数依据。
  3. 初始化 g0 栈:g0 栈是 Go 语言运行时用于调度器自身执行的栈 。初始化 g0 栈为调度器的运行准备好栈空间,后续调度相关操作都基于此栈进行。
  4. 运行时检查(runtime.check):对程序运行时的一些基础环境和类型信息进行检查,比如检查不同类型在当前系统架构下的大小是否符合预期等,确保运行环境正常。
  5. 初始化参数(runtime.args):对程序启动相关的参数进行初始化设置,处理命令行参数、环境变量等相关信息,为后续程序运行提供基础参数配置。
  6. 初始化操作系统设置(runtime.osinit):根据不同的操作系统进行相应初始化 ,如设置 CPU 个数、内存页大小等底层系统相关的配置,适配运行的操作系统环境。
  7. 初始化调度器(runtime.schedinit):初始化 Go 语言的调度器 ,设置调度器相关参数和数据结构,为 Goroutine 的调度做好准备,如创建和初始化与调度相关的队列、设置 P(处理器)的相关信息等。
  8. 创建协程(runtime.newproc):创建一个新的 Goroutine ,用于执行程序的主要逻辑,一般会将用户编写的main.main函数作为该 Goroutine 的执行体。
  9. m 开始执行协程任务(runtime.mstart):m 代表系统线程 ,此步骤让系统线程开始执行调度器分配的 Goroutine 任务,真正启动协程的执行。
  10. 程序入口(runtime.main):Go 程序运行时的入口函数 ,在这个函数中会进行一系列关键初始化,如启动垃圾回收器、执行包的init函数等。
  11. runtime.init:执行 Go 语言运行时自身相关的初始化操作 ,涉及运行时内部一些全局变量、数据结构的初始化等。
  12. gcenable:开启垃圾回收器 ,让垃圾回收机制开始工作,自动回收不再使用的内存,保障程序内存使用的有效性和稳定性。
  13. 用户包 init:依次执行用户代码中各个包的init函数 ,用于完成用户自定义的初始化逻辑,比如初始化数据库连接、注册自定义函数等。
  14. 执行用户入口函数 main.main:执行用户编写的主函数 ,这是程序业务逻辑的起始点,程序真正开始执行开发者定义的功能代码。

内存逃逸

1. 定义

在 Go 语言中,内存分配有栈分配和堆分配两种方式。栈分配由编译器自动管理,函数调用时为局部变量分配内存,函数返回时自动释放;堆分配通过newmake函数动态分配内存,由垃圾回收器(GC)回收 。内存逃逸是指原本应在栈上分配的变量,因某些原因被分配到了堆上,即使函数返回后,这部分内存也不会自动释放,需等待 GC 回收。

2. 发生原因

  • 变量生命周期超出作用域:当函数内部变量被函数外部引用,如赋值给包级变量、作为函数返回值时,其生命周期延长,编译器为保证内存安全,将变量分配到堆上。例如:

    func escape() *int {
        num := 10
        return &num // num 逃逸到堆上,因为返回后其地址仍被外部引用
    }
    
  • 栈空间不足:若变量较大,栈空间无法容纳,会逃逸到堆上。比如大数组、大结构体等。

  • 编译时无法确定类型或大小

    • 切片扩展:对切片进行append操作时,若不确定其最终大小,可能引发内存逃逸。因为可能需重新分配更大内存空间,且新空间可能在堆上。例如:

      func sliceEscape() []int {
          var sl []int
          for i := 0; i < 1000; i++ {
              sl = append(sl, i) // 随着元素增加,可能因空间不足导致内存逃逸
          }
          return sl
      }
      
  • 接口动态分配:具体类型变量赋值给接口类型时,因接口动态特性,具体值可能逃逸。例如:

    type Dog struct{}
    func bark() interface{} {
        d := Dog{}
        return d // Dog 类型的 d 逃逸到堆上
    }
    
  • 闭包引用:函数返回闭包且闭包引用了函数局部变量时,这些变量逃逸到堆上,以保证闭包后续调用时变量仍有效。例如

    func closureEscape() func() {
        num := 5
        return func() {
            println(num) // num 逃逸到堆上,因为闭包引用了它
        }
    }
    

3. 影响

  • 增加垃圾回收压力:堆内存需 GC 回收,逃逸变量多会使堆内存分配频繁,GC 扫描回收负担加重,尤其高频调用函数中,可能延长 GC 停顿时间,影响程序响应速度和吞吐量。
  • 降低性能:堆分配比栈分配更耗时,涉及更多 CPU 和内存资源,频繁内存逃逸会使程序运行速度变慢。
  • 减少缓存局部性:栈变量连续分配销毁,利于利用 CPU 缓存;堆上数据分散,导致缓存效率降低,增加内存访问延迟。
  • 增加内存泄漏风险:若对象不再使用但仍被引用(如闭包、全局变量引用),因内存逃逸未及时回收,可能引发内存泄漏。

4. 检测方法

在编译时使用go build -gcflags "-m"命令,编译器会输出内存逃逸信息,显示哪些变量发生逃逸及原因。例如:

package main
func main() {
    escape()
}
func escape() *int {
    num := 10
    return &num
}

执行go build -gcflags "-m" main.go,会看到类似main.escape: can inlinemain.escape: &num escapes to heap的输出,表明num发生了内存逃逸。

5. 避免和优化措施

  • 合理使用局部变量:将变量作用域限制在函数内部,避免外部引用,减少逃逸可能。
  • 选择合适传递方式:大结构体、数组等用指针传递,避免数据复制;小结构体、基本类型用值传递,减少指针开销。
  • 减少不必要堆分配:复用对象或使用对象池,降低堆内存分配频率。
  • 优化数据结构:避免过度复杂数据结构和指针引用,减少编译时不确定性。

算法

MySQL

Redis

Kafka

About Me

没什么想介绍的,一个很大众的全栈码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写文档 最厌烦的是别人的程序没有留下文档

目标

学习 K8S 源码!加油!