Go's Once and Do Functions in the sync Package: A Practical Guide

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Overview
  4. Using the Once Function
  5. Using the Do Function
  6. Real-World Example
  7. Recap

Introduction

In Go programming language, the sync package provides various synchronization primitives to simplify concurrent programming. Two such important functions are Once and Do. The Once function ensures that a given piece of code is executed only once regardless of the number of goroutines calling it. On the other hand, the Do function allows executing a function only if it has not been executed before. In this tutorial, we will explore these functions and understand how to use them effectively.

By the end of this tutorial, you will:

  • Understand the purpose and benefits of using Once and Do functions
  • Know how to utilize the Once function to execute code only once
  • Know how to utilize the Do function to execute code if it has not been executed before
  • Be able to apply these concepts in real-world scenarios

Prerequisites

To follow along with this tutorial, you should have a basic understanding of Go programming language, including goroutines and concurrency. Make sure Go is properly installed on your system.

Overview

Concurrency in Go brings challenges when multiple goroutines need to access shared resources or execute critical sections of code. The sync package provides atomic operations and synchronization primitives such as Mutex and WaitGroup to address these challenges. The Once and Do functions offer additional control by ensuring certain code portions are executed in a specific way.

Using the Once Function

The Once function ensures a specific code block is executed only once regardless of the number of goroutines calling it. It is particularly useful when initializing a resource that should be created only once. Here’s the basic syntax of Once:

var once sync.Once

func doOnce() {
    once.Do(func() {
        // code to be executed only once
    })
}

Let’s break down the code:

  • We declare a variable once of type sync.Once. This variable will keep track of the execution state.
  • In the doOnce function, we call once.Do() passing a function as an argument. The code inside this function block will be executed only once, even if doOnce is invoked by multiple goroutines simultaneously.

Note: The code inside the Do function will block until it completes. Hence, make sure to avoid any long-running or blocking operations to prevent other goroutines from being blocked.

Using the Do Function

The Do function allows executing a function only if it has not been executed before. This function can be used when you want to perform a specific action once, but not necessarily restrict other goroutines from executing it simultaneously. Here’s the basic syntax of Do:

var done bool
var mu sync.Mutex

func doIfNotDone() {
    mu.Lock()
    defer mu.Unlock()

    if !done {
        // code to be executed if not done before
        done = true
    }
}

Let’s analyze the code:

  • We declare a boolean variable done to track whether the code has been executed before.
  • A sync.Mutex is used to synchronize access to the done variable.
  • In the doIfNotDone function, we first acquire the lock using mu.Lock() and then release it using defer mu.Unlock(). This ensures that only one goroutine can check/modify done at a time.
  • Inside the function, we check if done is false. If it is, we execute the code and set done to true. Subsequent invocations will see done as true and skip the code.

Note: The sync.Mutex approach can be useful when you need a more fine-grained control over execution and want to allow concurrent execution up to a certain point.

Real-World Example

Let’s consider a real-world example where the Once and Do functions can be applied.

var (
    initDBOnce sync.Once
    db         *sql.DB
    dbErr      error
)

func initializeDB() {
    initDBOnce.Do(func() {
        db, dbErr = sql.Open("mysql", "user:password@tcp(localhost:3306)/db")
    })
}

func serveHTTP() {
    // ...
    initializeDB()
    // ...
}

Here’s what the code does:

  • We declare a sync.Once variable initDBOnce to ensure the database is initialized only once.
  • The initializeDB function uses initDBOnce.Do() to check if initialization has already been done. If not, it opens a connection to a MySQL database.
  • In the serveHTTP function (e.g., an HTTP handler), we call initializeDB to ensure the database is initialized before serving any requests.

With this pattern, regardless of the number of goroutines calling serveHTTP, the database will be initialized only once due to the Once guarantee.

Recap

In this tutorial, we explored the Once and Do functions in the sync package of Go programming language. We learned to utilize the Once function to ensure a given code block is executed only once, regardless of concurrent calls. We also understood how to use the Do function to execute code only if it has not been executed before. Additionally, we saw a real-world example where these functions can be applied.

Understanding and effectively using these functions can significantly simplify concurrent programming in Go while ensuring correctness and performance. Experiment with different scenarios to gain more familiarity with Once and Do functions.

Remember, leveraging the power of Go’s concurrency features requires careful design and synchronization. With the sync package and its functions like Once and Do, you can better control the execution of critical sections in concurrent programs.

Happy coding!