go 面试

目录

1. GMP 模型

G:go 的协程

M:系统线程的抽象

P:执行go code的资源和保留G的上下文信息,可以理解成承上启下的调度器

1.1 GMP模型为什么要有P

在 Go1.1 之前的Go的调度模型里面其实是只有GM模型,没有P。

tex 复制代码
权威描述

What's wrong with current implementation:
1. Single global mutex (Sched.Lock) and centralized state. The mutex protects all goroutine-related operations (creation, completion, rescheduling, etc).
2. Goroutine (G) hand-off (G.nextg). Worker threads (M's) frequently hand-off runnable goroutines between each other, this may lead to increased latencies and additional overheads. Every M must be able to execute any runnable G, in particular the M that just created the G.
3. Per-M memory cache (M.mcache). Memory cache and other caches (stack alloc) are associated with all M's, while they need to be associated only with M's running Go code (an M blocked inside of syscall does not need mcache). A ratio between M's running Go code and all M's can be as high as 1:100. This leads to excessive resource consumption (each MCache can suck up up to 2M) and poor data locality.
4. Aggressive thread blocking/unblocking. In presence of syscalls worker threads are frequently blocked and unblocked. This adds a lot of overhead.

个人理解:

  1. 从全局队列获取的G的时候有一个互斥锁lock mutex, 每次取的时候都要先持有锁,才能从全局队列里获取可执行的G。这里有大量的锁竞争。所以后来添加了P来进行解耦,每个P都有一个长度是256的定长数组作为本地队列,定义如下(runq [256]guintptr
  2. 如果一个普通协程创建一个协程,这个协程应该符合局部性原理(通过runnext guintptr来实现,比如g1 通过go关键字创建了g2,g2 不应该放在本地队列的尾部,而是优先执行,他就放在了runnext里),谁创建的由谁来运行,而不是放到全局队列让M再去取一遍
  3. 系统线程使用效率不能最大化(实现work-stealing)

如果P上没有可运行的G怎么办?

在GMP的模型中实现了work stealing的算法,如果P队列为空,则会先从全局队列中或者其他P的本地队列中窃取后半部分(后半部分竞争冲突小),以防止P空转导致的资源浪费

P在获取G的时候先从P的本地队列进行无锁获取,减少锁竞争,其次是从全局队列进行获取,最后从其他的P里窃取,让处理器尽量的CPU资源使用均衡。

在这个地方你有没有一个疑问?

获取本地队列中的g是无锁操作,别人过来窃取的时候需要加锁。只让别人加锁会不会有获取冲突呢?我执行g的时候会不会正好别人过来偷?

其实不会出现这种情况,是因为自己获取的时候是一个原子操作

这里需要注意,每个M被创建的时候都有一个G0同时被创建,G0是一个特殊的协程

  • 提供可靠的系统栈(为什么说他可靠呢?)
  • 这里的可靠性说的是g0的栈是固定大小,而普通的栈空间是可以弹性扩容的
  • 当需要执行调度器操作、栈管理、内存分配系统调用、垃圾回收等时,M 会切换到 g0上运行

Design

The general idea is to introduce a notion of P (Processors) into runtime and implement work-stealing scheduler on top of Processors.

M represents OS thread (as it is now). P represents a resource that is required to execute Go code. When M executes Go code, it has an associated P. When M is idle or in syscall, it does not need P.

There is exactly GOMAXPROCS P's. All P's are organized into an array, that is a requirement of work-stealing. GOMAXPROCS change involves stop/start the world to resize array of P's.

Some variables from sched are de-centralized and moved to P. Some variables from M are moved to P (the ones that relate to active execution of Go code).

复制代码
个人理解:
在go 中所有的P都放在了一个数组里,P的个数默认等于GOMAXPROCS,当M执行G的时候需要从数组中取出,当M不在需要M时返回数组,而不是销毁

Scheduling

When a new G is created or an existing G becomes runnable, it is pushed onto a list of runnable goroutines of current P. When P finishes executing G, it first tries to pop a G from own list of runnable goroutines; if the list is empty, P chooses a random victim (another P) and tries to steal a half of runnable goroutines from it.

M 执行系统调用的时候会与P解绑,P也可以给其他的M绑定,或者被其他P stealing

为了减少G 内存的频繁分配和回收,在P内由一个pFree 队列来回收已经dead的g以便重复利用

内核线程M并不保存G的状态,他的状态在P中保存,这是G可以跨M调度的基础

调度策略大致如下:

  1. 每执行61次都会从全局队列获取一个g去执行,如果有就直接返回来避免全局队列的g被饿死
  2. 优先从runnext上找到一个可执行的g去执行
  3. 如果runnext没有就从本地队列里获取一个g执行
  4. 如果本地队列为空则去全局队列、网络轮询器或者其他的P的本地队列进行窃取

hand off 机制(实现已经弃用,但是其他的实现仍然基于hand off理论):

早期(go1.0)

  • 如果M在执行G的过程中发生系统调用阻塞,会阻塞G和M,此时P会和当前的M解绑,并寻找新的M继续执行G,如果没有M就申请重新创建一个。M返回以后重新获取P。
  • 现在最新的代码已经不叫hand off了,但是handoff 的精神还在,被窃取机制代替,只保留其精神

M 什么时候会与P解绑

  • 阻塞的系统调用(主要场景):
  • CGO调用,gc、STW
    解绑处理:
  1. P标记为可用(_Psyscall)
  2. 其他M可窃取P继续工作
  3. 系统调用返回时,尝试快速重新绑定
  4. 失败时进入慢速路径

新G的产生

  1. 执行go func的时候主线程M会调用newproc()申请一个G的结构体
  2. 每个G 都会被尝试先放在p的 runnext 中,如果runnext为空则放入,生成结束
  3. 如果runnext 不为空且本地队列不满,则将原来的g放入本地队列中,将新的g放入 runnext 中, 生产结束
  4. 若本地队列也满了先将本地队列中的一半放入全局队列中,然后执行(3)生产结束

P中g的来源

  • 本地队列
  • 全局队列
  • netpoller
  • 其他p的本地队列(从队列头进行窃取,需要加锁)

基于信号的抢占式调度

抢占分为两种情况

  • _Prunning: 通常指的是死循环的逻辑
  • _Psyscall:系统调用,网络io等
    在1.14之前都是主动让出CPU,但是如果进入到了死循环,他就不能主动让出CPU了,为了解决这个问题1.14引入了基于信号的异步抢占。
  1. M创建的时候注册一个SIGURG信号的处理函数 sighandler
  2. sysmon启动后会间隔性的进行监控,最长间隔10ms,如果发现某个g执行时间连续超过10ms,会给M发送抢占信号
  3. M收到信号后,内核执行 sighandler 函数把正在运行的g放到全局队列,继续执行其他g来继续执行

M0和G0

G0:每个操作系统线程(M)都有一个专用的 G0 ,用于执行运行时系统的管理任务,而不是用户代码。

主要的作用:

  • 调度器工作(主要工作)
  • 系统调用处理(当用户 goroutine 执行系统调用时,会切换到 G0)
  • 栈增长处理(当 goroutine 需要更多栈空间时,切换到 G0)
  • 垃圾回收相关操作(GC 相关的停止世界(Stop-the-World)操作在 G0 上执行)
    M0:M0是 Go 程序启动时创建的第一个操作系统线程,负责初始化整个运行时系统。

slice

slice和数组的区别

  1. 数组初始化必须指定长度且长度是固定的,切片的长度不固定,可以追加元素。
  2. 函数传值不同
  • 数组是值传递,将一个数组赋值给另外一个数组时是深拷贝,作为参数传递的时候会复制整个数组,占用额外内存。被调函数修改数组的内容原数组值不改变
  • 切片是引用类型,可以使用make创建,将一个切片赋值给另外一个切片变量时时浅拷贝。作为函数的参数传递的时候不会拷贝整个切片,只拷贝len和cap。底层还是指向同一个数组,不会占用过多的内存。被调函数修改切片的内容,主调函数的切片跟着变动
  • 计算的长度的方式不一样,切片可以直接获取len,数组需要遍历
  • 切片可以使用make 进行创建

slice 怎么进行深拷贝

  1. copy函数
  2. 遍历然后重新赋值给新的slice

slice的扩容机制

扩容时机

当append的时候容量不足时,进行扩容

  • 如果所需容量超过原容量的2倍,直接使用所需容量
  • 如果原来长度小于256(1.25版本),每次扩容2倍
  • 如果原来长度大于256,每次扩容1.25倍

go 实现线程安全

  • 互斥锁
  • 读写锁
  • 原子操作
  • sync包
  • channel

map

底层实现(还在过渡期)

go中map是一个指针,占用8个字节,在1.24之前使用的hmap,1.24版本开始使用maps.Map.

1.24以后对于map有重构,代码放在src/maps/map.go中

修复点包括:

  • bucket 搬迁不再多线程操作
  • 读操作永远不会参与 rehash
  • grow pointer 行为重新设计
  • map 扩容后的旧表保持只读

改为双表查找策略(新 old-buckets + new-buckets)

Go1.24 修复了 map 在扩容时因并发读写而导致的内部结构损坏问题。

旧版本 map 使用协作扩容,多个 goroutine 在迁移 buckets 时可能造成不一致,从而触发 fatal error 或崩溃。

Go1.24 改为 non-cooperative resize,由写 goroutine 负责扩容,读 goroutine 不再参与搬迁,从而彻底避免 map 内部结构被破坏。

需要注意 Go1.24 并没有让 map 变成线程安全,只是让并发访问不会再破坏 map 内部结构。

map 为什么设计成无序的?(无序是因为哈希种子可能每次都不同)

  1. 如果语言允许 map 有固定访问顺序,很多程序员会不自觉地依赖这个顺序写逻辑,一旦后面设计变化,在不同的版本可能出现问题(Go 团队明确说过:不保证顺序就是为了让开发者"无法依赖顺序",避免脆弱代码。 )
  2. 选择哈希表天然就不能保证顺序
  3. 出于安全性------防止 Hash Flooding 攻击
  4. map 的渐进式扩容会损坏顺序

为什么1.24版本的map重构了还是不保证顺序,它解决了哪些问题?

SwissTable 的核心目标是:性能、密度、cache 效率

map buket 总结:

✔ Bucket 只能存 8 个 entry,所以有了溢出桶来扩容

✔ 溢出桶是链状结构:bucket → overflow1 → overflow2...

✔ 溢出桶太多会导致查找性能下降

✔ 扩容 grow 会减少 overflow bucket 数量

✔ Go1.24 SwissTable 不再使用 overflow bucket,性能更强

6.5 负载因子到底是干什么的?

✔ 这是 Go map(旧版)的负载因子阈值,用于触发扩容

✔ 原因是 bucket + overflow 结构在超过 81% 装载时性能急剧下降

✔ 6.5 是通过测试得出的最优性能点,不是随便选的

✔ 它避免产生过多 overflow bucket → 保持 O(1) 查找性能

  • 当map中所有的元素个数 > 6.5*buketNum
  • 溢出桶太多

使用map的时候要注意什么(1.23- 可以提高map的性能)?

  1. 先预估k/v对的数量,尽可能一次性申请足够的容量
  2. 如果不能预估,可以选择定期重建map
  3. key 要有辨识度,更有利于分布均匀。否则容易出现哈希冲突加剧

原生map和sync.map的性能比较

sync.Map更适合读多写少的场景,sync.Map的实现是通过空间换时间的方案进行实现的,冗余了两个数据结构。read和dirty

和原生的map + RWLock相比,sync.Map对于读的场景可以实现无锁访问read map。所以在读多写少的场景会有一些优势。但是在写多的场景下,read map缓存会失效,需要加锁,性能会急剧下降

channel

channel 有两种类型

  • 有缓冲
  • 无缓冲

channel 有3种模式

  • 只读
  • 只写
  • 读写

channel 有3种状态

未初始化 关闭 正常
关闭 panic panic 正常(只能关闭一次)
发送 死锁 panic 正常或阻塞
接收 死锁 缓冲区不为空,读取待读取的数据,为空,读取空值 阻塞或者接收成功

注意:

如果一个chan 被多个G监听,当生产者写入消息后,其他协程是随机读取数据的,只能被消费一次。

但是如果channel被关闭,所有的协程都能收到close信号

chan的设计背景

  • 通过通信来实现内存共享(CSP)是go的设计哲学
  • 同时实现了同步通信和异步通信
  • 简化并发,让程序的编写人员轻松安全的实现高并发的操作

chan 源码

go 复制代码
// src/runtime/chan.go 中的核心结构
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 为什么不设计成使用读写锁?

因为chan的读不是简单的向map那种的get,读消息的同时伴随着修改qcount、recvx等字段的值,所以不适合使用读写锁

chan 使用的是互斥锁

在空闲模式下直接加锁成功

如果锁被占用且不处于饥饿模式就进入自旋状态,以减少上下文切换的开销

如果自旋4次都没有获取到锁,直接进入饥饿状态。

进入饥饿状态以后开始计时

chan 锁升级

go 的内存分配机制

分配方式TCMalloc

Go 的内存分配机制深受 TCMalloc(Thread-Caching Malloc)的影响,其核心思想是多级缓存和对象大小分类,以减少锁竞争、提高并发性能。整个分配过程可以概括为:每个线程(在 Go 中是每个 P)维护一个本地缓存,小对象分配直接使用本地缓存,不足时向中央缓存或堆申请。

核心设计思想

  • 减少全局锁竞争:通过为每个执行线程(实际上是逻辑处理器 P)提供本地内存缓存,大部分小对象分配无需加锁,只在本地完成,这是高性能的关键。

  • 按对象大小分类管理:将内存分配请求按大小分为不同的类别,采用不同的策略,提高空间利用率和分配速度。

  • 内存复用:分配的内存释放后,不会立即归还给操作系统,而是被 Go 运行时缓存起来,用于后续的分配请求,减少系统调用。

几个核心概念

  • mspan:是包含多规格等大内存块的链表,内存管理的基本单位
  • mcache:
  • mcentral
  • mheap

内存的分配的过程

  1. 判断是大对象还是小对象
  1. 小对象直接计算size class(8byte-->32kb)
  2. 从 mcache无锁分配
  • Goroutine 会从它当前绑定的 P 中获取对应的 mcache
  • mcache为每个 Size Class 都预先维护着一个或多个完整的 mspan(内存段,一般是8kb)
  • mspan是一个包含等大小内存块的链表。分配器只需从链表中弹出第一个空闲块,移动链表头,然后返回该块的地址。
    关键:此过程完全无锁,因为每个 P 是独立的,这是高性能的基石

微对象(小于16B)

小对象(16b-32kb叫小对象)

大对象(超过32Kb叫大对象)

GC三色<黑白灰>表记法

  • 黑色:对象已被扫描,它引用的其他对象也已被扫描。是"安全"的,GC 不会再次扫描它。
  • 灰色:对象本身已被扫描到(从白色变为灰色),但它引用的其他对象还未被扫描。是"待处理"状态。
  • 白色:对象尚未被 GC 访问到。是"潜在垃圾"的初始状态。在标记阶段结束时,所有白色对象都会被回收。

GC的流程

GC 是一种类型准确的,允许多个 GC 线程并行运行,非分代且非压缩的,使用写屏障的并发标记清扫算法。

  1. GC 执行清扫终止
    a. 停止世界(STW)。这会让所有 P 到达 GC 安全点。
    b. 只有在强制触发 GC 的时候(非自动触发GC),会执行清扫任何未清扫的 span。
  2. GC 执行标记阶段
    a. 准备标记阶段:通过将 gcphase 设置为 _GCmark(从 _GCoff),启用写屏障启用修改器辅助,并将根标记作业加入队列。在所有 P 都启用写屏障之前,不能扫描任何对象,这是通过 STW 实现的。
    b. Start the world 。从此时起,GC 工作由调度程序启动的标记工作线程和作为分配一部分执行的辅助完成。写屏障对任何指针写入的被覆盖指针和新指针值进行着色(mbarrier.go)。新分配的对象立即标记为黑色。
    c. GC 执行根标记作业。这包括扫描所有堆栈,着色所有全局变量,以及着色堆外运行时数据结构中的任何堆指针。扫描堆栈会停止一个 goroutine,对其堆栈上找到的任何指针进行着色,然后恢复该 goroutine。
    d. GC 清空灰色对象的工作队列,将每个灰色对象扫描为黑色,并对对象中找到的所有指针进行着色(这又可能将这些指针添加到工作队列中)。
    e. 由于 GC 工作分布在本地缓存中,GC 使用分布式终止算法来检测何时没有更多的根标记作业或灰色对象(参见 gcMarkDone)。此时,GC 转换到标记终止。
  3. GC 执行标记终止
    a. 停止世界
    b. 将 gcphase 设置为 _GCmarktermination,并禁用工作线程和辅助。
    c. 执行内务处理,如刷新 mcaches。
  4. GC 执行清扫阶段
    a. 准备清扫阶段:通过将 gcphase 设置为 _GCoff,设置清扫状态并禁用写屏障
    b. 启动世界 。从此时起,新分配的对象为白色,并且在必要时分配会在使用前清扫 span。
    c. GC 在后台并响应分配进行并发清扫。参见下面的描述。
  5. 当发生足够分配时,重放上述序列

写屏障是怎么回事?

写屏障是"正确性机制",只做两件事:

  • 新对象直接标黑
  • 任何赋值写入的对象都必须标灰(保证新申请的资源和新申请的资源使用白色资源不会被回收

辅助 GC 是怎么回事?

并发标记允许程序继续运行并分配内存,但如果分配速度快得超过后台标记能跟上的速率,堆会膨胀超出目标(heap goal)。辅助 GC 使得分配者按分配量分担标记工作,把"标记的工作量"与"内存分配量"绑在一起,避免内存暴涨,让整个 GC 周期边长或者无法按计划完成
-----------------------------------------------------------------------------------------------------------------------------

程序行为

├─ 写指针(b.child = a)

│ └─ 写屏障(标灰)

└─ 分配内存(malloc)

└─ 辅助GC(做标记工作)

关于 GC 率的讨论。

  • 并发清扫

    清扫阶段与正常程序执行并发进行。堆是按 span 逐个清扫的,既延迟进行(当 goroutine 需要另一个 span 时,它首先尝试通过清扫回收该内存。当 goroutine 需要分配新的小对象 span 时,它会清扫相同对象大小的 span,直到至少释放一个对象),也在后台 goroutine 中并发进行(这有助于非 CPU 密集型的程序)。

    在 STW 标记终止结束时,所有 span 都被标记为"需要清扫"。

  • GC 率

    下一次 GC 发生在我们分配了与已使用内存量成比例的额外内存之后。该比例由 GOGC 环境变量控制(默认为 100)。如果 GOGC=100 并且我们正在使用 4M,我们将在达到 8M 时再次进行 GC(此标记由 gcController.heapGoal 方法计算)。这使 GC 成本与分配成本保持线性比例。调整 GOGC 只会改变线性常数。

    Oblets
    为了防止扫描大对象时出现长暂停并提高并行性,垃圾回收器将大于 maxObletBytes 的对象的扫描作业分解为最多 maxObletBytes 的"oblet"。当扫描遇到大对象的开头时,它只扫描第一个 oblet,并将剩余的 oblet 作为新的扫描作业加入队列。

为什么需要两次 STW

tex 复制代码
Go GC 在每个周期中确实需要两次短暂的 STW(Stop-The-World),这不是设计缺陷,而是算法正确性和性能平衡的必然选择。

 第一次 STW 的原因:
 	写屏障启用竞态原子性启用屏障确保屏障启用前无内存写入栈根扫描准确性静止状态扫描栈在运行时会变化根对象完整性捕获完整根集避免漏标根对象
第二次 STW 的原因
	标记完成检测原子性确认并发环境下无法确定完成点写屏障关闭竞态原子性关闭屏障确保屏障关闭期间无内存不一致状态转换安全原子性阶段转换避免GC状态机出现中间状态

gc的观测与优化

观测

  • GODEBUG=gctrace=1

一般认为:STW 过大(> 5ms), GC 频率过高(每秒 > 10 次)

  • pprof 观测
go 复制代码
go tool pprof http://localhost:6060/debug/pprof/heap
// allocs 是主要挂测点,看看谁一直在疯狂分配
go tool pprof http://localhost:6060/debug/pprof/allocs
go tool pprof http://localhost:6060/debug/pprof/profile
  • 堆对象数 > 10M

优化

  • 减少分配并复用
  • 使用sync.pool(高并发下复用response buffer 对于http的服务有显著提高)
  • 使用bytes.Buffer
  • make的时候预估容量,避免扩容
  • 内存充裕的情况下,提供GC的触发次数默认100,可以适当提高,比如export GOGC=200

它可以让gc次数减少50%,但是内存会增加50%

  • map数量超过300w的时候建议拆分map
  • G太多会让gc慢,因为每个g的栈都是gc的起点(root)

sync

sync.Pool

sync.Pool 是给 GC 使用的临时对象缓存,不是内存池,不保证对象一定被复用 会在 GC 时被清空,它会增加gc的压力,但是如果你不用,gc压力更大,你 Put 的对象,在下一次 GC 后就不一定在了。

它的目的是:

  • 减少高频小对象的分配
  • 利用 GC 清空机制避免内存泄漏

所以sync.Pool 不适合

  • 长生命周期对象
  • 文件句柄
  • 大对象

sync.Once

只调用一次,一半用于初始化或者创建资源的时候使用。
这个在数据库里不能直接使用,他的问题在于如果数据库连接断了他就不能重连了。

我觉得使用在加载配置的时候也不合适,如果使用配置加载的花,你配置热更新怎么实现呢?

还有人说使用在函数注册,注册中心如果不能提供这种重新配置那就不应该叫配置中心了,这是一个基本功能啊

所以我几乎在项目了从来没使用过sync.Once,学习这个完全是为了应付面试。

sync.Cond

他是协程间同步的一个利器,特别是1:N场景的信息同步,应该把他理解成一个条件变量

sync.Cond 解决的核心问题

  • 消除忙等待:goroutine挂起,零CPU消耗
  • 精确事件通知:支持单个(Signal)和批量(Broadcast)唤醒
  • 复杂条件同步:支持基于多个变量的条件判断
  • 避免竞态条件:Wait()原子性处理锁的释放和获取
  • 高效资源利用:避免轮询,减少不必要的上下文切换

经常给定义的队列一起使用

go 复制代码
type Queue struct {
    cond  *sync.Cond
    items []Item  // 数据存储在切片中,不是cond中
}

defer

按照结论写例子

  1. defer 在函数返回前执行,LIFO 顺序
go 复制代码
func f2() (x int) {
	fmt.Println("f2 start")
	// 先压入栈
	defer func() {
		fmt.Println("f2 01 defer")
	}()

	// 后压入栈
	defer func() {
		fmt.Println("f2 02 defer")
	}()
	fmt.Println("f2 end")
	return
}

func main() {
	fmt.Println(f2())
}
// -----输出
// f2 start
// f2 end
// f2 02 defer
// f2 01 defer
// 0
  1. return 返回值是不是指定的变量,这个非常重要。下面是两个信息的实例。
go 复制代码
// 实例1
func main() {
	fmt.Println("开始执行main函数")
	result := testFunction()
	fmt.Printf("main函数中获取的返回值: %d\n", result)
}

func testFunction() (result int) {
	// 1. 先执行return表达式(包括计算返回值)
	defer func() {
		fmt.Printf("defer执行前,返回值 = %d\n", result)
		result += 10 // 修改返回值
		fmt.Printf("defer执行后,返回值 = %d\n", result)
	}()
	fmt.Println("开始执行 testFunction")
	// return表达式先执行:计算返回值并写入命名返回值result
	result = 5 + 3
	fmt.Printf("return表达式执行后,返回值 = %d\n", result)
	// 2. 然后执行defer函数
	// 3. 最后函数返回
	return result // 这里实际上返回的是defer修改后的值
}

//开始执行main函数
//开始执行 testFunction
//return表达式执行后,返回值 = 8
//defer执行前,返回值 = 8
//defer执行后,返回值 = 18
//main函数中获取的返回值: 18

// ====================================
func main() {
	fmt.Println("开始执行main函数")
	result := testFunction()
	fmt.Printf("main函数中获取的返回值: %d\n", result)
}

func testFunction() int {
	var result = 0
	// 1. 先执行return表达式(包括计算返回值)
	defer func() {
		fmt.Printf("defer执行前,返回值 = %d\n", result)
		result += 10 // 修改返回值
		fmt.Printf("defer执行后,返回值 = %d\n", result)
	}()

	fmt.Println("开始执行 testFunction")

	// return表达式先执行:计算返回值并写入命名返回值result
	result = 5 + 3
	fmt.Printf("return表达式执行后,返回值 = %d\n", result)

	// 2. 然后执行defer函数
	// 3. 最后函数返回
	return result // 这里实际上返回的是defer修改`前`的值
}

//开始执行main函数
//开始执行 testFunction
//return表达式执行后,返回值 = 8
//defer执行前,返回值 = 8
//defer执行后,返回值 = 18
//main函数中获取的返回值: 8
  1. defer 的参数在声明处求值
go 复制代码
// 待更新
  1. panic 时也会执行 defer
go 复制代码
// 待更新
相关推荐
侠客行031718 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪18 小时前
深入浅出LangChain4J
java·langchain·llm
子兮曰18 小时前
OpenClaw入门:从零开始搭建你的私有化AI助手
前端·架构·github
吴仰晖18 小时前
使用github copliot chat的源码学习之Chromium Compositor
前端
1024小神18 小时前
github发布pages的几种状态记录
前端
老毛肚19 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎20 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
不像程序员的程序媛20 小时前
Nginx日志切分
服务器·前端·nginx
Yvonne爱编码20 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚20 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言