Go 言語における Singleton Pattern

今回は小ネタでお送りします。

いや,ネットでね,Go 言語での Singleton 実装をこんな感じに書く人をやたら見かけるのだが

//Hello class
type Hello struct{}

var instance *Hello

//GetInstance returns singleton instance
func GetInstance() *Hello {
    if instance == nil {
        instance = &Hello{}
    }
    return instance
}

はっきり言って Singleton なめんな! ですよ。

そうそう。 プログラマで Singleton Pattern を知らない人はいないと思うけど,一応解説しておくと, Singleton Pattern というのは,あるクラスに対してプログラム全体でインスタンスがひとつだけ生成されるよう制限するプログラミング・パターンである。 たとえば,外部と通信を行う entity class なんかはインスタンスがぼこぼこできて各々勝手に処理をされると困るわけで, singleton インスタンスの内部で同期をとっていく必要があるわけ。

という説明からも分かると思うけど「スレッドセーフでない singleton 実装に存在意義はない」のである。 ちなみに結城浩さんのデザパタ本にあるサンプルコードも以下のようになっている(こっちは Java での記述だけど1)。

import java.util.Date;

public class MySystem {
    private static MySystem instance = null;
    private Date date = new Date();
    private MySystem() {
    }
    public Date getDate() {
        return date;
    }
    public static synchronized MySystem getInstance() {
        if (instance == null) {
            instance = new MySystem();
        }
        return instance;
    }
}

staticsynchronized なのがポイントね。 つまり,実際にインスタンスを生成する処理では何らかの手段でスレッドセーフであることが保証されてないといけない2。 最初の Go 言語のパターンが何故ダメなのかは実際に動くコードを書いてみれば分かる。

package main

import (
    "fmt"
    "time"
)

//Hello class
type Hello struct{}

func (h *Hello) String() string {
    return "Hello"
}

var instance *Hello

//GetInstance returns singleton instance
func GetInstance() *Hello {
    if instance == nil {
        fmt.Println("new instance")
        time.Sleep(1 * time.Second) //delay 1sec
        instance = &Hello{}
    }
    return instance
}

func main() {
    ch := make(chan interface{})
    go run(ch, "Alice")
    go run(ch, "Bob")
    go run(ch, "Chris")
    <-ch
    <-ch
    <-ch
}

func run(ch chan<- interface{}, person string) {
    hello := GetInstance()
    ch <- hello //blocking
    fmt.Println(hello, person)
}

簡単に説明すると,まず GetInstance() 関数内部で初期化処理時間を演出するために1秒間の delay を発生させている。 run() 関数内で channel ch にインスタンスを食わせているのはブロッキングのため。 別に何を食わせてもいいのだが,手近に GetInstance() 関数で取得したインスタンスがあるので,それを食わせている。 main() 関数では run() 関数を goroutine で3連続起動したあと, <-ch でブロックを解除している。

実行結果は以下の通り。

new instance
new instance
new instance
Hello Alice
Hello Chris
Hello Bob

並行処理下の3つの run() 関数に対してインスタンスが3つ生成されてしまっているのが分かると思う。

ではどう書けばいいのか。 一番簡単なのは var 宣言時に初期化してしまうことである。

var instance = &Hello{}

//GetInstance returns singleton instance
func GetInstance() *Hello {
    return instance
}

次に簡単なのは init() 関数を使うことである。

var instance *Hello

func init() {
    //create instance and initialize
    instance = &Hello{}
}

init() 関数は少し特殊な関数で,main() 関数がキックされる前,パッケージ内の var 宣言時の初期化の後に呼ばれる。 ひとつのパッケージ内またはひとつのファイル内にいくつも init() 関数を設置できるのが特徴なのだが,どういう順番に起動するかは言語仕様として明記されていないため3,パッケージ内の複数の init() 関数同士が依存また干渉するような書き方は避けるべきだろう。

main() 関数がキックされるまではメイン以外の goroutine は(生成は可能だが)起動されないことが保証されているため,記述がスレッドセーフか否か気にする必要はない。 言い方を変えると,何らかの同期を伴う初期化処理の場合はこの方法では記述できないことになる。

「どうしても GetInstance() 関数内で同期をとりたいんじゃ」という場合は… たとえば sync パッケージを使うとかだろうか。 Singleton Pattern におあつらえ向きの sync.Once というのがある。 最初に挙げた例を流用するならこんな感じだろうか。

//Hello class
type Hello struct{}

var instance *Hello
var once sync.Once

//GetInstance returns singleton instance
func GetInstance() *Hello {
    once.Do(func() {
        instance = &Hello{}
    })
    return instance
}

以下のコードで実際に動かして検証してみよう。

package main

import (
    "fmt"
    "sync"
    "time"
)

//Hello class
type Hello struct{}

func (h *Hello) String() string {
    return "Hello"
}

var instance *Hello
var once sync.Once

//GetInstance returns singleton instance
func GetInstance() *Hello {
    once.Do(func() {
        fmt.Println("new instance")
        time.Sleep(1 * time.Second) //delay 1sec
        instance = &Hello{}
    })
    return instance
}

func main() {
    ch := make(chan interface{})
    go run(ch, "Alice")
    go run(ch, "Bob")
    go run(ch, "Chris")
    <-ch
    <-ch
    <-ch
}

func run(ch chan<- interface{}, person string) {
    hello := GetInstance()
    ch <- hello //blocking
    fmt.Println(hello, person)
}

実行結果は以下の通り。

new instance
Hello Chris
Hello Alice
Hello Bob

ちゃんと singleton として動作していることが分かる。

ブックマーク

参考図書

photo
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan Brian W. Kernighan 柴田 芳樹
丸善出版 2016-06-20
評価

スターティングGo言語 (CodeZine BOOKS) Go言語によるWebアプリケーション開発 Kotlinスタートブック -新しいAndroidプログラミング Docker実戦活用ガイド グッド・マス ギークのための数・論理・計算機科学

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。

reviewed by Spiegel on 2016-07-13 (powered by G-Tools)

photo
Java言語で学ぶリファクタリング入門
結城 浩
SBクリエイティブ 2007-01-26
評価

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編 増補改訂版 Java言語で学ぶデザインパターン入門 数学ガールの秘密ノート/積分を見つめて プログラマの数学 Java言語プログラミングレッスン 第3版(下) オブジェクト指向を始めよう 実践Javaコーディング作法 プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則 数学ガールの秘密ノート/微分を追いかけて 数学ガール/ガロア理論 Java言語プログラミングレッスン 第3版(上) Java言語を始めよう

結城浩さんによる通称「デザパタ本」。 Java 以外でも使える優れもの。

reviewed by Spiegel on 2017-10-24 (powered by G-Tools)

photo
増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編
結城 浩
SBクリエイティブ 2004-06-18
評価

増補改訂版 Java言語で学ぶデザインパターン入門 Java言語で学ぶリファクタリング入門 数学ガールの秘密ノート/積分を見つめて アプリケーションアーキテクチャ設計パターン C言語による スーパーLinuxプログラミング Cライブラリの活用と実装・開発テクニック プログラマの数学 数学ガール/ガロア理論 数学ガールの秘密ノート/微分を追いかけて 数学ガールの秘密ノート/数列の広場 数学ガールの秘密ノート/整数で遊ぼう

結城浩さんによる通称「デザパタ本」のマルチスレッド編。 Java 以外でも使える優れもの。

reviewed by Spiegel on 2017-10-24 (powered by G-Tools)


  1. このコードのパターンは Singleton Pattern を説明するにはよく出来ているしちゃんと動く(ココ重要)が,同期コストが高いため,実際にはあまり使われない。 Java における Singleton Pattern には様々な実装例があるので探してみるといいだろう。ちなみに Java 使いではなくとも結城浩さんのデザパタ本は買って読んでおくことを強くお勧めする。 Java 使いの方から見ると古いバージョンで書かれたコードなのが難点だが,紙の本で買うとサンプルコード入りのディスクが付いてくるので若干お得? [return]
  2. 他にもインスタンスのコピー(コピーコンストラクタ等)を暗黙的に許容する言語ではコピーを無効にする措置が必要,とかある。そういう意味じゃ今回私が書いたコードも不完全で,実際には singleton インスタンスを隠蔽するためのラッパークラスが必要になる。ビジネス・ロジックも含めると,実は Singleton の実装ってそう甘くないのよねー [return]
  3. どうもソースファイルのファイル名が影響するらしい。つまりファイル名を工夫すれば init() 関数の呼び出し順を制御できる,という噂。 [return]