news 2026/9/29 11:36:11

GO学习笔记

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GO学习笔记

个人学习笔记,资源来自网上各位大佬

一、协程

1、coroutine

  • M:1,一个协程阻塞,从属的协程也会阻塞

2、goroutine

  • 有调度器,实现协程和线程的动态绑定和灵活调度
  • 栈空间动态伸缩(默认2KB)
  • M:N,实现真正的并行

二、GMP

1.G0调度器代码:

  1. 为线程m寻找空闲的p,绑定

  2. 在本地队列(lrp)取出来goroutine,执行代码

2.线程M阻塞发生了什么

  1. 当前g将当前p放回空闲p队列,取出当前g和他一同阻塞
  2. 线程m终于读取到文件后
  3. 找一个p进行绑定(分配内存的第一路径是通过p的本地内存缓存申请,没有p,m无法申请执行g的内存)
  4. 阻塞完了,先当前g寻找原先的p进行绑定,再去g0在空闲p队列寻找p
  5. 失败
  6. g0将当前g释放到全局队列,当前线程m休眠,等待被唤醒去绑定空闲p

3.GMP调度流程

  • G=Goroutine(Go 协程,用户态线程)
  • M=Machine(操作系统线程)
  • P=Processor(逻辑处理器,代表 CPU 核心)
    它解决了传统线程模型中“线程切换开销大、资源浪费”问题
  1. 启动:Go 启动时,创建GOMAXPROCS个 P(默认 CPU 核心数)。
  2. 创建 G:go func()→ G 被加入当前 P 的本地队列。
  3. 调度:M 绑定 P → 从本地队列取 G 执行。(M中的G0进行的逻辑调度)
  4. 阻塞处理:
    • M 解绑P(P 会被新 M 绑定)。
    • G 被挂起,等待阻塞操作完成。(只有系统调用m才阻塞,否则挂起)
  5. 恢复:阻塞完成 → G 重新加入 P 的LRQ。
  6. 工作窃取:P 的队列为空时,M 会从其他 P 的队列“偷”任务(避免空闲)。
    顺序:本地队列->->全局P队列其他P->(从tail开始,偷一半)

4.P设计的好处

P的存在实现了无锁的本地调度。每个P维护独立的本地队列,M绑定P后可以直接从本地队列取G执行,大部分情况下都不需要全局锁。只有本地队列空了才去偷取,这大大减少了锁竞争。

5.介绍一下GMP模型

GMP模型是goroutine调度模型,让成千上万的goroutine高效地运行在少量os线程上

  • G(Goroutine):用户态协程,初始栈2kb,可动态伸缩
  • M(Machine):OS 线程(内核线程)
  • P(Processor):逻辑处理器,连接 G 和 M 的核心
  1. go func(),创建一个g
  2. g 进入当前 p 的本地队列LRQ,满了就进入全局队列 GRQ
  3. m 绑定 p 在本地队列取 g 运行,如果无 g 就 work stealing 机制
  4. 从其他 p 本地队列尾部开始偷取一半,没有得偷就去全局队列 GRQ 偷
  5. 如果 g 阻塞,m 解绑 p(避免卡住cpu),然后挂起,等待g阻塞完毕
  6. p会被新 m 绑定执行 g

通过本地队列,work stealing,抢占调度,syscall解绑来让海量goroutine高效运行

一、G 阻塞的两种核心场景(及 M-P 解绑逻辑)

场景 1:G 进入系统调用(如文件 IO、sleep、网络阻塞早期)
  1. G 执行到os.Open()/time.Sleep()等系统调用 → 触发 M 进入内核态阻塞;
  2. 关键动作:M 立刻释放绑定的 P,P 会被 runtime 重新放入 “空闲 P 列表”;
  3. 空闲 M(或新建 M)会从空闲列表中抢占这个 P → P 继续调度本地队列的其他 G 执行;
  4. 当原 G 的系统调用结束 → 这个 G 会被放入可运行队列(本地 / 全局),而原 M 则:
    • 若能抢到空闲 P → 继续执行这个 G;
    • 若抢不到 → M 会被休眠(减少系统线程开销),等待后续唤醒。
场景 2:G 因网络 IO 阻塞(如net.Dial()/http.Get())

这种场景更高效:Go 的网络库会把 IO 事件交给NetPoller(网络轮询器)处理,而非让 M 阻塞:

  1. G 触发网络 IO → runtime 先把 G 挂到 NetPoller 的等待队列,直接让 M 释放 G;
  2. M 无需阻塞,继续用 P 调度下一个 G(M-P 不解绑);
  3. 当 IO 事件完成(如连接建立 / 数据到达)→ NetPoller 把 G 重新放回可运行队列,等待 M 调度。

二、为什么要这么设计?(核心价值)

你可以把 P 理解为 “CPU 核心的使用权”(GOMAXPROCS 本质是限制 P 的数量 = CPU 核心数):

  • 如果 M 阻塞时不释放 P → 这个 P 就被 “锁死” 在阻塞的 M 上,对应 CPU 核心空转,其他 G 只能排队;
  • 如果 M 阻塞时释放 P → P 能立刻被其他 M 复用,CPU 核心始终有 G 在执行,利用率拉满。

举个直观例子:假设你的机器是 4 核(GOMAXPROCS=4),有 4 个 P。如果一个 G 因 sleep (10s) 阻塞,若 M 不释放 P → 这 10s 内只有 3 个 P 在干活,1 个 CPU 核心闲置;而释放 P 后 → 4 个 P 全程都在调度 G,CPU 利用率保持 100%。

三、

1. go程序启动发生了什么

深入原理

  1. 命令行参数复制:读取命令行参数,复制到 argc 和 argv。
  2. 初始化 g0 栈:g0 是运行时系统的一个特殊的 goroutine,它在程序启动时被创建,用于执行系统调用和协程调度。
  3. runtime.check 运行时检查:
    1. 类型长度,指针操作,结构体字段偏移量.......
  4. runtime.args 参数初始化:将 argc 和 argv 的参数赋值到 Go 的变量中。
  5. runtime.osinit 初始化操作系统特点的设置:主要是判断系统字长和 CPU 核数。
  6. runtime.schedinit 初始化调度器:
    1. 锁初始化,内存分配器初始化,GC初始化,调度器初始......
  7. runtime.newproc 创建主协程 g0 并将其放入队列中等待执行。
  8. runtime. mstart 启动调度器:初始化 m0,并调度 g0 去执行

2. Slice切片原理

type slice struct { array unsafe.Pointer // 指向底层数组的指针 len int // 切片的有效长度(当前可访问的元素个数) cap int // 切片的容量(底层数组从指针位置开始的总元素个数) }
  1. 赋值、传参、截取,都是操作array本身
  2. 自动扩容(1.8以前)
    1. cap < 1024,翻倍
    2. cap >= 1024,新容量=原容量 X 1.25
    3. 深拷贝,向上取整 到 内存对齐的整数倍
  3. 自动扩容(1.8及以后)
    1. 需要容量 > 2倍: 直接使用需要的容量
    2. cap < 256,翻倍(2x)
    3. cap>=256,newcap = oldcap + (oldcap + 3 * 256 ) / 4,直到容量够
    4. 深拷贝,向上取整 到 内存对齐的整数倍

3. Map映射原理

type hmap struct { count int // map中实际存储的键值对数量 (len(map)返回的值) flags uint8 // 状态标识(是否正在扩容、是否并发写等) B uint8 // 哈希桶的数量 = 2^B 个,核心扩容相关字段 noverflow uint16 // 溢出桶的数量,越多代表哈希冲突越严重 hash0 uint32 // 哈希种子,随机值,保证哈希结果的均匀性 buckets unsafe.Pointer // 指向「哈希桶数组」的指针,核心存储区域 oldbuckets unsafe.Pointer // 扩容时用,指向「旧哈希桶数组」的指针 nevacuate uintptr // 扩容时的搬迁进度标记 extra *mapextra // 存储溢出桶的内存地址 } type bmap struct { tophash [8]uint8 // 存储每个键的哈希值高8位,用于快速查找匹配 // 后续是8个key、8个value,内存地址连续存储,编译期自动填充 // 最后是一个溢出桶指针,指向当前桶存满后,挂载的下一个bmap }
  • Go 的map是引用类型(和切片一样,不是值类型),赋值 / 传参时只拷贝底层结构体指针,修改会互相影响;
  • Gomap的底层实现是「拉链法」哈希表,核心解决哈希冲突的问题;
  • Gomap无序,遍历结果的元素顺序不固定,每次遍历都可能不一样;
  • Gomap非并发安全,多个 goroutine 同时读写会 panic;
  • map的键key必须满足可比较性(支持==/!=),切片、map、函数不能作为 key。
  1. 插入和查找(hash后共64位)
    1. key:低B位
    2. value:高8位
  2. 自动扩容
    1. 装载因子 > 6.5,桶数量翻倍
    2. 溢出桶太多,等量扩容,桶的数量不变,只是将键值对重新排列,减少溢出桶的使用。
      1. bucket_cnt < 2^15,如果溢出桶数量超过桶数量,就会触发扩容;
      2. bucket_cnt >= 2^15时,如果溢出桶数量超过2^15,就会触发扩容
    3. 渐进式扩容,每次写 操作迁移 1-2 个桶

4. Channel原理

type hchan struct { qcount uint // 当前队列元素个数 dataqsiz uint // 缓冲区长度(由 make 的 N 决定) buf unsafe.Pointer // 指向环形缓冲区,元素大小 = sizeof(T) elemsize uint16 closed uint32 // 关闭标志,CAS 置 1 elemtype *_type // 元素类型信息,用于 GC 扫描 sendx uint // 发送索引 recvx uint // 接收索引 //无缓冲channel只用到下面字段 recvq waitq // 等待接收的 sudog 双链表 sendq waitq // 等待发送的 sudog 双链表 lock mutex // 一把全局锁,保护 hchan 内部字段 } type waitq struct { first, last *sudog }

发送chansend

  1. lock
  2. 判断是否关闭,是则panic
  3. 判断recvq有无,有则直接发(完整拷贝一个过去),goready唤醒,unlock
  4. 判断是否有缓冲区qcount<dataqsiz,有则发送到缓冲区,unlock
  5. 无则初始化sudog放入sendq,gopark

接受chanrecv

  1. lock
  2. 判断是否关闭,是则return
  3. 判断sendq有无,有则直接接受(完整拷贝一个过来),goready唤醒,unlock
  4. 判断缓冲区是否已经有数据qcount>0,是则接受到缓冲区,unlock
  5. 无则初始化sudog放入recvq,gopark

为什么选用chanel来进行协会通信?

  1. 首先是重点,go推崇“不要通过共享内存来通信,而应通过通信来共享内存”。
  2. 天然避免了数据竞争,传统锁机制需要手动管理,容易死锁遗漏,channal通过数据所有权的转移,保证同一时刻只有一个goroutine来操作数据

  3. 简化流程,数据传递和同步合二为一,发送/接受数据本身包含阻塞等待,不需要额外的信号量或者condition,简单易读

PS

  1. ⽆缓冲的 channel 是同步的,⽽有缓冲的 channel 是⾮同步的
  2. 从⼀个 nil channel 接收数据,造成永远阻塞
  3. 给⼀个 nil channel 发送数据,造成永远阻塞
  4. 从⼀个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回⼀个零值
  5. 给⼀个已经关闭的 channel 发送数据,引起 panic
  6. 关闭⼀个 nil channel 将会发⽣ panic

5、defer

defer 是 Go 语言中用于延迟执行函数调用的关键字。它的主要作用是:

  1. 确保资源释放:如文件关闭、锁释放

  2. 异常恢复:配合 recover 处理 panic

  3. 执行顺序:后进先出(LIFO)

  4. 参数求值:参数在 defer 声明时立即求值

  5. 返回值修改:可以修改命名返回值

实现原理上,defer 在编译时会被转换成_defer结构体,形成一个链表。函数返回时按链表顺序执行 defer。Go 1.13+ 对 defer 进行了性能优化,支持栈分配和开放编码。"

//① 先计算返回值 → ② 再按 LIFO 执行 defer → ③ 最终才真正返回给调用方。 func f() (r int) { // 命名返回值 defer func() { r++ }() // ② 在RET之前执行,成功改值 return 1 // ① 先完成 r = 1 } // ③ 真正返回 2

6、for range

// 源代码 slice := []int{1, 2, 3} for i, v := range slice { fmt.Println(i, v) } // 编译器大致转换为 for i := 0; i < len(slice); i++ { v := slice[i] // 关键:值拷贝! fmt.Println(i, v) } //所以如果需要用到指针,要用i索引 slice := []int{1, 2, 3} for i, v := range slice { func(&slice[i]) }

7、内存逃逸

内存逃逸:原本应该在栈上分配的内存被分配到了堆上

// 1、返回局部变量指针 → 生命周期>栈帧,必逃 func foo() *int { x := 42; return &x } // &x escapes to heap // 2、把指针扔进 channel → 编译期不知接收方,只能堆上 func bar() { ch := make(chan *int) v := 1 ch <- &v // &v escapes to heap } // 3、切片里存指针 → 切片本体可栈,但指向的元素必逃 func baz() []*string { s := "hi"; return []*string{&s} } // &s escapes to heap // 4、append 导致重新分配 → 运行时才能决定新数组地址,逃 func qux() []int { s := make([]int, 0, 2) // 初始栈上 s = append(s, 1, 2, 3) // 超出 cap,新数组 escapes to heap return s } // 5、interface 方法调用 → 动态派发,接收器与实参都逃 type animal interface { run() } type dog struct {} func (a dog) run() {} func main() { //编译时就知道类型,栈 var a animal = dog{} a.run() //不知道类型,堆 var a1 animal a1=dog{} a1.run() } // 6. 大块对象 > 64 KB func bigHeap() { b := make([]byte, 1<<20) // 1 MB b[0] = 1 fmt.Printf("big slice len = %d\n", len(b)) }

简而言之

  1. 局部变量的生命周期超出了所在函数的生命周期
  2. 变量类型不确定
  3. 变量大小不确定或者太大

为什么要避免内存逃逸?

大量的逃逸会影响应用程序性能

8、CSP模型

核心思想:不要通过共享内存来通信,而应通过通信来共享内存。

  1. 避免竞态条件
  2. 简化同步,不需要显式上锁解锁,通道自动同步
特性CSP(通道通信)共享变量通信
通信方式通过通道发送和接受消息通过共享内存读写数据
数据共享数据通过消息传递,不直接共享内存数据存储在共享内存
同步机制通道的发送和接受操作自动同步需要显示使用锁,信号量等同步机制
编程复杂度更简单,避免竞态条件更复杂,手动管理
典型语言GoC、C++、Java
性能通道通信存在一定开销

直接访问内存,性能较高

使用场景高并发、任务间通信频繁的场景需要精细控制内存访问的场景

9、GO GC垃圾回收

三色标记法

1、开始全部对象都在白色集合

2、然后把根节点移到灰色集合

3、循环遍历灰色集合,把其引用对象不断加入灰色集合,然后自己去黑色集合

4、直到灰色集合为空

完整的 GC 周期分为 4 个阶段,其中仅 2 个阶段有极短的 STW:

  1. STW - 初始标记(Initial Mark):暂停所有 goroutine,标记根对象(栈、全局变量),耗时极短(百微秒级);
  2. 并发标记(Concurrent Mark):恢复 goroutine,GC 后台线程并发遍历堆中对象,标记存活对象;
    • 此阶段若程序修改对象引用(如指针赋值),写屏障会记录修改,确保标记不遗漏;
    • 若程序内存分配速率过快,会触发 “辅助 GC”:让分配内存的 goroutine 协助做标记工作,避免 GC 滞后。
  3. STW - 最终标记(Final Mark):再次短暂暂停 goroutine,处理剩余未标记的少量对象(如写屏障记录的修改);
  4. 并发清理(Concurrent Sweep):恢复 goroutine,GC 后台线程并发清理未标记的死亡对象,将内存归还给堆(或操作系统)。

请你介绍一下go的gc垃圾回收?

混合屏障:只要堆上指针修改,同时标记新旧对象
Go的gc采用的是三色回收法,主要分4个阶段

  1. STW-初始标记,暂停goroutine,开启写屏障,标记根节点,加入灰色集合
  2. 并发标记,恢复goroutine,并发循环遍历灰色集合,把引用的对象加入灰色集合,自己进入黑色集合(有写屏障,会处理并发对象)
  3. STW-最终标记,暂停goroutine,处理剩下未标记的对象(写屏障的修改)
  4. 并发清理,恢复goroutine,并发清理未标记的对象

为什么混合屏障?

  1. 跳过栈扫描,采用栈全黑,提高性能
  2. 无需快照,减少内存开销

10、go的优势

1、极致的并发性能

go的并发模型csp:goroutine(初始栈2KB,一台机器就能百万级goroutine)+channel

2、语法简洁

只有25个关键字,快速上手

3、高性能

编译性语言,执行效率高

gc垃圾回收stw停顿时间短,微秒级

4、标准库非常丰富

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 11:31:55

不辞职也能读硕士?同等学力申硕这条路怎么走

有人问我&#xff0c;上班了还想拿个硕士&#xff0c;是不是只能辞职去考全日制。不是。除了全国统考的非全日制研究生&#xff0c;还有一条叫“同等学力人员申请硕士学位”的路&#xff0c;不用脱产、先入学后考试&#xff0c;适合一边上班一边提升的人。今天把它走一遍。 一、…

作者头像 李华
网站建设 2026/9/29 11:29:34

用 scrcpy 在电脑上跑手机游戏:轻量安卓投屏与控制方案

用 scrcpy 在电脑上跑手机游戏&#xff1a;轻量安卓投屏与控制方案 【免费下载链接】scrcpy Display and control your Android device 项目地址: https://gitcode.com/GitHub_Trending/sc/scrcpy scrcpy 是一款开源的安卓投屏工具&#xff0c;能把手机画面镜像到电脑&a…

作者头像 李华
网站建设 2026/9/29 11:22:45

【ARM 裸机开发 (IMX6ULL-mini)】SPI 协议与ADXL345 三轴加速度传感器

文章目录前言一、SPI基础概念二、SPI的时序三、IMX6ULL上的SPI3.1 概念及原理框图3.2 相关寄存器RXDATATXDATACONREGCONFIGREGSTATREG四、ADXL345加速度传感器五、SPI初始化六、SPI读写函数七、ADXL345与IMX6ULL通信八、UART、I2C与SPI对比前言 在【ARM 裸机开发 (IMX6ULL-min…

作者头像 李华
网站建设 2026/9/29 11:20:40

编排还是事件协作?用 AcmeFlow 讲清企业 Workflow 的控制权

编排还是事件协作&#xff1f;用 AcmeFlow 讲清企业 Workflow 的控制权 企业工作流最容易被低估的设计问题&#xff0c;并不是“下一步调用哪个接口”&#xff0c;而是“谁有权决定下一步”。同一份客户服务开通申请经过运营审批、合同签署、到账、ERP 建档和 CRM 通知。每个团…

作者头像 李华
网站建设 2026/9/29 11:20:12

红蚂蚁YOLO数据集:VOC/COCO/YOLO三格式一键生成与训练实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华