Golang 单例模式与sync.Once

简介: 本文介绍了 Golang 中实现单例模式的两种方式,重点分析了使用 `sync.Once` 的优势。通过对比不安全的双重检查锁定实现,讲解了并发安全初始化的重要性,并结合 `sync.Once` 源码说明其内部机制,最后推荐在实际开发中使用该方式实现单例模式,以保证线程安全和初始化的正确性。

Golang 单例模式与sync.Once

背景

单例模式可以说是最简单的设计模式之一了,功能很简单:一个类型的东西只实例化一次,全局只有一个实例,并提供方法来获取该实例。

在 Golang 中变量或说明实例只初始化一次的效果通过init函数是可以实现的,包在被引入时就会执行一次init函数且无论同一包被引入多少次也都只执行一次。

不过本文主要想讨论的单例模式是第一次需要用到时才去初始化,也就是延迟初始化。

不太好的单例实现

go

体验AI代码助手

代码解读

复制代码

// bad_singleton.go

package main

import (
	"sync"
)

var svcMu sync.Mutex
var svc *Svc

type Svc struct {
	Num int
}

func GetSvc() *Svc {
	if svc == nil { // 这一步判断不是并发安全的
		svcMu.Lock()
		defer svcMu.Unlock()
		if svc == nil {
			svc = &Svc{Num: 1}
			svc = &Svc{}
			svc.Num = 1

		}
	}

	return svc
}

注意执行互斥锁svcMu.Lock()前的语句if svc == nil 并不是并发安全的,即在多个 goroutine 并发调用的场景下,其中的一个 goroutine 正在初始化这个变量svc的过程中,这里别的 goroutine 判断得到svc不等于nil的结果时也并不意味着svc就一定完成初始化了。

因为在缺乏显式同步的情况下,编译器和CPU在能保证每个 goroutine 内满足串行一致性的基础上可以自由地重排访问内存的指令顺序。

比如svc = &Svc{Num: 1}这行看上去只是一条执行语句,可能重排后的一种实现是像下面这样的:

ini

体验AI代码助手

代码解读

复制代码

svc = &Svc{}
svc.Num = 1

可见,不等于nil并不意味着就一定完成了初始化,因此上面示例是一种不太好的单例实现。

比较好的单例实现

go

体验AI代码助手

代码解读

复制代码

// good_singleton.go

package main

import (
	"sync"
)

var svcOnce sync.Once
var svc *Svc

type Svc struct {
	Num int
}

func GetSvc() *Svc {
	svcOnce.Do(func() {
		svc = &Svc{Num: 1}
	})

	return svc
}

sync.Once提供的Do方法无论被调用多少次都只执行传入的函数一次,那为什么说直接使用Do方法执行初始化而不是套一层if svc == nil 才是比较好的做法呢,下面结合sync.Once源码来说明。

go

体验AI代码助手

代码解读

复制代码

// sync.Once 源码

package sync

import (
	"sync/atomic"
)

type Once struct {
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 { // 这步是判断是否已经完成初始化的关键
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

官方对于sync.Once的实现是非常短小精悍的。其中atomic.LoadUint32(&o.done) == 0是关键的一步,这里采用的是原子操作语句,保证了即使在并发场景下也是安全的,对数据的读写都是完整的。

o.done的值为0时表示未进行初始化或正在初始化中,只有等于1时才表示初始化已经完成,即f()执行完成后由defer atomic.StoreUint32(&o.done, 1)语句给o.done赋值1;也就是o.done作为是否完成初始化的标识,可能的值只有前面说的两个,为0时则加锁并尝试初始化流程,反之则视为已完成初始化直接跳过,这样就完美兼顾了效率与并发安全。

由此可见sync.Once内置的初始化完成标识判断远比if svc == nil 靠谱,因此像上面这样使用sync.Once实现单例模式是最推荐的方式。

额外推荐

实则开发中用到的设计模式经常不止一种,越是复杂大型的项目就越需要使用更多合适的模式来优化代码。


转载来源:https://juejin.cn/post/7149214496546881572

相关文章
|
3月前
|
存储 设计模式 安全
Go 语言单例模式全解析:从青铜到王者段位的实现方案
单例模式确保一个类只有一个实例,并提供全局访问点,适用于日志、配置管理、数据库连接池等场景。在 Go 中,常用实现方式包括懒汉模式、饿汉模式、双重检查锁定,最佳实践是使用 `sync.Once`,它并发安全、简洁高效。本文详解各种实现方式的优缺点,并提供代码示例与最佳应用建议。
94 5
|
监控 网络协议 Go
Golang抓包:实现网络数据包捕获与分析
Golang抓包:实现网络数据包捕获与分析
|
11月前
|
存储 缓存 NoSQL
【redis】数据量庞大时的应对策略
【redis】数据量庞大时的应对策略
162 2
|
11月前
|
机器学习/深度学习 监控 Serverless
无服务器架构(Serverless)
无服务器架构(Serverless)
710 4
|
10月前
|
机器学习/深度学习 算法 大数据
蓄水池抽样算法详解及Python实现
蓄水池抽样是一种适用于从未知大小或大数据集中高效随机抽样的算法,确保每个元素被选中的概率相同。本文介绍其基本概念、工作原理,并提供Python代码示例,演示如何实现该算法。
207 1
|
10月前
|
存储 缓存 API
深入理解RESTful API设计原则
【10月更文挑战第28天】 在现代软件开发中,RESTful API已经成为了前后端分离架构下不可或缺的一部分。本文将探讨RESTful API的核心设计原则,包括资源导向、无状态性、统一的接口以及可缓存性等关键概念,并通过实例解析如何在实际应用中遵循这些原则来设计高效、可扩展的API。我们将深入了解REST架构风格的理论基础,并讨论其对提升系统互操作性和简化客户端实现的重要性。
210 3
|
Java Go
go如何读取yaml配置文件?
本文介绍了如何在Go项目中利用YAML文件进行配置管理,以简化变量更改及维护工作。首先,通过`go get gopkg.in/yaml.v3`命令安装YAML处理库。接着,展示了如何创建并解析YAML配置文件,包括定义结构体映射YAML字段、读取文件内容以及错误处理等步骤。此外,还提供了通过Go代码生成YAML文件的方法。使用`gopkg.in/yaml.v3`库能够有效提升项目的可维护性和开发效率。
873 1
go如何读取yaml配置文件?
|
Unix Go
Golang语言标准库time之日期和时间相关函数
这篇文章是关于Go语言日期和时间处理的文章,介绍了如何使用Go标准库中的time包来处理日期和时间。
455 3
|
Ubuntu
systemd挂载cgroup文件系统流程简要分析
systemd挂载cgroup文件系统流程简要分析

热门文章

最新文章