在日常开发中,我们经常会遇到这样的场景:某个函数只需要执行一次,其结果可以被多次重复使用。比如配置文件的读取、数据库连接初始化、复杂计算结果的缓存等。在Go语言中,sync.Once 是解决这类问题的老牌利器,但它在使用上存在一些不便——无法直接返回计算结果。

Go 1.21版本引入了新的函数 sync.OnceValue,它完美解决了这一痛点。

为什么需要 sync.OnceValue?

在介绍 sync.OnceValue 之前,我们先回顾一下传统的 sync.Once 如何使用:

var (
    config map[string]string
    once   sync.Once
)

func GetConfig() map[string]string {
    once.Do(func() {
        // 从文件或远程加载配置
        fmt.Println("Loading config...")
        config = make(map[string]string)
        config["host"] = "127.0.0.1"
        config["port"] = "8080"
    })
    return config
}

这种方式虽然能保证初始化逻辑只执行一次,但需要在外层声明一个变量来存储结果,代码显得有些冗余。

sync.OnceValue 的出现正是为了解决这个问题,它将 sync.Once 与结果缓存封装在一起,提供了更简洁的API。

什么是 sync.OnceValue?

sync.OnceValue 是 Go 1.21 中新增的函数,其函数签名如下:

func OnceValuef func( T) func() T

它接受一个函数 f 作为参数,该函数返回一个类型为 T 的值。OnceValue 返回一个新函数,这个新函数在首次调用时执行 f 并返回结果,后续调用直接返回缓存的結果,不再执行 f

简单来说,sync.OnceValue 将"只执行一次"的逻辑与结果缓存结合在一起,让我们能够用更少的代码实现相同的功能。

如何使用 sync.OnceValue?

基础用法

package main

import (
    "fmt"
    "sync"
)

func main() {
    // 创建一个只会执行一次的计算函数
    calculate := sync.OnceValue(func() int {
        fmt.Println("Start complex calculation")
        sum := 0
        for i := 0; i < 1000000; i++ {
            sum += i
        }
        return sum
    })

    // 多次调用,但只会计算一次
    fmt.Println("Result:", calculate())
    fmt.Println("Result:", calculate()) // 这次不会重新计算
}

运行上述代码,你会发现尽管我们调用了两次 calculate(),但 "Start complex calculation" 只打印了一次,说明计算确实只执行了一次。

并发安全的使用方式

sync.OnceValue 天生支持并发安全,多个 goroutine 同时调用也不会导致重复执行:

package main

import (
    "fmt"
    "sync"
)

func main() {
    calculate := sync.OnceValue(func() int {
        fmt.Println("Start complex calculation")
        sum := 0
        for i := 0; i < 1000000; i++ {
            sum += i
        }
        return sum
    })

    // 并发调用
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println("Result:", calculate())
        }()
    }
    wg.Wait()
}

在这个例子中,我们启动了5个goroutine同时调用 calculate(),但计算逻辑只会执行一次。

错误处理:OnceValues

实际开发中,很多函数会返回值和错误信息,比如读取文件、数据库查询等。Go 1.21 还提供了 sync.OnceValues 函数来处理这种情况:

func OnceValuesf func( (T1, T2)) func() (T1, T2)

使用示例:

package main

import (
    "fmt"
    "os"
    "sync"
)

func main() {
    // 只读取一次文件
    readFile := sync.OnceValues(func() ([]byte, error) {
        fmt.Println("Reading file")
        return os.ReadFile("config.json")
    })

    // 并发读取
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            data, err := readFile()
            if err != nil {
                fmt.Println("Error:", err)
                return
            }
            fmt.Println("File length:", len(data))
        }()
    }
    wg.Wait()
}

这种方式特别适合处理需要返回 (value, error) 的场景,符合 Go 语言的错误处理习惯。

panic 处理机制

sync.OnceValuesync.Once 在 panic 处理上有重要区别:

  • sync.Once:如果 f 函数发生 panic,Once 会将其视为已执行完毕,后续调用不会再次执行 f
  • sync.OnceValue:如果 f 函数发生 panic,返回的函数在每次调用时都会再次以相同的 panic 值终止

这种差异意味着在使用 sync.OnceValue 时,我们需要确保传入的函数有足够的健壮性,或者在函数内部处理好可能的 panic。

性能特点

sync.OnceValue 基于 sync.Once 实现,因此具有相似的性能特征:

  • 首次调用:约 50-100 纳秒(主要开销来自互斥锁)
  • 后续调用:约 1-2 纳秒(仅一次原子加载操作)

这种性能表现使得 sync.OnceValue 非常适合高性能场景,初始化后几乎零开销。

写在最后

sync.OnceValue 是 Go 1.21 中一个简单但实用的新增功能,它通过将 sync.Once 与结果缓存封装在一起,提供了更简洁、更直观的API。与传统的 sync.Once 相比,它的主要优势包括:

  1. 代码更简洁:无需在外层声明变量来存储结果
  2. 使用更方便:直接返回计算结果,无需额外的变量声明
  3. 错误处理更完善:通过 OnceValues 支持 (value, error) 返回模式
  4. 并发安全:天生支持多 goroutine 并发调用

虽然 sync.OnceValue 在大多数场景下是更好的选择,但如果你需要更细粒度的控制(比如在初始化失败时希望重试),传统的 sync.Once 可能更合适。

总的来说,sync.OnceValue 是 Go 语言在并发编程方面的一个有用补充,值得大家在合适场景中积极采用。