Go中sync.Once源码的深度讲解

 更新时间:2025年01月28日 09:53:08   作者:gopher_looklook  
sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有所帮助

概念

sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次。它在多线程环境中非常有用,尤其是在需要初始化共享资源或执行某些一次性任务时。

简单示例

当我们在web服务访问某个路由时,如果需要事先获取某些配置,往往会写一个loadConfig函数,获取一个cfg配置项。多次路由访问所需要获取的配置项通常是相同的,如果对于每次路由访问,都加载一次loadConfig函数,会导致产生一些不必要的开销。如果loadConfig涉及到读取文件、解析配置、网络请求时,有可能会额外增加的请求响应时间,降低服务的吞吐量。使用sync.Once包提供的Do函数,就可以只在第一次请求时调用loadConfig函数加载配置,之后的请求都复用第一次请求的配置,缩短响应时间。

package main

import (
    "log"
    "net/http"
    "sync"
)

type Config struct {
    APIKey   string
    LogLevel string
}

var (
    config *Config
    once   sync.Once
)

func loadConfig() {
    // 模拟从文件或环境变量加载配置
    config = &Config{
       APIKey:   "secret-key",
       LogLevel: "debug",
    }
    log.Println("Configuration loaded")
}

func GetConfig() *Config {
    once.Do(loadConfig) // 仅第一次访问时会执行loadConfig函数
    return config
}

func handler(w http.ResponseWriter, r *http.Request) {
    cfg := GetConfig()
    log.Printf("Request handled with API key: %s", cfg.APIKey)
    w.Write([]byte("OK"))
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8888", nil))
}

源码解读

源码文件:src/sync/once.go (go 1.23 版本)

package sync

import (
    "sync/atomic"
)

type Once struct {
    done atomic.Uint32 // 是否已执行标识位,0-未执行 1-已执行
    m    Mutex         // 互斥锁,确保并发安全
}

func (o *Once) Do(f func()) {
    // 第一次执行Do函数时,原子操作检查o.done==0,执行doSlow函数后,o.done==1
    // 第二次及之后执行Do函数,原子操作检查o.done标识位为1,Do函数不执行任何功能,确保了f函数只在第一次被执行
    if o.done.Load() == 0 {
       o.doSlow(f) // 调用doSlow函数执行f方法。第一次执行时,同一时间可能有多个goroutine尝试同时执行doSlow函数
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock() // 加锁保护,避免多个goroutine同时绕过之前的原子操作检查,并发修改o.done的值
    defer o.m.Unlock()
    // 二次检查o.done的值,同一时间并发执行doSlow函数的goroutine,在第一个goroutine将o.done置为1并解除互斥锁后,
    // 剩下的goroutine识别到自身的o.done已经被设为1,无法绕过二次检查
    if o.done.Load() == 0 { 
       defer o.done.Store(1) // 需要在f()函数执行完成之后,原子性地将o.done设为1
       f() // 执行f方法,一定只有一个goroutine会调用这个方法
    }
}

可以看到,once.go文件的代码非常精炼。仅定义了一个含2个非导出字段donem的结构体Once,并提供了一个doSlow方法用于执行f函数。当我们调用Do方法时,程序经历了几个关键步骤:

  • 判断done标志位是否等于0,如果是,说明f函数还没有被执行,执行doSlow方法
  • mu互斥锁加锁,防止多个goroutine并发操作
  • double-check done标志位是否等于0,如果是,说明f函数还没有被执行,执行f函数
  • f函数执行完成之后,再将done标志位原子性设为1。使用原子操作是从内存可见性的角度出发,如果done使用uint32而不是atomic.Uint32done修改可能不会立即被其它goroutine感知,解锁后仍有可能存在goroutinedone等于0,重复执行f函数
  • mu互斥锁解锁。此时进入到doSlow函数的其它goroutine也感知到了o.done等于1,不会重复执行f函数了

总结

上文就是针对Go源码sync.Once原理和使用方式的讲解。在实际开发中,sync.Once的使用还是非常普遍的。掌握sync.Once的底层原理,有助于我们在今后的开发中更有把握地利用它永远只执行一次函数的特性,完成复杂的技术需求或者业务需求。

到此这篇关于Go中sync.Once源码的深度讲解的文章就介绍到这了,更多相关Go sync.Once内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go 自定义package包设置与导入操作

    Go 自定义package包设置与导入操作

    这篇文章主要介绍了Go 自定义package包设置与导入操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • Go语言中进行API限流的实战详解

    Go语言中进行API限流的实战详解

    API 限流是控制和管理应用程序访问量的重要手段,旨在防止恶意滥用、保护后端服务的稳定性和可用性,下面我们就来看看如何在Go语言中具体实现吧
    2025-01-01
  • 手把手带你走进Go语言之条件表达式

    手把手带你走进Go语言之条件表达式

    条件表达式由条件运算符构成,并常用条件表达式构成一个赋值语句,本文给大家介绍了在Go语言中条件表达式的具体用法,讲述的非常详细,对大家的学习或工作具有一定的参考借鉴价值
    2021-09-09
  • Golang Gorm实现自定义多态模型关联查询

    Golang Gorm实现自定义多态模型关联查询

    GORM 是一个流行的开源 ORM (Object-Relational Mapping) 库,专为 Go 语言设计,它简化了与 SQL 数据库的交互,GORM 封装了数据库操作,使得开发者能够通过简单的链式调用来执行 CRUD,本文给大家介绍了Golang Gorm实现自定义多态模型关联查询,需要的朋友可以参考下
    2024-11-11
  • golang不到30行代码实现依赖注入的方法

    golang不到30行代码实现依赖注入的方法

    这篇文章主要介绍了golang不到30行代码实现依赖注入的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • go语言中proto文件的使用

    go语言中proto文件的使用

    在Go语言编程中,.proto文件用于定义Protocol Buffers数据结构和服务,是实现跨语言通信和高效序列化的关键,具有一定的参考价值,感兴趣的可以了解一下
    2024-10-10
  • go MethodByName()不能获取私有方法的解决

    go MethodByName()不能获取私有方法的解决

    本文主要介绍了go MethodByName()不能获取私有方法的解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-02-02
  • 一文掌握Golang的panic和recover实战

    一文掌握Golang的panic和recover实战

    Go语言中,异常处理通常依赖error返回值,本文将通过示例展示如何在Go语言中正确使用recover来处理panic异常,防止程序直接崩溃,感兴趣的朋友跟随小编一起看看吧
    2024-09-09
  • IdeaGo启动报错Failed to create JVM的问题解析

    IdeaGo启动报错Failed to create JVM的问题解析

    这篇文章主要介绍了IdeaGo启动报错Failed to create JVM的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • GoLang语法之标准库fmt.Printf的使用

    GoLang语法之标准库fmt.Printf的使用

    fmt包实现了类似C语言printf和scanf的格式化I/O,主要分为向外输出内容和获取输入内容两大部分,本文就来介绍一下GoLang语法之标准库fmt.Printf的使用,感兴趣的可以了解下
    2023-10-10

最新评论