当且仅当 Do 是第一次为 Once 的实例调用时,Do 才调用函数 f。
func (o *Once) Do(f func())
sync.Once
应用场景
sync.Once
是 Go 标准库中提供的一种并发原语,用于确保某个操作只执行一次,不论有多少个 goroutine 调用它。它常用于初始化操作或需要确保只执行一次的操作。下面是一些实际中的应用场景:
1. 单例模式(Singleton)
在创建单例对象时,使用 sync.Once
可以确保对象只被创建一次:
package main
import (
"fmt"
"sync"
)
type Singleton struct{}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
func main() {
s1 := GetInstance()
s2 := GetInstance()
fmt.Println(s1 == s2) // 输出:true,说明两个实例是相同的
}
2. 初始化全局资源
例如,数据库连接池、配置文件加载等,只需要在应用启动时初始化一次:
package main
import (
"database/sql"
"log"
"sync"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
var once sync.Once
func initDB() {
var err error
db, err = sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
}
func GetDB() *sql.DB {
once.Do(initDB)
return db
}
func main() {
// 使用数据库连接
database := GetDB()
// 执行查询或其他操作
_ = database
}
3. 延迟初始化
当某些资源需要延迟初始化时,使用 sync.Once
可以确保初始化操作只执行一次:
package main
import (
"fmt"
"sync"
)
var config map[string]string
var once sync.Once
func loadConfig() {
config = map[string]string{
"host": "localhost",
"port": "8080",
}
}
func GetConfig() map[string]string {
once.Do(loadConfig)
return config
}
func main() {
cfg := GetConfig()
fmt.Println(cfg)
}
4. HTTP Handler 初始化
在 HTTP 服务器中,有时需要确保某些初始化操作(如加载模板、初始化缓存等)只执行一次:
package main
import (
"fmt"
"net/http"
"sync"
)
var once sync.Once
func initHandler() {
fmt.Println("Handler initialized")
}
func handler(w http.ResponseWriter, r *http.Request) {
once.Do(initHandler)
fmt.Fprintf(w, "Hello, world!")
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
5. 日志初始化
确保日志只初始化一次,避免重复创建文件或连接日志服务:
package main
import (
"log"
"os"
"sync"
)
var logger *log.Logger
var once sync.Once
func initLogger() {
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
logger = log.New(file, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
}
func GetLogger() *log.Logger {
once.Do(initLogger)
return logger
}
func main() {
logger := GetLogger()
logger.Println("This is a log message")
}
以上是一些常见的 sync.Once
应用场景,通过这些例子可以看出,sync.Once
在确保某些操作只执行一次方面非常有用,尤其是在并发环境中。
sync.Once和init函数区别
在Go中,sync.Once
和init
函数都可以用于初始化操作,但它们有不同的适用场景和特性。下面是它们的区别和适用场景:
init
函数
- 自动调用:
init
函数在包被导入时自动调用,无需显式调用。 - 每个文件可以有多个
init
函数:一个包中的每个源文件都可以定义自己的init
函数,它们在包初始化时按顺序自动执行。 - 包级初始化:
init
函数通常用于包级别的初始化操作,如初始化包级变量或设置环境。 - 无参数和返回值:
init
函数不接受参数,也没有返回值。
示例:
package main
import (
"fmt"
)
var globalVar string
func init() {
globalVar = "initialized in init"
fmt.Println("init function called")
}
func main() {
fmt.Println(globalVar) // 输出:initialized in init
}
sync.Once
- 显式调用:
sync.Once
的Do
方法需要显式调用来执行初始化操作。 - 用于并发环境:
sync.Once
主要用于并发环境中,确保某个操作只执行一次,无论有多少个goroutine同时调用它。 - 可以多次调用
Do
方法:可以在代码中多次调用Do
方法,但只有第一次调用会执行初始化操作,后续调用不会重复执行。
示例:
package main
import (
"fmt"
"sync"
)
var once sync.Once
var globalVar string
func initialize() {
globalVar = "initialized in sync.Once"
fmt.Println("initialize function called")
}
func main() {
for i := 0; i < 10; i++ {
go func() {
once.Do(initialize)
}()
}
// 等待一段时间以确保所有goroutine都完成
// 在实际代码中应使用sync.WaitGroup或其他同步机制
select {}
}
区别总结
-
调用时机:
init
函数在包初始化时自动调用。sync.Once
需要显式调用其Do
方法。
-
并发支持:
init
函数不涉及并发,只在包加载时执行一次。sync.Once
设计用于并发环境,确保某个操作在并发情况下只执行一次。
-
适用场景:
init
函数适用于包级别的初始化,如设置包级变量、初始化配置等。sync.Once
适用于需要在并发环境中确保某个操作只执行一次的情况,如单例模式、延迟初始化等。
-
灵活性:
init
函数只能在包初始化时使用,不灵活。sync.Once
更灵活,可以在任何需要的地方使用,并且可以根据条件决定是否执行初始化操作。
通过了解这些区别和适用场景,你可以根据具体需求选择使用init
函数或sync.Once
进行初始化。
延迟初始化好处
延迟初始化(Lazy Initialization)是在需要时才进行初始化,而不是在应用程序启动时立即初始化。这种方法通常用于资源密集型对象、配置文件、连接池等,只有在第一次使用时才创建,从而节省启动时间和资源。
延迟初始化的应用场景
- 资源密集型对象:如数据库连接、文件句柄等,这些资源在程序启动时不需要立即创建,只有在实际使用时才需要初始化。
- 配置文件加载:配置文件可能只在某些操作需要时才加载,而不是在程序启动时加载。
- 插件加载:某些插件可能只在特定功能需要时才加载。
- 单例模式:单例对象在需要时才创建,而不是在应用程序启动时立即创建。
延迟初始化示例
下面是一个使用sync.Once
进行延迟初始化的例子:
示例:延迟初始化数据库连接
假设我们有一个需要在第一次访问时才进行初始化的数据库连接:
package main
import (
"database/sql"
"fmt"
"sync"
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
var once sync.Once
func initDB() {
var err error
db, err = sql.Open("mysql", "user:password@/dbname")
if err != nil {
fmt.Println("Failed to connect to the database:", err)
}
// 这里可以添加更多初始化代码,比如设置数据库连接池参数等
fmt.Println("Database initialized")
}
func GetDB() *sql.DB {
once.Do(initDB)
return db
}
func main() {
// 第一次调用GetDB时会初始化数据库连接
database := GetDB()
if database != nil {
fmt.Println("Database connection established")
}
// 再次调用GetDB时不会再次初始化
anotherDatabase := GetDB()
if anotherDatabase != nil {
fmt.Println("Database connection reused")
}
}
在这个示例中,initDB
函数只会在第一次调用GetDB
时执行,后续的调用不会再次初始化数据库连接。
延迟初始化的好处
- 节省资源:避免在程序启动时初始化不必要的资源。
- 提高启动速度:减少启动时的初始化操作,使程序更快启动。
- 按需分配资源:只有在需要时才分配资源,避免不必要的资源占用。
延迟初始化的潜在问题
- 第一次访问延迟:第一次访问时可能会有初始化延迟,尤其是初始化操作耗时较长时。
- 线程安全:需要确保延迟初始化在并发环境中是线程安全的,使用
sync.Once
可以确保线程安全。
通过使用延迟初始化,可以优化资源使用,提高应用程序的性能和响应速度。