見出し画像

Go 1.21 で追加された sync パッケージの新機能について


はじめに

皆様、こんにちは。バックエンドエンジニアの栄です。

先日、Go 1.21 がリリースされました。
今回のリリースは、新しい組み込み関数の追加や既存パッケージの更新が比較的多いリリースでありました。
本記事では、私が特に興味深いと感じた sync パッケージの変更点を取り上げてみたいと思います。

変更点サマリー

下記の3つの関数が新たに追加されました。

  • sync.OnceFunc

  • sync.OnceValue

  • sync.OnceValues

これらの関数は sync.Once 構造体の一般的な使用方法を関数化したものとなります。

sync.Once とは

追加された三つの関数を理解するには、sync.Once 構造体の概要について知っておくことが重要です。

sync.Once 構造体には、任意の操作を一度だけ行う為の機能として Do メソッドが実装されています。
Do は引数として、引数無し返り値無しの関数 func() を受け取り、同一の sync.Once 構造体において Do が初めて呼び出される場合のみ引数の関数が実行されます。
なお、ゴルーチンセーフに実装されている為、異なるゴルーチン間で呼び出した場合にも引数の関数は一度しか実行されないことが担保されています。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

var once sync.Once

func fn() {
    fmt.Println("Hello")
}

func main() {
    once.Do(fn)
    once.Do(fn)
}

実行結果:

Hello

注意点は、Do を複数回呼び出す際に呼び出し毎に異なる関数を渡したとしても、実行されるのは初めて Do が呼び出される際に渡された関数のみである点です。
Do が初めて呼び出されたかどうか?の判定のみが実行のトリガーとなります。

サンプルコード: 

package main

import (
    "fmt"
    "sync"
)

var once sync.Once

func fn() {
    fmt.Println("Hello")
}

func fn2() {
    fmt.Println("Hello2")
}

func main() {
    once.Do(fn)
    once.Do(fn2)
}

実行結果:

Hello

sync.OnceFunc とは

sync.OnceFunc は、引数で渡された関数を一度だけ実行する関数を返します。
sync.OnceFunc を利用することで、関数の実行を初回呼び出し時だけに制限したいケースにて処理を簡潔に記述することができます。
Do メソッドに毎回同じ関数を渡して呼び出す処理が不要となり、実行したい関数を sync.OnceFunc の引数で渡し、返り値の関数を繰り返し実行するように実装すれば同様の処理を実現することが可能です。
sync.Once 構造体の Do メソッドと同様にゴルーチンセーフとなっています。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

var onceFunc = sync.OnceFunc(func() {
    fmt.Println("Hello")
})

func main() {
    onceFunc()
    onceFunc()
}

実行結果:

Hello

sync.OnceValue とは

sync.OnceValue は sync.OnceFunc と同様に、引数で渡された関数を一度だけ実行する関数を生成します。
引数で渡す関数は一つの値を返す関数である必要があります。
生成される関数は、引数で渡した関数の返り値と同じ型の一つの値を返すようになっており、この値には引数で指定した関数の実行結果が格納されます。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

var count int

var onceValue = sync.OnceValue(func() int {
    count++
    return count
})

func main() {
    fmt.Println(onceValue())
    fmt.Println(onceValue())
}

実行結果:

1
1

sync.OnceValues とは

sync.OnceValues は基本的には sync.OnceValue と同じような機能が提供されています。
sync.OnceValue と異なるのは、引数で渡す関数は二つの値を返す関数である必要があり、生成される関数も二つの値を返すようになっている点です。
Go では関数の返り値として (T, error) または (T, bool) のように二つの値を返すのが一般的であるため、そのようなケースを想定した作りとなっています。

サンプルコード:

package main

import (
    "fmt"
    "sync"
)

var count int

var onceValues = sync.OnceValues(func() (int, bool) {
    isInitialValue := count == 0
    count++
    return count, isInitialValue
})

func main() {
    fmt.Println(onceValues())
    fmt.Println(onceValues())
}

実行結果:

1 true
1 true

シングルトンパターンでの使用例

上記で紹介した関数の簡単な使用例として、デザインパターンの一つであるシングルトンパターンの実装を考えてみます。
シングルトンパターンでは、単一のインスタンスをアプリケーション全体で共有します。このインスタンスを生成する際に sync.OnceValue で生成された関数を使うことで、確実に一度だけインスタンスが生成されるように実装することができます。

サンプルコード:

package main

import (
	"fmt"
	"sync"
)

type singleton struct{}

var newSingletonOnce = sync.OnceValue(func() *singleton {
	return &singleton{}
})

func newSingleton() *singleton {
	return newSingletonOnce()
}

func main() {
	instance1 := newSingleton()
	instance2 := newSingleton()
	fmt.Println(instance1 == instance2)
}

実行結果:

true

シングルトンパターンのインスタンス生成の他にも、データベース接続の初期化やキャッシュの生成などの、処理を一度だけ実行したい様々なケースで使用できます。
Web アプリケーションなどの複数のゴルーチンが同時に立ち上がるようなシステムの開発においてかなり役立ちそうですね。

さいごに

今回は Go 1.21 で追加された sync パッケージの三つの関数について簡単に紹介しました。
標準ライブラリや組み込みの機能が乏しいと言われがちな Go ですが、リリース毎に汎用的で便利な機能が追加されており、書きやすく読みやすい言語へと成長していると感じています。
より効率的にシステムの開発、運用ができるよう、今後もバックエンドチーム全体でキャッチアップを継続していきたいと思います。

この記事が気に入ったらサポートをしてみませんか?