Appearance
Once
1. 概述
Once 是 Go 语言中用于保证某个操作只执行一次的同步原语,它是 sync 包中的一个结构体。Once 提供了一种简单有效的方式来实现单例模式、初始化操作等需要确保只执行一次的场景。
在整个 Go 语言课程体系中,Once 是并发编程的重要组件之一,与 Goroutine、通道、WaitGroup 和上下文一起构成了 Go 语言并发模型的核心。掌握 Once 的使用和原理,对于构建可靠的并发系统至关重要。
2. 基本概念
2.1 语法
2.1.1 基本用法
go
import "sync"
// 创建 Once
var once sync.Once
// 执行只需要执行一次的操作
once.Do(func() {
// 初始化代码或其他需要只执行一次的操作
})2.1.2 示例代码
go
func main() {
var once sync.Once
var initialized bool
// 多次调用 Do,但函数只会执行一次
for i := 0; i < 5; i++ {
once.Do(func() {
initialized = true
fmt.Println("Initialized")
})
fmt.Printf("Iteration %d: initialized = %v\n", i, initialized)
}
}2.2 语义
- Do 方法:接收一个函数参数,该函数只会被执行一次,无论
Do被调用多少次。 - 并发安全:
Do方法是并发安全的,多个 Goroutine 同时调用Do时,只有一个 Goroutine 会执行函数,其他 Goroutine 会等待函数执行完成。 - 零值可用:Once 的零值是可用的,不需要初始化。
- 不可重置:Once 一旦执行了函数,就不能重置,无法再次执行新的函数。
2.3 规范
- 命名规范:Once 变量通常命名为
once。 - 使用场景:用于初始化操作、单例模式、配置加载等需要只执行一次的场景。
- 函数要求:传递给
Do的函数不应该有参数和返回值。 - 错误处理:如果函数执行过程中发生 panic,Once 会认为函数已经执行过,不会再次执行。
- 不可复制:Once 是一个结构体,不是引用类型,不要复制使用中的 Once。
3. 原理深度解析
3.1 Once 结构体
Once 的底层实现是一个结构体,包含以下字段:
go
type Once struct {
noCopy noCopy // 防止复制
done uint32 // 标记是否已经执行过,0 表示未执行,1 表示已执行
m Mutex // 互斥锁,用于保证并发安全
}其中:
done是一个 uint32 类型的标记,用于表示操作是否已经执行过。m是一个 Mutex,用于保证并发安全。noCopy是一个空结构体,用于防止 Once 被复制。
3.2 Do 方法实现
Do 方法的主要功能是保证传入的函数只执行一次:
- 首先通过原子操作检查
done标记,如果已经是 1,则直接返回。 - 如果
done是 0,则获取互斥锁。 - 再次检查
done标记(双重检查锁定模式),如果已经是 1,则释放锁并返回。 - 执行传入的函数。
- 将
done标记设置为 1。 - 释放互斥锁。
3.3 并发安全
Once 的 Do 方法是并发安全的,使用了双重检查锁定模式(Double-Checked Locking)来提高性能:
- 第一次检查
done标记时不获取锁,避免了每次调用都获取锁的开销。 - 第二次检查
done标记时获取了锁,确保只有一个 Goroutine 执行函数。 - 使用原子操作检查和设置
done标记,确保在多 Goroutine 环境中的可见性。
3.4 内存模型
Once 遵循 Go 语言的内存模型,确保以下顺序:
- 函数执行过程中的所有操作,发生在
Do方法返回之前。 - 多个 Goroutine 同时调用
Do时,只有一个 Goroutine 会执行函数,其他 Goroutine 会等待函数执行完成。 - 函数执行完成后,
done标记被设置为 1,所有后续调用Do的 Goroutine 都会直接返回。
4. 常见错误与踩坑点
4.1 传递有参数的函数
错误表现:无法直接传递有参数的函数给 Do 方法。
产生原因:Do 方法只接受无参数、无返回值的函数。
解决方案:使用闭包来捕获参数。
go
// 错误示例
func initWithParam(param string) {
fmt.Printf("Initialized with %s\n", param)
}
func main() {
var once sync.Once
param := "test"
once.Do(initWithParam(param)) // 错误:不能直接传递参数
}
// 正确示例
func main() {
var once sync.Once
param := "test"
once.Do(func() {
fmt.Printf("Initialized with %s\n", param) // 使用闭包捕获参数
})
}4.2 函数执行过程中发生 panic
错误表现:Once 会认为函数已经执行过,不会再次执行。
产生原因:Once 的设计是即使函数执行过程中发生 panic,也会标记为已执行。
解决方案:在函数内部处理 panic,确保初始化操作能够正确完成。
go
// 错误示例
func main() {
var once sync.Once
once.Do(func() {
fmt.Println("Initializing...")
panic("Initialization failed")
})
// 这里不会再次执行初始化
once.Do(func() {
fmt.Println("This won't be executed")
})
}
// 正确示例
func main() {
var once sync.Once
once.Do(func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
// 可以在这里进行清理操作
}
}()
fmt.Println("Initializing...")
panic("Initialization failed")
})
}4.3 复制 Once
错误表现:复制的 Once 与原 Once 状态不同步,导致不可预期的行为。
产生原因:Once 是结构体,不是引用类型,复制后会创建一个新的实例,与原实例状态无关。
解决方案:通过指针传递 Once,而不是复制它。
go
// 错误示例
func worker(once sync.Once) { // 复制 Once
once.Do(func() {
fmt.Println("Initialized in worker")
})
}
func main() {
var once sync.Once
worker(once) // 传递副本
worker(once) // 会再次执行初始化
}
// 正确示例
func worker(once *sync.Once) { // 通过指针传递
once.Do(func() {
fmt.Println("Initialized in worker")
})
}
func main() {
var once sync.Once
worker(&once) // 传递指针
worker(&once) // 不会再次执行初始化
}4.4 尝试重置 Once
错误表现:Once 一旦执行了函数,就不能重置,无法再次执行新的函数。
产生原因:Once 的设计是不可重置的,没有提供重置方法。
解决方案:如果需要重复执行初始化操作,可以使用自定义的重置机制,或者为每次初始化创建一个新的 Once 实例。
go
// 自定义可重置的 Once
type ResettableOnce struct {
m sync.Mutex
done uint32
}
func (o *ResettableOnce) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
func (o *ResettableOnce) Reset() {
o.m.Lock()
defer o.m.Unlock()
atomic.StoreUint32(&o.done, 0)
}
func main() {
var once ResettableOnce
once.Do(func() {
fmt.Println("First initialization")
})
once.Reset()
once.Do(func() {
fmt.Println("Second initialization")
})
}4.5 在 Do 中调用 Do
错误表现:在 Do 方法中调用同一个 Once 的 Do 方法会导致死锁。
产生原因:Once 内部使用互斥锁,在 Do 方法中再次调用同一个 Once 的 Do 方法会尝试再次获取已经持有的锁。
解决方案:避免在 Do 方法中调用同一个 Once 的 Do 方法。
go
// 错误示例
func main() {
var once sync.Once
once.Do(func() {
fmt.Println("Outer initialization")
once.Do(func() { // 错误:会导致死锁
fmt.Println("Inner initialization")
})
})
}
// 正确示例
func main() {
var once1 sync.Once
var once2 sync.Once
once1.Do(func() {
fmt.Println("Outer initialization")
once2.Do(func() {
fmt.Println("Inner initialization")
})
})
}5. 常见应用场景
5.1 单例模式
场景描述:需要创建一个全局唯一的实例,确保只初始化一次。
使用方法:使用 Once 来保证初始化函数只执行一次。
示例代码:
go
package singleton
import "sync"
type Singleton struct {
data string
}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{data: "initialized"}
println("Singleton initialized")
})
return instance
}
func main() {
for i := 0; i < 5; i++ {
go func() {
s := GetInstance()
println(s.data)
}()
}
// 等待所有 Goroutine 完成
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}5.2 配置加载
场景描述:需要从文件或环境变量加载配置,确保只加载一次。
使用方法:使用 Once 来保证配置加载函数只执行一次。
示例代码:
go
package config
import (
"fmt"
"sync"
)
type Config struct {
Port int
Host string
}
var (
config Config
once sync.Once
)
func LoadConfig() Config {
once.Do(func() {
// 模拟从文件加载配置
config = Config{
Port: 8080,
Host: "localhost",
}
fmt.Println("Config loaded")
})
return config
}
func main() {
for i := 0; i < 3; i++ {
go func() {
c := LoadConfig()
fmt.Printf("Config: %+v\n", c)
}()
}
// 等待所有 Goroutine 完成
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}5.3 数据库连接初始化
场景描述:需要初始化数据库连接池,确保只初始化一次。
使用方法:使用 Once 来保证数据库连接初始化函数只执行一次。
示例代码:
go
package database
import (
"fmt"
"sync"
)
type DB struct {
connection string
}
var (
db *DB
once sync.Once
)
func GetDB() *DB {
once.Do(func() {
// 模拟数据库连接初始化
db = &DB{connection: "connected"}
fmt.Println("Database connected")
})
return db
}
func main() {
for i := 0; i < 4; i++ {
go func() {
d := GetDB()
fmt.Println(d.connection)
}()
}
// 等待所有 Goroutine 完成
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}5.4 资源初始化
场景描述:需要初始化一些重量级资源,如网络连接、文件句柄等,确保只初始化一次。
使用方法:使用 Once 来保证资源初始化函数只执行一次。
示例代码:
go
package resource
import (
"fmt"
"sync"
)
type Resource struct {
name string
}
var (
resource *Resource
once sync.Once
)
func GetResource() *Resource {
once.Do(func() {
// 模拟资源初始化
resource = &Resource{name: "initialized resource"}
fmt.Println("Resource initialized")
})
return resource
}
func main() {
for i := 0; i < 5; i++ {
go func() {
r := GetResource()
fmt.Println(r.name)
}()
}
// 等待所有 Goroutine 完成
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}5.5 延迟初始化
场景描述:需要在首次使用时才初始化某个组件,确保只初始化一次。
使用方法:使用 Once 来保证延迟初始化函数只执行一次。
示例代码:
go
package lazy
import (
"fmt"
"sync"
)
type LazyComponent struct {
data string
}
var (
component *LazyComponent
once sync.Once
)
func GetComponent() *LazyComponent {
once.Do(func() {
// 模拟延迟初始化
component = &LazyComponent{data: "lazy initialized"}
fmt.Println("Component lazily initialized")
})
return component
}
func main() {
fmt.Println("Program started")
// 首次使用时才初始化
fmt.Println("First use:")
c1 := GetComponent()
fmt.Println(c1.data)
// 再次使用时不会重新初始化
fmt.Println("Second use:")
c2 := GetComponent()
fmt.Println(c2.data)
}6. 企业级进阶应用场景
6.1 全局缓存初始化
场景描述:在大型应用中,需要初始化全局缓存,确保只初始化一次,避免重复加载数据。
使用方法:使用 Once 来保证缓存初始化函数只执行一次,结合通道和 WaitGroup 处理并发初始化。
示例代码:
go
package cache
import (
"fmt"
"sync"
"time"
)
type Cache struct {
data map[string]string
mu sync.RWMutex
}
var (
globalCache *Cache
once sync.Once
initDone chan struct{}
)
func InitCache() {
once.Do(func() {
// 模拟缓存初始化,加载大量数据
globalCache = &Cache{
data: make(map[string]string),
}
// 模拟加载数据
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key%d", i)
value := fmt.Sprintf("value%d", i)
globalCache.data[key] = value
}
fmt.Println("Global cache initialized")
close(initDone)
})
}
func GetCache() *Cache {
if initDone == nil {
initDone = make(chan struct{})
go InitCache()
}
select {
case <-initDone:
return globalCache
case <-time.After(5 * time.Second):
panic("Cache initialization timeout")
}
}
func main() {
// 多个 Goroutine 同时获取缓存
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cache := GetCache()
fmt.Printf("Goroutine %d: cache size = %d\n", id, len(cache.data))
}(i)
}
wg.Wait()
}6.2 服务注册与发现
场景描述:在微服务架构中,需要注册服务到服务 registry,确保只注册一次。
使用方法:使用 Once 来保证服务注册函数只执行一次,结合 Context 处理取消操作。
示例代码:
go
package service
import (
"context"
"fmt"
"sync"
"time"
)
type Service struct {
name string
port int
}
var (
service *Service
once sync.Once
registered bool
registerCtx context.Context
cancel context.CancelFunc
)
func RegisterService(name string, port int) error {
var err error
once.Do(func() {
registerCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 模拟服务注册
fmt.Printf("Registering service %s on port %d\n", name, port)
time.Sleep(2 * time.Second) // 模拟网络延迟
service = &Service{
name: name,
port: port,
}
registered = true
fmt.Println("Service registered successfully")
})
if !registered {
err = fmt.Errorf("service registration failed")
}
return err
}
func GetService() *Service {
if service == nil {
panic("Service not registered")
}
return service
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
err := RegisterService("user-service", 8080)
if err != nil {
fmt.Printf("Goroutine %d: registration failed: %v\n", id, err)
return
}
s := GetService()
fmt.Printf("Goroutine %d: service = %+v\n", id, s)
}(i)
}
wg.Wait()
}6.3 插件系统初始化
场景描述:在插件系统中,需要加载和初始化插件,确保每个插件只初始化一次。
使用方法:使用 Once 来保证每个插件的初始化函数只执行一次,结合映射来管理多个插件。
示例代码:
go
package plugin
import (
"fmt"
"sync"
)
type Plugin interface {
Name() string
Init() error
}
type pluginWrapper struct {
plugin Plugin
once sync.Once
err error
}
var (
plugins = make(map[string]*pluginWrapper)
mu sync.RWMutex
)
func RegisterPlugin(p Plugin) {
mu.Lock()
defer mu.Unlock()
plugins[p.Name()] = &pluginWrapper{
plugin: p,
}
}
func InitPlugin(name string) error {
mu.RLock()
pw, ok := plugins[name]
mu.RUnlock()
if !ok {
return fmt.Errorf("plugin %s not found", name)
}
pw.once.Do(func() {
pw.err = pw.plugin.Init()
if pw.err == nil {
fmt.Printf("Plugin %s initialized successfully\n", name)
} else {
fmt.Printf("Plugin %s initialization failed: %v\n", name, pw.err)
}
})
return pw.err
}
func GetPlugin(name string) (Plugin, error) {
mu.RLock()
pw, ok := plugins[name]
mu.RUnlock()
if !ok {
return nil, fmt.Errorf("plugin %s not found", name)
}
if err := InitPlugin(name); err != nil {
return nil, err
}
return pw.plugin, nil
}
// 示例插件
type LoggerPlugin struct{}
func (p *LoggerPlugin) Name() string {
return "logger"
}
func (p *LoggerPlugin) Init() error {
fmt.Println("Initializing logger plugin")
return nil
}
func main() {
// 注册插件
RegisterPlugin(&LoggerPlugin{})
// 多个 Goroutine 同时初始化插件
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
plugin, err := GetPlugin("logger")
if err != nil {
fmt.Printf("Goroutine %d: failed to get plugin: %v\n", id, err)
return
}
fmt.Printf("Goroutine %d: got plugin: %s\n", id, plugin.Name())
}(i)
}
wg.Wait()
}6.4 全局状态管理
场景描述:在大型应用中,需要管理全局状态,确保状态初始化只执行一次。
使用方法:使用 Once 来保证全局状态初始化函数只执行一次,结合互斥锁保护状态访问。
示例代码:
go
package global
import (
"fmt"
"sync"
)
type GlobalState struct {
counter int
mu sync.RWMutex
}
var (
state *GlobalState
once sync.Once
)
func InitState() {
once.Do(func() {
state = &GlobalState{
counter: 0,
}
fmt.Println("Global state initialized")
})
}
func GetState() *GlobalState {
InitState()
return state
}
func (s *GlobalState) Increment() {
s.mu.Lock()
defer s.mu.Unlock()
s.counter++
}
func (s *GlobalState) GetCounter() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.counter
}
func main() {
var wg sync.WaitGroup
// 多个 Goroutine 同时访问全局状态
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s := GetState()
s.Increment()
}()
}
wg.Wait()
s := GetState()
fmt.Printf("Final counter value: %d\n", s.GetCounter())
}7. 行业最佳实践
7.1 用于初始化单例
实践内容:使用 Once 实现单例模式,确保全局唯一实例只初始化一次。
推荐理由:Once 提供了简洁、线程安全的方式来实现单例模式,避免了手动管理锁和状态的复杂性。
7.2 用于延迟初始化
实践内容:使用 Once 实现延迟初始化,只有在首次使用时才初始化组件。
推荐理由:延迟初始化可以减少程序启动时间,避免初始化不必要的组件,提高资源利用效率。
7.3 结合 Context 使用
实践内容:结合 context 包使用 Once,实现带超时控制的初始化。
推荐理由:Context 提供了取消信号和超时控制,与 Once 结合使用可以构建更加健壮的初始化流程。
7.4 错误处理
实践内容:在 Once 的 Do 函数中处理错误,确保初始化失败时能够正确处理。
推荐理由:Once 本身不处理错误,需要在 Do 函数中自行处理错误,确保初始化过程中的错误能够被捕获和处理。
7.5 避免在 Do 中执行耗时操作
实践内容:避免在 Once 的 Do 函数中执行耗时操作,以免阻塞其他 Goroutine。
推荐理由:Once 的 Do 方法会阻塞所有调用它的 Goroutine,直到函数执行完成,执行耗时操作会影响系统性能。
7.6 使用闭包捕获参数
实践内容:当需要向 Do 函数传递参数时,使用闭包来捕获参数。
推荐理由:Once 的 Do 方法只接受无参数、无返回值的函数,使用闭包可以灵活地传递参数。
7.7 不可复制 Once
实践内容:通过指针传递 Once,避免复制使用中的 Once。
推荐理由:Once 是结构体,复制后会创建一个新的实例,与原实例状态无关,通过指针传递可以确保操作的是同一个实例。
7.8 与 WaitGroup 结合使用
实践内容:当需要等待多个初始化操作完成时,结合 WaitGroup 使用 Once。
推荐理由:Once 保证单个操作只执行一次,WaitGroup 等待多个操作完成,两者结合可以构建复杂的初始化流程。
8. 常见问题答疑(FAQ)
8.1 Once 和 sync.Mutex 有什么区别?
问题描述:Once 和 sync.Mutex 都是同步原语,它们有什么区别?
回答内容:
- Once:专门用于保证某个操作只执行一次,内部使用互斥锁实现。
- sync.Mutex:用于保护共享资源,防止多个 Goroutine 同时访问导致的竞态条件。
- 使用场景:
- 当需要确保某个操作只执行一次时,使用 Once。
- 当需要保护共享资源时,使用 sync.Mutex。
示例代码:
go
// 使用 Once 实现单例
var (
instance *Service
once sync.Once
)
func GetService() *Service {
once.Do(func() {
instance = &Service{}
})
return instance
}
// 使用 Mutex 保护共享资源
var (
counter int
mu sync.Mutex
)
func Increment() {
mu.Lock()
defer mu.Unlock()
counter++
}8.2 Once 可以用于哪些场景?
问题描述:Once 适用于哪些场景?
回答内容:
- 单例模式:创建全局唯一的实例。
- 配置加载:从文件或环境变量加载配置。
- 数据库连接初始化:初始化数据库连接池。
- 资源初始化:初始化重量级资源,如网络连接、文件句柄等。
- 延迟初始化:在首次使用时才初始化某个组件。
- 服务注册:在微服务架构中注册服务到服务 registry。
示例代码:
go
// 配置加载
var (
config Config
once sync.Once
)
func LoadConfig() Config {
once.Do(func() {
// 加载配置
})
return config
}8.3 Once 是线程安全的吗?
问题描述:Once 是否是线程安全的?
回答内容:
- 是的,Once 是线程安全的。
- Once 的 Do 方法使用了互斥锁和原子操作来保证并发安全。
- 多个 Goroutine 同时调用 Do 时,只有一个 Goroutine 会执行函数,其他 Goroutine 会等待函数执行完成。
示例代码:
go
var once sync.Once
func main() {
for i := 0; i < 10; i++ {
go func() {
once.Do(func() {
fmt.Println("Initialized")
})
}()
}
// 等待所有 Goroutine 完成
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}
// 输出:Initialized(只输出一次)8.4 Once 可以重置吗?
问题描述:Once 执行后可以重置吗?
回答内容:
- 不可以,Once 一旦执行了函数,就不能重置,无法再次执行新的函数。
- 这是 Once 的设计意图,确保操作只执行一次。
- 如果需要重复执行初始化操作,可以使用自定义的可重置 Once 实现。
示例代码:
go
// 自定义可重置的 Once
type ResettableOnce struct {
m sync.Mutex
done uint32
}
func (o *ResettableOnce) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
func (o *ResettableOnce) Reset() {
o.m.Lock()
defer o.m.Unlock()
atomic.StoreUint32(&o.done, 0)
}8.5 Once 在函数执行过程中发生 panic 会怎么样?
问题描述:如果 Once 的 Do 函数执行过程中发生 panic,会怎么样?
回答内容:
- Once 会认为函数已经执行过,不会再次执行。
- 这是因为 Once 在执行函数之前会获取锁,执行完成后会设置 done 标记,即使函数发生 panic,锁也会被释放,done 标记也会被设置。
- 因此,在 Do 函数中应该处理 panic,确保初始化操作能够正确完成。
示例代码:
go
var once sync.Once
func main() {
once.Do(func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recovered from panic: %v\n", r)
}
}()
panic("Initialization failed")
})
// 这里不会再次执行
once.Do(func() {
fmt.Println("This won't be executed")
})
}8.6 Once 和 init() 函数有什么区别?
问题描述:Once 和 init() 函数都可以用于初始化,它们有什么区别?
回答内容:
- init() 函数:在包被导入时自动执行,不能控制执行时机,会增加程序启动时间。
- Once:在首次调用 Do 方法时执行,可以控制执行时机,支持延迟初始化。
- 使用场景:
- 当需要在包导入时就初始化时,使用 init() 函数。
- 当需要延迟初始化或控制初始化时机时,使用 Once。
示例代码:
go
// 使用 init() 函数
func init() {
fmt.Println("Package initialized")
}
// 使用 Once
var once sync.Once
func Init() {
once.Do(func() {
fmt.Println("Initialized on first call")
})
}9. 实战练习
9.1 基础练习:实现单例模式
题目:使用 Once 实现一个线程安全的单例模式。
解题思路:
- 使用 Once 来保证初始化函数只执行一次。
- 定义一个全局变量来存储单例实例。
- 提供一个获取单例实例的函数。
常见误区:
- 忘记使用 Once,导致多次初始化。
- 没有处理并发访问的情况。
分步提示:
- 定义单例结构体。
- 定义全局变量存储单例实例和 Once。
- 实现获取单例实例的函数,使用 Once 保证只初始化一次。
- 测试多个 Goroutine 同时获取单例实例。
参考代码:
go
package main
import (
"fmt"
"sync"
)
type Singleton struct {
data string
}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{data: "initialized"}
fmt.Println("Singleton initialized")
})
return instance
}
func main() {
var wg sync.WaitGroup
// 多个 Goroutine 同时获取单例实例
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
s := GetInstance()
fmt.Printf("Goroutine %d: %s\n", id, s.data)
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}9.2 进阶练习:配置管理器
题目:使用 Once 实现一个配置管理器,支持从文件加载配置,确保只加载一次。
解题思路:
- 使用 Once 来保证配置加载函数只执行一次。
- 定义配置结构体和全局变量。
- 提供加载配置和获取配置的函数。
常见误区:
- 没有处理配置加载错误。
- 没有使用 Once,导致重复加载配置。
分步提示:
- 定义配置结构体。
- 定义全局变量存储配置和 Once。
- 实现加载配置的函数,使用 Once 保证只加载一次。
- 实现获取配置的函数。
- 测试多个 Goroutine 同时获取配置。
参考代码:
go
package main
import (
"fmt"
"sync"
)
type Config struct {
Server struct {
Host string
Port int
}
Database struct {
DSN string
}
}
var (
config Config
once sync.Once
)
func LoadConfig() {
once.Do(func() {
// 模拟从文件加载配置
config = Config{
Server: struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
},
Database: struct {
DSN string
}{
DSN: "user:pass@tcp(localhost:3306)/db",
},
}
fmt.Println("Config loaded")
})
}
func GetConfig() Config {
LoadConfig()
return config
}
func main() {
var wg sync.WaitGroup
// 多个 Goroutine 同时获取配置
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
c := GetConfig()
fmt.Printf("Goroutine %d: Server=%s:%d, DB=%s\n",
id, c.Server.Host, c.Server.Port, c.Database.DSN)
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}9.3 挑战练习:插件系统
题目:使用 Once 实现一个简单的插件系统,支持插件注册和初始化,确保每个插件只初始化一次。
解题思路:
- 定义插件接口。
- 使用映射存储插件和对应的 Once。
- 实现插件注册、初始化和获取的函数。
常见误区:
- 没有处理插件初始化错误。
- 没有使用 Once,导致插件重复初始化。
- 没有保护映射的并发访问。
分步提示:
- 定义插件接口。
- 定义映射存储插件和对应的 Once。
- 实现插件注册函数。
- 实现插件初始化函数,使用 Once 保证每个插件只初始化一次。
- 实现获取插件的函数。
- 测试多个 Goroutine 同时初始化和获取插件。
参考代码:
go
package main
import (
"fmt"
"sync"
)
type Plugin interface {
Name() string
Init() error
Execute() string
}
type pluginWrapper struct {
plugin Plugin
once sync.Once
err error
}
var (
plugins = make(map[string]*pluginWrapper)
mu sync.RWMutex
)
func RegisterPlugin(p Plugin) {
mu.Lock()
defer mu.Unlock()
plugins[p.Name()] = &pluginWrapper{
plugin: p,
}
}
func InitPlugin(name string) error {
mu.RLock()
pw, ok := plugins[name]
mu.RUnlock()
if !ok {
return fmt.Errorf("plugin %s not found", name)
}
pw.once.Do(func() {
pw.err = pw.plugin.Init()
if pw.err == nil {
fmt.Printf("Plugin %s initialized successfully\n", name)
} else {
fmt.Printf("Plugin %s initialization failed: %v\n", name, pw.err)
}
})
return pw.err
}
func GetPlugin(name string) (Plugin, error) {
mu.RLock()
pw, ok := plugins[name]
mu.RUnlock()
if !ok {
return nil, fmt.Errorf("plugin %s not found", name)
}
if err := InitPlugin(name); err != nil {
return nil, err
}
return pw.plugin, nil
}
// 示例插件
type LoggerPlugin struct{}
func (p *LoggerPlugin) Name() string {
return "logger"
}
func (p *LoggerPlugin) Init() error {
fmt.Println("Initializing logger plugin")
return nil
}
func (p *LoggerPlugin) Execute() string {
return "Logger plugin executed"
}
type DatabasePlugin struct{}
func (p *DatabasePlugin) Name() string {
return "database"
}
func (p *DatabasePlugin) Init() error {
fmt.Println("Initializing database plugin")
return nil
}
func (p *DatabasePlugin) Execute() string {
return "Database plugin executed"
}
func main() {
// 注册插件
RegisterPlugin(&LoggerPlugin{})
RegisterPlugin(&DatabasePlugin{})
var wg sync.WaitGroup
// 多个 Goroutine 同时初始化和获取插件
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 初始化和获取 logger 插件
logger, err := GetPlugin("logger")
if err != nil {
fmt.Printf("Goroutine %d: failed to get logger plugin: %v\n", id, err)
return
}
fmt.Printf("Goroutine %d: %s\n", id, logger.Execute())
// 初始化和获取 database 插件
db, err := GetPlugin("database")
if err != nil {
fmt.Printf("Goroutine %d: failed to get database plugin: %v\n", id, err)
return
}
fmt.Printf("Goroutine %d: %s\n", id, db.Execute())
}(i)
}
wg.Wait()
fmt.Println("All goroutines completed")
}10. 知识点总结
10.1 核心要点
- Once 是什么:Once 是 Go 语言中用于保证某个操作只执行一次的同步原语,属于
sync包。 - 基本用法:使用
once.Do(func())来执行只需要执行一次的操作。 - 并发安全:Once 的 Do 方法是并发安全的,多个 Goroutine 同时调用 Do 时,只有一个 Goroutine 会执行函数。
- 零值可用:Once 的零值是可用的,不需要初始化。
- 不可重置:Once 一旦执行了函数,就不能重置,无法再次执行新的函数。
- 不可复制:Once 是结构体,不是引用类型,不要复制使用中的 Once。
10.2 易错点回顾
- 传递有参数的函数:使用闭包来捕获参数。
- 函数执行过程中发生 panic:在函数内部处理 panic,确保初始化操作能够正确完成。
- 复制 Once:通过指针传递 Once,避免复制它。
- 尝试重置 Once:Once 不可重置,需要使用自定义的重置机制。
- 在 Do 中调用 Do:避免在 Do 方法中调用同一个 Once 的 Do 方法,会导致死锁。
- 执行耗时操作:避免在 Once 的 Do 函数中执行耗时操作,以免阻塞其他 Goroutine。
11. 拓展参考资料
11.1 官方文档链接
11.2 进阶学习路径建议
- 并发安全:学习
sync包中的其他同步原语,如 Mutex、RWMutex、Cond 等。 - 通道:学习通道的使用和通道模式,如生产者-消费者模式、扇入扇出模式等。
- Context:学习 Context 的使用,实现更复杂的并发控制,如超时控制、取消操作等。
- 错误处理:学习
errgroup包,结合 Once 和错误处理。 - 性能优化:学习并发性能优化技巧,如工作池、批量处理等。
11.3 相关资源
- Go 语言实战:介绍 Go 语言的并发编程和同步原语。
- Go 并发编程实战:深入讲解 Go 语言的并发编程技术。
- Go 语言设计与实现:深入分析 Go 语言的内部实现,包括 Once 的底层原理。
