1. Introduction
This article mainly introduces the Once concurrent primitives in the Go language, including the basic usage methods, principles and precautions of Once, so as to have a basic understanding of the use of Once.
2. Basic use
2.1 Basic definition
It is a concurrent primitive in Go language, which is used to ensure that a certain function is executed only once.
Once
There is one typeDo
Method, which takes a function as an argument and executes the function on the first call. ifDo
The method is called multiple times, and only the first call will execute the incoming function.
2.2 How to use
useVery simple, just create one
Once
variable of type, then call it where it needs to be guaranteed that the function is executed only onceDo
Just the method. Here is a simple example:
var once func initOperation() { // Some initialization operations will only be performed once} func main() { // Execute the initOperation function when the program starts, ensuring that the initialization is executed only once (initOperation) // Subsequent code}
2.3 Use examples
Here is a simple useExamples of which we use
To ensure that the global variable config will only be initialized once:
package main import ( "fmt" "sync" ) var ( config map[string]string once ) func loadConfig() { // Simulate loading configuration information from the configuration file ("load config...") config = make(map[string]string) config["host"] = "127.0.0.1" config["port"] = "8080" } func GetConfig() map[string]string { (loadConfig) return config } func main() { // The first call to GetConfig will execute the loadConfig function and initialize the config variable (GetConfig()) // The second call to GetConfig will not execute the loadConfig function, and will directly return the initialized config variable (GetConfig()) }
In this example, we define a global variableconfig
And oneVariables of type
once
. existGetConfig
In the function, we call itMethod to ensure
loadConfig
The function will be executed only once, thus ensuring thatconfig
The variable will only be initialized once. Run the above program and the output is as follows:
load config... map[host:127.0.0.1 port:8080] map[host:127.0.0.1 port:8080]
You can see,GetConfig
The function was executed on the first callloadConfig
Function, initializedconfig
variable. On the second call,loadConfig
The function will not be executed, and it will directly return the initializedconfig
variable.
3. Principle
Below isThe specific implementation is as follows:
type Once struct { done uint32 m Mutex } func (o *Once) Do(f func()) { // Determine whether the done flag bit is 0 if atomic.LoadUint32(&) == 0 { // Outlined slow-path to allow inlining of the fast-path. (f) } } func (o *Once) doSlow(f func()) { // Add lock () defer () // Perform double check to determine whether the function has been executed again if == 0 { defer atomic.StoreUint32(&, 1) f() } }
The implementation principle is relatively simple and mainly depends on one
done
Flag bit and a mutex lock. whenDo
When the method is called for the first time, it will be read atomically first.done
Flag bit. If the flag bit is 0, it means that the function has not been executed yet. At this time, the incoming function will be locked and executed, and thedone
The flag position is 1, and then release the lock. If the flag is 1, it means that the function has been executed and will be returned directly.
4. Precautions for use
4.1 Cannot be used as a local variable for function
Here is a simple example that willProblems caused by local variables:
var config map[string]string func initConfig() { ("initConfig called") config["1"] = "hello world" } func getConfig() map[string]string{ var once (initCount) ("getConfig called") } func main() { for i := 0; i < 10; i++ { go getConfig() } () }
Here the initialization function will be called multiple times, which is the same asinitConfig
The method will only be executed once and the expected inconsistency is not true. This is becauseWhen used as a local variable, a new function is created every time the function is called
Instance, each
All instances have their own
done
Flags, state cannot be shared between multiple instances. This results in the initialization function being called multiple times.
IfThis problem can be avoided as a global variable or a package-level variable. Therefore, based on this, it cannot be defined
Used as a function local variable.
4.2 Cannot be called again in
Here is one inCalled again in the method
Examples of methods:
package main import ( "fmt" "sync" ) func main() { var once var onceBody func() onceBody = func() { ("Only once") (onceBody) // Call the method again } // Execution method (onceBody) ("done") }
In the above code,(onceBody)
When the first execution is executed, "Only once" will be output, and then it is executed(onceBody)
Deadlock will occur and the program cannot continue to execute.
This is because()
The method will acquire the mutex during execution and call it again within the method.()
Method, then a deadlock will appear when a mutex is acquired.
Therefore, we cannot call the method again in the method.
4.3 Error processing is required for the passed function
4.3.1 Basic description
Generally speaking, if the passed function does not have an error, error processing can be avoided. However, if an incoming function may have an error, it must be handled incorrectly, otherwise it may cause the program to crash or unpredictable errors.
Therefore, when writing functions that pass into Once, error handling needs to be taken into account to ensure the robustness and stability of the program.
4.3.2 Problems caused by unerrorized handling
Here is an example where an incoming function may have an error, but there is no error handling:
import ( "fmt" "net" "sync" ) var ( initialized bool connection initOnce ) func initConnection() { connection, _ = ("tcp", "err_address") } func getConnection() { (initConnection) return connection } func main() { conn := getConnection() (conn) () }
In the above example, whereinitConnection
It is an incoming function, used to establish a TCP network connection, but inWhen executing this function in the process, it is possible to return an error, but there is no error processing here, so the error is directly ignored. Called at this time
getConnection
Method, ifinitConnection
If an error is reported, an empty connection will be returned when obtaining the connection, and a null pointer exception will occur after subsequent calls. Therefore, ifThe function in it may have an exception and it should be processed at this time.
4.3.3 Processing method
- 4.3.3.1 Panic exits execution
When the application is first started, it is calledTo initialize some resources, an error occurs at this time. At the same time, the initialized resources must be initialized. You can consider using panic to exit the program in the event of an error to avoid the program continuing to execute, which will lead to greater problems. Specific code examples are as follows:
import ( "fmt" "net" "sync" ) var ( connection initOnce ) func initConnection() { // Try to establish a connection connection, err = ("tcp", "err_address") if err != nil { panic(" error") } } func getConnection() { (initConnection) return connection }
As mentioned above, when the initConnection method reports an error, we directly panic and exit the execution of the entire program.
- 4.3.3.2 Modification
Implementation, the semantics of the Do function are modified to be executed successfully once only
During the program running, you can choose to record the log or return the error code without interrupting the execution of the program. Then the initialization logic is executed the next time you call it. Need to be correct hereRenovation, original
The implementation of the Do function is executed once, and here it is modified to be executed successfully only once. The specific usage method needs to be determined based on the specific business scenario. Here is one of the implementations:
type MyOnce struct { done int32 m } func (o *MyOnce) Do(f func() error) { if atomic.LoadInt32(&) == 0 { (f) } } func (o *MyOnce) doSlow(f func() error) { () defer () if == 0 { // Done will be set only if the function call does not return err if err := f(); err == nil { atomic.StoreInt32(&, 1) } } }
In the above code, an error handling logic is added. whenf()
When the function returns an error, it will not bedone
The mark position is 1 so that the initialization logic can be re-executed the next time it is called.
It should be noted that although this method can solve the problem after initialization failure, it may cause the initialization function to be called multiple times. Therefore, in writingf()
When functioning, this problem needs to be taken into account to avoid unexpected results.
Here is a simple example, using our re-implemented Once, showing that when the first initialization fails, the second call will re-execute the initialization logic and be successfully initialized:
var ( hasCall bool conn m MyOnce ) func initConn() (, error) { ("initConn...") // The first execution will directly return an error if !hasCall { return nil, ("init error") } // The second execution is successful, the initialization is successful here. conn, _ = ("tcp", ":80") return conn, nil } func GetConn() (, error) { (func() error { var err error conn, err = initConn() if err != nil { return err } return nil }) // After the first execution, set hasCall to true and let it execute the initialization logic hasCall = true return conn, nil } func main() { // The first execution of initialization logic failed GetConn() // The initialization logic will still be executed the second time, and the execution will be successful. GetConn() // The second execution is successful and the third call will not execute the initialization logic GetConn() }
In this example, the first callDo
The method initialization failed,done
The mark bit is set to 0. On the second callDo
When the method isdone
If the flag bit is 0, the initialization logic will be re-execute. This time the initialization is successful.done
The mark bit is set to 1. The third call, due to the previousDo
The method has been executed successfully and the initialization logic will not be executed again.
5. Summary
This article aims to introduce the Once concurrent primitives in the Go language, including their basic usage, principles and precautions, so that everyone can have a basic understanding of Once.
First, we demonstrate the basic usage of Once with examples and emphasize its feature that it will only be executed once. We then explain why Once is executed only once, allowing readers to better understand how Once works. Finally, we pointed out some precautions when using Once to avoid misuse.
In short, this article comprehensively introduces the Once concurrent primitive in Go, allowing readers to better understand and apply it.
The above is the detailed explanation of the use examples of go concurrent tools. For more information about go concurrent tools, please pay attention to my other related articles!