news 2026/4/16 15:55:28

【Go】从defer关键字到锁

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Go】从defer关键字到锁

学完了基础的golang语法,就开始看工作中的项目了。看到一个比较经典常见的代码块,来理解defer感觉正好用。让AI去除业务逻辑写了一个demo,在此记录一下

代码片

packagemainimport("fmt""sync""time")varstudentLocks=make(map[string]*sync.Mutex)varlockForMap sync.MutexfuncgetStudentLock(studentIdstring)*sync.Mutex{lockForMap.Lock()fmt.Printf("[%s] Locking map \n",studentId)deferfunc(){lockForMap.Unlock()fmt.Printf("[%s] Unlocking map \n",studentId)}()if_,ok:=studentLocks[studentId];!ok{studentLocks[studentId]=&sync.Mutex{}}returnstudentLocks[studentId]}funcGetStudentData(studentIdstring,cachemap[string]string)(string,error){//cache checkdata,found:=cache[studentId]iffound{fmt.Printf("[%s] Cache HIT \n",studentId)returndata,nil}fmt.Printf("[%s] Cache MISS. Preparing to lock... \n",studentId)//lockmutex:=getStudentLock(studentId)fmt.Printf("[%s] Acquiring lock... \n",studentId)mutex.Lock()deferfunc(){fmt.Printf("[%s] Unlocking lock... \n",studentId)mutex.Unlock()}()fmt.Printf("[%s] Lock acquired \n",studentId)//2nd cache check//another goroutine might have populated it while we were waiting for the lockdata,found=cache[studentId]iffound{fmt.Printf("[%s] Double-Check Cache HIT \n",studentId)returndata,nil}//fetch from storagefmt.Printf("[%s] Double-Check Cache MISS. Preparing to lock... \n",studentId)time.Sleep(1*time.Second)dataFromStorage:=fmt.Sprintf("Data for %s from storage \n",studentId)//set to cachecache[studentId]=dataFromStorage fmt.Printf("[%s] Data stored in cache \n",studentId)returndata,nil}funcmain(){sharedCache:=make(map[string]string)varwg sync.WaitGroup wg.Add(2)gofunc(){deferwg.Done()GetStudentData("student-123",sharedCache)}()gofunc(){deferwg.Done()GetStudentData("student-123",sharedCache)}()wg.Wait()}

执行结果

./main[student-123]Cache MISS. Preparing to lock...[student-123]Locking map[student-123]Unlocking map[student-123]Acquiring lock...[student-123]Lock acquired[student-123]DCL Cache MISS. Preparing to lock...[student-123]Cache MISS. Preparing to lock...[student-123]Locking map[student-123]Unlocking map[student-123]Acquiring lock...[student-123]Data storedincache[student-123]Unlocking lock...[student-123]Lock acquired[student-123]DCL Cache HIT[student-123]Unlocking lock...

defer 关键字

在以上代码片中有多个defer 关键字,会发现它常常与锁的lock绑定。在lock之后的unlock通常放到defer语句中。
unlock 逻辑放在defer 语句中,来确保无论func 如何退出,锁都会释放。类似的还有资源的关闭也会放在defer 中

defer 是在什么时候执行的呢?注意在上面的代码块中,有两个锁,一个是锁定lockMap的,另一个是锁定一条缓存记录的,这两个锁的上锁和释放都在defer 中,写法是类似的。分别代表着方法成功执行、方法失败或者报错时,锁都被释放。这样写原因是防止忘记所释放而引起的内存泄漏或死锁。

defer 的作用域是方法,而不是代码块,这点很重要,有的时候它存在于{}包围的代码块中,以往的Java经历让我误会defer 是退出代码块的,并不是,它像return一样是属于方法的。

还有一种情况,此时defer 放在一个if块里,如果Cache HIT,没有走到if 块里去lock,那么unlock同样也不会执行,这就相当于主函数压根不会挂载一个defer 回调,此时就不涉及defer 的执行了。

func(){ifdataFromCache==nil{lock.Lock()//other logic... defer lock.Unlock()}//...returndata}

番外:锁与Double Check Locking

虽然这块代码是为了熟悉defer的作用,但是也是一个比较好的并发编程场景:先从缓存中查数据,缓存命中则直接返回;缓存不存在则去数据库里查,然后加载到缓存。

  1. 为什么加锁
    在这个场景中,对数据以studentId为粒度加锁(getStudentLock),为了防止并发对studentId数据的查询。例如如果student-123在缓存中失效时,同时有100个请求过来,此时100个请求都收到cache miss,并去DB中读100次,浪费资源,给数据库压力。
    加了这个锁,保证只有一个请求可以获取到锁并到数据库中加载数据,其它99个请求等待。直到写回缓存,锁释放。此时剩下的99个请求依次获取锁,可以再次检查缓存,命中即可返回,不需要再查数据库

  2. 为什么有两把锁
    以上代码是有两把锁的,有一把mapLock是为了保护map的并发读写,要区分它studentId 锁的区别,后者才是承担了上面的功能。在实际的业务代码中,缓存一般是Redis, 那么mapLock的逻辑实际上会封装在Redis client的实现中,不需要手动写。

  3. 为什么有缓存的double-check
    其实这个代码最开始是没有2nd 检查的逻辑的,当执行main 方法时,会发现两个student-123都从storage中获取了,而我们想达到的目的是,只有一个从storage中获取,加载到cache之后就从cache读了。这就是因为当时没有第二次缓存检查,代码如下。

mutex :=getStudentLock(studentId)fmt.Printf("[%s] Acquiring lock...\n", studentId)mutex.Lock()deferfunc(){fmt.Printf("[%s] Unlocking lock...\n", studentId)mutex.Unlock()}()fmt.Printf("[%s] Lock acquired\n", studentId)//fetch from storage fmt.Printf("[%s] DCL Cache MISS. Preparing to lock...\n", studentId)time.Sleep(1* time.Second)dataFromStorage :=fmt.Sprintf("Data for %s from storage\n", studentId)

那么在高并发场景下,协程A获取了锁,另一个协程B此时获取锁失败,等待。协程A从storage中读到数据,写到缓存,释放锁。此时B获取锁,但注意,它会继续执行从storage中读数据,写缓存,释放锁。这样的结果是100个请求从并行改为线性了,数据库的压力缓解了,但是99个不必要的请求资源还是浪费了。

加了double-check,就保证B在获取锁的时候先检查缓存,缓存命中,那么就不需要再次去storage加载了。这就是Double-check的作用。

其实业务代码里我也没看到double-check,大概率是bugfeature。因为DCL这种模式,在高并发场景中是很有效的,但是在实际的业务中,并不会有同一个studentId 的并发访问,所以没有DCL就没有了,多调用几次storage问题也不大,查的也很快的,DCL不是必须的。能跑就行。

学习DCL是主要的,以后在高并发场景中可以考虑应用。一个pattern,一定有它的业务场景的。如果是刚写代码的我,发现了问题,学了新东西,就想challenge别人,但有的时候code review也是人情世故。我真的是成熟的程序员了,哈哈

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

AI辅助高效研究工具:3个技巧快速上手open-notebook智能笔记本

AI辅助高效研究工具:3个技巧快速上手open-notebook智能笔记本 【免费下载链接】open-notebook An Open Source implementation of Notebook LM with more flexibility and features 项目地址: https://gitcode.com/GitHub_Trending/op/open-notebook 你是否曾…

作者头像 李华
网站建设 2026/4/16 10:43:30

解决Gyroflow视频边缘变形问题:提升300%稳定效果的镜头校准方案

解决Gyroflow视频边缘变形问题:提升300%稳定效果的镜头校准方案 【免费下载链接】gyroflow Video stabilization using gyroscope data 项目地址: https://gitcode.com/GitHub_Trending/gy/gyroflow 诊断画面变形原因 症状分析 当你使用Gyroflow处理视频时…

作者头像 李华
网站建设 2026/4/16 9:20:52

MGeo功能全测评:中文地址匹配准确率有多高?

MGeo功能全测评:中文地址匹配准确率有多高? 1. 引言:地址匹配不是“看字面”,而是“懂地理” 你有没有遇到过这样的情况? 用户在App里填了“上海徐汇漕河泾开发区”,后台数据库存的是“上海市徐汇区漕河泾…

作者头像 李华
网站建设 2026/4/16 10:55:55

实测YOLOv9性能表现,推理训练全链路体验报告

实测YOLOv9性能表现,推理训练全链路体验报告 在工业质检产线的实时图像流中,一张PCB板图像从进入系统到完成缺陷定位仅需38毫秒;在智慧农业无人机巡检场景下,模型需在低功耗Jetson设备上稳定识别数十类作物病害——这些严苛需求背…

作者头像 李华
网站建设 2026/4/15 14:41:37

跨平台桌面应用开发指南:从架构设计到部署实践

跨平台桌面应用开发指南:从架构设计到部署实践 【免费下载链接】AppFlowy AppFlowy 是 Notion 的一个开源替代品。您完全掌控您的数据和定制化需求。该产品基于Flutter和Rust构建而成。 项目地址: https://gitcode.com/GitHub_Trending/ap/AppFlowy &#x1…

作者头像 李华
网站建设 2026/4/16 9:25:13

IndexTTS 2.0在虚拟主播中的应用:定制化语音快速落地

IndexTTS 2.0在虚拟主播中的应用:定制化语音快速落地 虚拟主播正在从“能说话”迈向“会表达”的关键拐点。当观众不再满足于机械朗读,而是期待一个有辨识度、有情绪张力、能与直播画面严丝合缝同步的声音IP时,传统语音合成方案就暴露了本质…

作者头像 李华