Go for-range 循环变量陷阱:goroutine 里全打印同一个值(Go 1.22 前后差异)
📅 2026/7/27 11:52:03
👁️ 次浏览
Go for-range 循环变量陷阱:goroutine 里全打印同一个值(Go 1.22 前后差异)你大概率写过这样的代码:开一批 goroutine 并发处理切片里的元素,结果打印出来的值全是最后一个,或者干脆乱序重复。这不是并发调度的锅,而是for-range循环变量的作用域问题。这个坑在 Go 1.22 之前坑了无数人,Go 1.22 改了语义之后又出现了「同一份代码在两个版本跑出不同结果」的新问题。这篇把来龙去脉和正确写法一次讲清。先复现这个经典 bugpackagemainimport(fmtsync)funcmain(){nums:[]int{1,2,3}varwg sync.WaitGroupfor_,n:rangenums{wg.Add(1)gofunc(){deferwg.Done()fmt.Println(n)// 期望 1 2 3(乱序),实际可能全是 3}()}wg.Wait()}在Go 1.21 及更早版本里,这段代码大概率打印三个3(顺序还不定)。很多人第一反应是「goroutine 调度有问题」,其实跟调度没关系。问题根源:循环变量是「复用」的在 Go 1.22 之前,for _, n : range nums里的n在整个循环中只有一个实例。每次迭代不是新建一个n,而是把新值赋给同一个n。关键在于:goroutine 里的闭包捕获的是n这个变量的地址,不是它某一刻的值。等 goroutine 真正被调度执行时,循环往往早就跑完了,n的最终值停在最后一个元素3,于是三个 goroutine 读到的都是3。用一句话记:闭包捕获的是变量,不是值。循环共用一个变量,闭包自然共用同一份数据。不止 goroutine,把地址存进切片也一样中招:varptrs[]*intfor_,n:rangenums{ptrsappend(ptrs,n)// 三个指针指向同一个 n}for_,p:rangeptrs{fmt.Println(*p)// Go 1.21: 全是 3}Go 1.21 及之前的正确写法写法一:循环内部重新声明(最常用)for_,n:rangenums{n:n// 关键:在循环体内新建一个同名局部变量,遮蔽外层的 nwg.Add(1)gofunc(){deferwg.Done()fmt.Println(n)// 每个 goroutine 捕获的是各自的副本}()}n : n看着别扭,但它的作用是:每次迭代都创建一个新的局部n,把当前值拷进去。闭包捕获的是这个新变量,每个 goroutine 各拿各的,互不干扰。写法二:用参数传值for_,n:rangenums{wg.Add(1)gofunc(nint){// n 作为参数,调用时就完成了值拷贝deferwg.Done()fmt.Println(n)}(n)// 立刻把当前值传进去}调用go func(n int){...}(n)时,实参n的当前值被拷贝给形参,这个拷贝发生在循环迭代的当下,而不是 goroutine 执行的当下。所以每个 goroutine 拿到的是正确的快照值。这两种写法本质一样:在迭代的当下把值固定下来,不让闭包去追那个会变的循环变量。Go 1.22 改了什么从Go 1.22开始,循环变量的作用域改成了每次迭代都是一个新变量。也就是说,上面那段最初的「有 bug」代码,在 Go 1.22 里直接就是对的:// go.mod 里 go 1.22,这段现在打印 1 2 3(乱序)for_,n:rangenums{wg.Add(1)gofunc(){deferwg.Done()fmt.Println(n)// Go 1.22 正确,每次迭代 n 都是新的}()}这是 Go 团队少见的破坏性语义变更,靠go.mod里声明的版本号来控制:go.mod写go 1.22或更高 → 新语义,每轮迭代新变量。go.mod写go 1.21或更低 → 老语义,循环共用变量。编译器按模块声明的版本决定用哪套语义,所以同一份源码,改一下go.mod的版本号,行为就变了。新的坑:版本混淆语义变更解决了老 bug,却带来两个新的现实问题。第一,老代码里的n : n现在是冗余的,但它无害,别急着删。因为你的库可能同时被 1.21 和 1.22 的项目引用,留着它两个版本都安全:for_,n:rangenums{n:n// 1.22 下冗余但无害;1.21 下必需。跨版本兼容就留着gowork(n)}第二,依赖「循环结束后变量停在最后一个值」的代码会被悄悄改坏。这种写法本来就少见、也不推荐,但确实存在:varlastintfor_,n:rangenums{lastn_n}// 用 last,没问题,last 是普通变量,不受影响真正会变的是这种「循环外读循环变量」——但 Go 本来就不允许在循环外访问n(作用域限制),所以实际影响面比想象小。真正要留意的是跨版本行为差异:CI 用 1.22、老服务器编译用 1.20,同一份代码可能跑出不同结果。排查清单遇到「goroutine/闭包读到的循环变量值不对」,按这个顺序查:看go.mod的版本:go 1.21及以下 → 老语义,必须手动拷贝;go 1.22 → 新语义,可以不拷。确认是不是闭包捕获:goroutine、append(n)、defer func(){...n...}()都是捕获变量地址的典型场景。加n : n一律安全:不确定版本时,加上它两个版本都对,代价只是一行。用go vet:go vet的loopclosure检查能在编译期揪出「在 1.21 语义下会出错」的循环闭包,CI 里挂上它。# 让 vet 帮你抓循环闭包问题go vet ./...小结Go 1.22之前:for-range的循环变量整个循环共用一个,闭包捕获的是变量本身,goroutine 延迟执行时读到的是最终值 → 全打印最后一个。修法:循环体内n : n,或用go func(n int){}(n)传值——本质都是在迭代当下把值固定住。Go 1.22之后:每轮迭代都是新变量,老 bug 自动消失,但要警惕同一代码在不同 Go 版本行为不同。跨版本兼容就保留n : n(1.22 下冗余但无害),CI 挂go vet兜底。一句话记忆:闭包捕获的是变量不是值;循环变量是不是「每轮一个」,取决于你的go.mod写的是不是 1.22。
1. 项目概述:为什么要在终端里“画”进度条? 如果你在Linux下写过脚本,或者用过 wget 、 curl 、 rsync 这类命令行工具,肯定对进度条不陌生。它不像GUI程序那样有华丽的滑块,只是在终端里一行不断变化的字符&am…
📅 2026/7/27 11:52:03
1. 项目概述:一个基于PMBus的100W数字电源是如何炼成的在电源设计领域,从传统的模拟控制转向数字管理,就像是从手动挡汽车换成了自动驾驶。它带来的不仅是控制精度的提升,更是整个系统智能化、可观测性和可维护性的飞跃。今天&…
📅 2026/7/27 11:52:03
1. 问题概述:当心这颗“定时炸弹”如果你正在使用或者计划使用德州仪器(TI)的TLV320AIC14、AIC12、AIC13、AIC15、AIC20、AIC21、AIC24、AIC25这一系列音频编解码器,那么这篇文章你必须仔细读完。这不是一篇普通的器件介绍&#x…
📅 2026/7/27 11:52:03
说实话,现在回头看2013年的星象,心里还是有点五味杂陈。那时候大家还在用诺基亚或者刚换iPhone 5,朋友圈也没现在这么卷。那时候的GEO2013年星座运,其实跟现在的感觉完全不一样。很多人现在翻旧账,问以前到底准不准,我觉得吧,运势这东西,信则有不信则无,但2013年确实是…
📅 2026/7/27 12:57:53
一、项目简介
机动车保养管理系统是一套面向车辆所有者与运营管理方的在线保养服务平台,采用 Spring Boot 3 Vue 3 前后端分离架构。系统覆盖从预约下单、保养执行、费用结算到支付完成的完整业务链路,支持车主端与管理员端双角色登录。车主可以管理个人…
📅 2026/7/27 12:58:19
Visual C运行库AI工具:3分钟解决所有Windows程序启动问题的终极方案 【免费下载链接】vcredist AIO Repack for latest Microsoft Visual C Redistributable Runtimes 项目地址: https://gitcode.com/gh_mirrors/vc/vcredist
你是否曾遇到过"找不到msvc…
📅 2026/7/27 12:58:19
M9A终极指南:如何用智能助手解放《重返未来:1999》的日常时间 【免费下载链接】M9A 重返未来:1999 小助手 | Assistant For Reverse: 1999 项目地址: https://gitcode.com/gh_mirrors/m9/M9A
M9A是一款专为《重返未来:1999…
📅 2026/7/27 12:58:19
一、项目简介
华勤考试管理系统是一套基于 Spring Boot 3 Vue 3 前后端分离架构的在线考试平台。系统覆盖从考试发布到成绩分析的全流程,支持用户注册登录、在线答题、自动判分、错题回顾等功能。系统包含普通用户和管理员两个角色,普通用户可参与考试、…
📅 2026/7/27 12:58:19
3分钟快速上手:免费开源的英雄联盟智能助手完整使用指南 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit
League Akari(…
📅 2026/7/27 12:58:19
现象在 WezTerm 终端中,包含中文路径的文本(如标签页标题、Shell 提示符、路径补全)中,某些汉字时而渲染为日文字形,时而显示为简体中文(中国大陆)字形。以「径」字为例,日文写法右侧…
📅 2026/7/27 0:00:07
这个问题看似在寻找一个答案,实际上是在寻找一种“值得继续投入的方向感”。很多人在问:
“人生有什么意义?”
深层可能是在问:
我现在做的事情值得吗?我的努力有没有价值?我的存在是不是重要?未…
📅 2026/7/27 0:00:07
1. 为什么MoE架构让大模型参数量翻倍却不增加推理成本?去年我在部署一个千亿参数大语言模型时,首次接触到混合专家模型(Mixture of Experts,简称MoE)架构。当时最让我震惊的是,这种架构的模型参数量可以达到…
📅 2026/7/27 0:00:07
更多请点击:
https://codechina.net
第一章:AI帮助理解数学概念 人工智能正以前所未有的方式重塑数学学习的路径。通过自然语言处理与符号计算的深度融合,AI不仅能解析抽象定义,还能将定理、证明和几何直觉转化为可交互、可验证的…
📅 2026/7/27 1:11:21
1. 项目背景与核心价值去年参与的一个短剧项目让我深刻体会到传统创作流程的痛点:编剧团队花了三周打磨剧本,角色设计反复修改了七版,最后成片时又因为演员档期问题不得不临时调整分镜。这种低效的创作模式在快节奏的内容行业越来越难以为继。…
📅 2026/7/27 1:11:21
remix-i18next TypeScript类型安全实践:确保翻译键与类型定义同步 【免费下载链接】remix-i18next The easiest way to translate your React Router framework mode apps 项目地址: https://gitcode.com/gh_mirrors/re/remix-i18next
在开发多语言应用时&am…
📅 2026/7/27 1:11:21
目录
第一步:选对模板,省心一半
第二步:打开扫码点餐功能
开启功能按钮
桌台管理与桌码生成
第三步:个性化设计,打造品牌感
调整点餐页面
设置点餐规则 你还在让顾客站着排队点餐吗?2025年ÿ…
📅 2026/7/27 7:11:38
在业务中快速构建一个能理解私有文档、准确回答专业问题的智能助手,是很多开发团队面临的共同挑战。传统方案往往需要从零开始搭建复杂的 RAG(检索增强生成)系统,涉及文档解析、向量化、检索、大模型调用等多个环节,整…
📅 2026/7/26 17:10:53
FAE放射组学分析工具:医学影像特征探索的完整解决方案 【免费下载链接】FAE FeAture Explorer 项目地址: https://gitcode.com/gh_mirrors/fae/FAE
你是否曾经面对海量医学影像数据感到无从下手?想要从CT、MRI等影像中提取有价值的定量特征&#…
📅 2026/7/27 5:11:32