Suppose you have some initialization code that should only be executed once, regardless of how many times it's called from different parts of your program or by different goroutines. If you don't have a mechanism to control this, you might end up with race conditions where multiple goroutines are attempting to initialize the same resource concurrently, leading to unpredictable and potentially incorrect behavior.
The Go language provides a simple and efficient way to guarantee that a given function is executed only once. It uses a combination of a boolean flag and a mutex to ensure that only one goroutine executes the specified function, while other goroutines that attempt to execute the same function will wait until the initialization is complete. It call Sync.Once
.
>> Read more about Golang:
- Go Tutorial: Golang Basics of Knowledge for Beginners
- API Development in Go with Gin Framework
- A Comprehensive Guide To Dockerize A Golang Application
- Detailed Guide for Simplifying Testing with Golang Testify
- Type Conversion in Golang
- Understanding Golang Ordered Map with Code Examples
What is sync.Once?
sync.Once is a synchronization primitive provided by the Golang sync package. It is used to perform a certain operation exactly once, regardless of how many times the Once instance is consulted.
3 Common Scenarios for Using sync.Once
sync.Once is particularly useful in scenarios where you want to perform a certain initialization or setup operation only once, regardless of how many times that operation is requested. It's commonly used in scenarios like this:
Lazy Initialization
When you want to initialize a resource or perform a setup operation only when it is first needed, and you want to avoid the overhead of repeated initialization.
var once sync.Once
var expensiveResource *SomeType
func getExpensiveResource() *SomeType {
once.Do(func() {
expensiveResource = initializeExpensiveResource()
})
return expensiveResource
}
Singleton Pattern
Ensuring that a certain operation, like creating a singleton instance, is performed only once, even in a concurrent environment.
var once sync.Once
var instance *Singleton
func GetInstance() *Singleton {
once.Do(func() {
instance = createSingletonInstance()
})
return instance
}
Package-Level Initialization
In package-level variables or initialization functions, where you want to ensure that certain setup code is executed only once when the package is used.
package mypackage
import (
"sync"
)
var (
once sync.Once
initialized bool
)
func initialize() {
// Initialization code here
initialized = true
}
func MyFunction() {
once.Do(initialize)
// Rest of the code
}
In these cases, sync.Once
provides a clean and efficient way to handle the one-time initialization, and it ensures that the initialization is safe for concurrent use. It helps avoid race conditions and guarantees that the initialization code is executed exactly once.
3 New Features of sync.Once in Go 1.21
The recently introduced OnceFunc
, OnceValue
, and OnceValues
functions encapsulate a common usage of Once, designed for the deferred initialization of a value upon its initial use.
OnceFunc
func OnceFunc(f func()) func()
OnceFunc
return a function that supports concurrent calls and can be invoked multiple times.
The following code, onceVoid
, is only executed once.
package main
import (
"log"
"sync"
)
func main() {
onceVoid := func() {
log.Println("Only once")
}
call := sync.OnceFunc(onceVoid)
for i := 0; i < 10; i++ {
call()
}
}
Result:
2023/12/18 22:45:01 Only once
OnceValue
func OnceValue[T any](f func() T) func() T
OnceValue
the returned function retrieves the result of the initial function call, ensuring that subsequent invocations yield the same value.
In the code below, randNum
only be executed once, returning the result as n
, and each call to the bar function will return n
. bar can be called concurrently.
package main
import (
"log"
"math/rand"
"sync"
)
func main() {
randNum := func() int {
return rand.Int() % 10
}
getNum := sync.OnceValue(randNum)
for i := 0; i < 10; i++ {
log.Println(getNum())
}
}
Result:
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
2023/12/18 22:48:48 4
randNum
return one random value no matter how many getNum
called that the reason why call many time but still receive value 4.
OnceValues
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
In the case of OnceValues
and OnceValue
, they serve a comparable purpose, with the distinction that the former returns two parameters.
To summarise:
- The three functions yield 0, 1, and 2 return values, respectively, upon invocation of the associated function.
- The returned functions can be called concurrently.
- Should the execution of function f result in a panic, the returned function will also panic upon subsequent calls, preserving the same panic value as encountered during the execution of f.
package main
import (
"log"
"math/rand"
"sync"
)
func main() {
randTwoNum := func() (int, int) {
return rand.Int() % 10, rand.Int() % 10
}
getTwoNum := sync.OnceValues(randTwoNum)
for i := 0; i < 10; i++ {
log.Println(getTwoNum())
}
}
Result:
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
2023/12/18 22:56:52 4 6
Same result with OnceValue
but the different is that return two value only once.
Keep going with OnceValues
we can receive once more than two value
package main
import (
"log"
"math/rand"
"sync"
)
func main() {
getThreeNum := sync.OnceValues(func() (int, func() (int, int)) {
return rand.Int() % 10, sync.OnceValues(func() (int, int) {
return rand.Int() % 10, rand.Int() % 10
})
})
for i := 0; i < 10; i++ {
v1, f1 := getThreeNum()
v2, v3 := f1()
log.Println(v1, v2, v3)
}
}
Result:
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
2023/12/18 23:09:28 8 4 9
OnceValues
returns a function that invokes f
only once and returns the values returned by f
. The returned function may be called concurrently. If f
panics, the returned function will panic with the same value on every call.
>> You may be interested in these Golang-related blogs:
- Best Practices For Dependency Inversion in Golang
- Detailed Code Examples of Dependency Inversion in Golang
- Hands-On Implementation for Dependency Injection in Go
- Practical SOLID in Golang: Single Responsibility Principle
Conclusion
In conclusion, the introduction of the new functions, OnceFunc
, OnceValue
, and OnceValues
, in Go's sync.Once
package is a significant enhancement that adds valuable flexibility and convenience to the already powerful synchronization primitive. Understanding and leveraging these new functions is crucial for Go developers, as they provide elegant solutions for deferred initialization scenarios with varying degrees of complexity.
- The ability to use
OnceFunc
enables developers to encapsulate operations that need to be executed only once, creating a function that supports concurrent calls and can be invoked multiple times. This feature is particularly useful in scenarios where an operation should be performed exactly once, but the result is not needed immediately. - With
OnceValue
, the Go language introduces a mechanism that not only ensures a function is executed only once but also caches and returns the result upon subsequent invocations. This is especially beneficial for scenarios where the cost of computation is high, and the result can be reused multiple times. - The introduction of
OnceValues
extends this capability further, allowing developers to handle functions that return multiple values. This function provides a clean and concise way to deal with scenarios where initialization involves multiple results or configurations.
In essence, mastering these new sync.Once
functions is a must for any Go developer, as they address common challenges in concurrent programming, enhance code readability, and provide a more expressive and efficient way to handle deferred initialization. As the Go language evolves, staying informed and adopting these improvements will contribute to writing more robust, scalable, and maintainable Go code.
>>> Follow and Contact Relia Software for more information!
- golang
- coding
- development