Go从defer关键字到锁用法实例demo

 更新时间:2026年06月04日 08:31:42   作者:代码N年归来仍是新手村成员  
在Go语言中,defer语句用于延迟执行一个函数或方法,直到包含它的函数执行完毕时才执行,这篇文章主要介绍了Go从defer关键字到锁用法的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

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

代码片

package main

import (
	"fmt"
	"sync"
	"time"
)

var studentLocks = make(map[string]*sync.Mutex)
var lockForMap sync.Mutex

func getStudentLock(studentId string) *sync.Mutex {
	lockForMap.Lock()
	fmt.Printf("[%s] Locking map \n", studentId)

	defer func() {
		lockForMap.Unlock()
		fmt.Printf("[%s] Unlocking map \n", studentId)
	}()

	if _, ok := studentLocks[studentId]; !ok {
		studentLocks[studentId] = &sync.Mutex{}
	}

	return studentLocks[studentId]
}

func GetStudentData(studentId string, cache map[string]string) (string, error) {
	//cache check
	data, found := cache[studentId]

	if found {
		fmt.Printf("[%s]  Cache HIT \n", studentId)
		return data, nil
	}

	fmt.Printf("[%s]  Cache MISS. Preparing to lock... \n", studentId)

	//lock
	mutex := getStudentLock(studentId)
	fmt.Printf("[%s]  Acquiring lock... \n", studentId)
	mutex.Lock()

	defer func() {
		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 lock
	data, found = cache[studentId]
	if found {
		fmt.Printf("[%s]  Double-Check Cache HIT \n", studentId)
		return data, nil
	}

	//fetch from storage
	fmt.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 cache
	cache[studentId] = dataFromStorage
	fmt.Printf("[%s] Data stored in cache \n", studentId)
	return data, nil
}

func main() {
	sharedCache := make(map[string]string)

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		GetStudentData("student-123", sharedCache)
	}()

	go func() {
		defer wg.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 stored in cache 
[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 (){
if dataFromCache == nil{

	lock.Lock()
	//other logic...

	defer lock.Unlock()
}

//...

return data
}

番外:锁与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()
	
	defer func() {
		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,大概率是bug feature。因为DCL这种模式,在高并发场景中是很有效的,但是在实际的业务中,并不会有同一个studentId 的并发访问,所以没有DCL就没有了,多调用几次storage问题也不大,查的也很快的,DCL不是必须的。能跑就行。

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

总结 

到此这篇关于Go从defer关键字到锁用法的文章就介绍到这了,更多相关Go defer关键字到锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang语言中切片的长度和容量的概念和使用

    Golang语言中切片的长度和容量的概念和使用

    这篇文章主要介绍了Golang语言中切片的长度和容量的概念和使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-11-11
  • go将request body绑定到不同的结构体中教程

    go将request body绑定到不同的结构体中教程

    这篇文章主要为大家介绍了go将request body绑定到不同的结构体中教程,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • golang对自定义类型进行排序的解决方法

    golang对自定义类型进行排序的解决方法

    学习一门编程语言,要掌握原子数据类型,还需要掌握自定义数据类型。下面这篇文章主要给大家介绍了关于golang如何对自定义类型进行排序的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下。
    2017-12-12
  • Go高级特性探究之recover捕获panic详解

    Go高级特性探究之recover捕获panic详解

    在Go语言中,当程序出现panic(即运行时错误)时,程序会立即停止当前的执行流程,而recover函数的作用就是捕获这个panic,下面就来看看具体是怎么操作的吧
    2023-06-06
  • go 迭代string数组操作 go for string[]

    go 迭代string数组操作 go for string[]

    这篇文章主要介绍了go 迭代string数组操作 go for string[],具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • go select的用法

    go select的用法

    本文主要介绍了go select的用法,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Golang接口使用教程详解

    Golang接口使用教程详解

    在 Go 语言中接口包含两种含义:它既是方法的集合, 同时还是一种类型并且在Go 语言中是隐式实现的。本文通过示例详细介绍了Golang接口的使用,需要的可以参考一下
    2022-09-09
  • go语言make初始化的实现

    go语言make初始化的实现

    Go语言中的make函数用于初始化切片、映射和通道,本文就来介绍一下go语言make初始化的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-12-12
  • Go语言库系列之dotsql详解

    Go语言库系列之dotsql详解

    这篇文章主要介绍了Go语言库系列之dotsql的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-04-04
  • 探索Go语言中的switch高级用法

    探索Go语言中的switch高级用法

    在Go语言中,switch语句除了常见的用法外,还有一种不常用但有趣的写法,这种写法中,switch后面不跟任何表达式,而每个case后面跟的是返回bool类型的函数调用表达式,这实际上是一个等价于switch true的用法,通过从上到下逐一比较case后的表达式是否为true来决定执行哪个分支
    2024-10-10

最新评论