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 なんかはインスタンスがぼこぼこできて各々勝手に処理をされると困るわけで1, singleton インスタンスの内部で同期をとっていく必要があるわけ。

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

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 なのがポイントね。 つまり,実際にインスタンスを生成する処理では何らかの手段でスレッドセーフであることが保証されてないといけない3。 最初の 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() 関数を設置できるのが特徴なのだが,どういう順番に起動するかは言語仕様として明記されていないため4,パッケージ内の複数の 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 として動作していることが分かる。

ブックマーク

参考図書

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan, Brian W. Kernighan
丸善出版
評価 

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

reviewed by Spiegel on 2018.10.19 (powered by Amakuri)

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

結城浩さんによる「リファクタリング本」の Kindle 版。意外にも Java 以外でも応用できる優れもの。

reviewed by Spiegel on 2018.12.7 (powered by Amakuri)

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

結城浩さんによる通称「デザパタ本」のマルチスレッド編。 Kindle 版が出ていた。意外にも Java 以外でも応用できる優れもの。

reviewed by Spiegel on 2018.12.7 (powered by Amakuri)


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