SoFunction
Updated on 2025-03-05

Detailed explanation of the use example of go concurrency tool

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.OnceThere is one typeDoMethod, which takes a function as an argument and executes the function on the first call. ifDoThe method is called multiple times, and only the first call will execute the incoming function.

2.2 How to use

useVery simple, just create oneOncevariable of type, then call it where it needs to be guaranteed that the function is executed only onceDoJust 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 useTo 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 variableconfigAnd oneVariables of typeonce. existGetConfigIn the function, we call itMethod to ensureloadConfigThe function will be executed only once, thus ensuring thatconfigThe 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,GetConfigThe function was executed on the first callloadConfigFunction, initializedconfigvariable. On the second call,loadConfigThe function will not be executed, and it will directly return the initializedconfigvariable.

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 onedoneFlag bit and a mutex lock. whenDoWhen the method is called for the first time, it will be read atomically first.doneFlag 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 thedoneThe 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 asinitConfigThe 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 calledInstance, eachAll instances have their owndoneFlags, 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 definedUsed as a function local variable.

4.2 Cannot be called again in

Here is one inCalled again in the methodExamples 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, whereinitConnectionIt 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 timegetConnectionMethod, ifinitConnectionIf 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 ModificationImplementation, 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, originalThe 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(&amp;) == 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(&amp;, 1)
      }
   }
}

In the above code, an error handling logic is added. whenf()When the function returns an error, it will not bedoneThe 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 callDoThe method initialization failed,doneThe mark bit is set to 0. On the second callDoWhen the method isdoneIf the flag bit is 0, the initialization logic will be re-execute. This time the initialization is successful.doneThe mark bit is set to 1. The third call, due to the previousDoThe 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!