Skip to content

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 方法的主要功能是保证传入的函数只执行一次:

  1. 首先通过原子操作检查 done 标记,如果已经是 1,则直接返回。
  2. 如果 done 是 0,则获取互斥锁。
  3. 再次检查 done 标记(双重检查锁定模式),如果已经是 1,则释放锁并返回。
  4. 执行传入的函数。
  5. done 标记设置为 1。
  6. 释放互斥锁。

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,导致多次初始化。
  • 没有处理并发访问的情况。

分步提示

  1. 定义单例结构体。
  2. 定义全局变量存储单例实例和 Once。
  3. 实现获取单例实例的函数,使用 Once 保证只初始化一次。
  4. 测试多个 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,导致重复加载配置。

分步提示

  1. 定义配置结构体。
  2. 定义全局变量存储配置和 Once。
  3. 实现加载配置的函数,使用 Once 保证只加载一次。
  4. 实现获取配置的函数。
  5. 测试多个 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,导致插件重复初始化。
  • 没有保护映射的并发访问。

分步提示

  1. 定义插件接口。
  2. 定义映射存储插件和对应的 Once。
  3. 实现插件注册函数。
  4. 实现插件初始化函数,使用 Once 保证每个插件只初始化一次。
  5. 实现获取插件的函数。
  6. 测试多个 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 相关资源