この記事は新たにgoという言語を覚えて、ついでにデザインパターンも復習するという一石二鳥な効果を目指している記事である。

Singletonとは?

シングルトンは、開発者であれば誰もが知っている・聞いたことのあるデザインパターンだと思う。主に複数のインスタンスを生成するのを避けたい時に使われる、理由は様々だが、よくあるのは:

  1. メモリの節約(factoryなど、特に複数のインスタンスが必要としないオブジェクトを再利用する)
  2. 状態の統一(インスタンスはそれぞれ状態を持っているので、一つにすることで状態も統一される)

早速goで実装してみる

今回はよくある、設定を維持するためのConfigというシングルトンをgoで実装してみる。使い方は下記のように想定している:

// config取得
config := GetConfig()

// 文字列型の設定を入れる
config.PutString("host", "127.0.0.1")

// 取得する
val, ok := config.GetString("host")
if ok {
  ... (use val)
}

1.Interfaceを宣言

package mypkg

// Config interface [1]
type Config interface {
    PutString(key string, value string)
    GetString(key string) (string, bool)

    PutInt(key string, value int)
    GetInt(key string) (int, bool)
}

typeキーワードは次のものは何を定義するのかに使う:

// MyInterfaceというinterfaceを定義する
type MyInterface interface {
    ...
}
// MyStructというstructを定義する
type MyStruct struct {
    ...
}

GetString(key string) (string, bool)を見ればわかると思うが、goでは<引数名・変数名> <型>という記述で型を指定する。最後の(string, bool)は戻り値で、goでは複数の戻り値をサポートしている

goのinterfaceは通常のclassのinterfaceと違って、goの中にはclassという概念がなく、すなわちclassでinterfaceをimplementするようなことはない

goはちょっと変わったアプローチでinterfaceを考えている。説明はあとにする。

2.構造体を宣言

// configの構造体を作る [2]
type config struct {
    stringValue map[string]string
    intValue    map[string]int
}

map[string]stringgoのmapで、いわゆる「ハッシュマップ(HashMap)」に近いもの、map[キーの型]値の型のように型を指定できる。ここはキーと値ともstringmapstringValueの型に指定する。

3.構造体のfactoryを定義

// config factory [3]
func newConfig() *config {
    ret := new(config)
    ret.stringValue = make(map[string]string)
    ret.intValue = make(map[string]int)
    return ret
}

goではfunc 関数名(引数) (型) {...}のように関数を定義する

Newほにゃららという関数で構造体を作成することはgoでよく使う手法、いわゆるfactoryという役割。頭文字のnが小文字にしている理由は、goでは小文字で始まる関数はプライベート関数で、逆に大文字で始まるとパブリック関数になるということだ。

Singletonでは特に外部でnewすることはないので、プライベートにしたわけだ。

:=というのは、変数の宣言とアサインと同時に行う時に使う演算子。人によってかなり癖を感じるが、大まかに下記のルールがある

  1. 関数内しか使えない
  2. すでに前に宣言された変数にのアサインには使えない
  3. 2を実現するには=を使う

4.instanceを宣言

// instance [4]
var instance *config

初期化せずに変数を宣言する際は、var‌というキーワードを使う。 *instanceはポインターなので、型に``が付く**。

5.アクセスポイントを作成

// GetConfig はconfigを取得用メソード [5]
func GetConfig() Config {
    if instance == nil {
        instance = newConfig()
    }
    return instance
}

GetConfigの戻り値がConfigになっているのに、instance(configタイプ)を返しているという疑問一瞬あると思うが、ここでgoのinterfaceを少し説明しよう。

まず上記の記述だけだと間違えなくコンパイルエラーが出る。

確かに現状はconfigConfigは全く関係ない二つのもの、ここから関連つけるためには、下記のように実装する:

// methods for Config interface [6]
func (c *config) PutString(key string, value string) {
    c.stringValue[key] = value
}

func (c *config) GetString(key string) (string, bool) {
    val, valid := c.stringValue[key]
    return val, valid
}

func (c *config) PutInt(key string, value int) {
    c.intValue[key] = value
}

func (c *config) GetInt(key string) (int, bool) {
    val, valid := c.intValue[key]
    return val, valid
}

func (c *config) GetString(key string) (string, bool)のように記述することで、(*configタイプの変数).GetString()をコールする際、goは勝手にこの関数に‌(*configタイプの変数)cという引数(同じ*configタイプ)として、GetStringの中に渡し、実行する。

これがgoの面白いところ、継承ではなく、コンポジションというアプローチでinterfaceを考える。このアプローチによって、従来の「継承地獄」にもなりにくく、より柔軟なコードが書けるわけだ。

一通りをまとめると下記のようになる:

// mypkg/config.go

package mypkg

// Config interface [1]
type Config interface {
    PutString(key string, value string)
    GetString(key string) (string, bool)

    PutInt(key string, value int)
    GetInt(key string) (int, bool)
}

// configの構造体を作る [2]
type config struct {
    stringValue map[string]string
    intValue    map[string]int
}

// config factory [3]
func newConfig() *config {
    ret := new(config)
    ret.stringValue = make(map[string]string)
    ret.intValue = make(map[string]int)
    return ret
}

// instance [4]
var instance *config

// GetConfig はconfigを取得用メソード [5]
func GetConfig() Config {
    if instance == nil {
        instance = newConfig()
    }
    return instance
}

// methods for Config interface [6]
func (c *config) PutString(key string, value string) {
    c.stringValue[key] = value
}

func (c *config) GetString(key string) (string, bool) {
    val, valid := c.stringValue[key]
    return val, valid
}

func (c *config) PutInt(key string, value int) {
    c.intValue[key] = value
}

func (c *config) GetInt(key string) (int, bool) {
    val, valid := c.intValue[key]
    return val, valid
}

テストを書く

goでテストを書くのが非常に簡単。パッケージごとに書いてもいいが、ここではconfig.goだけのテストを書くので、同じディレクトリでconfig_test.goを作ってテストする、中身は下記となる:

人によっては、クライアント側にどういう風にを使って欲しいのかを考えながら、先にテストを書いて後で実装する方もいると思います。実際この記事を書く時に先にテストを書きました。

// mypkg/config_test.go

package mypkg

import "testing"

func TestGetConfig(t *testing.T) {
    config := GetConfig()
    if config == nil {
        t.Error("expected GetConfig return an instance of Config")
    }

    testStringValue := "value1"
    config.PutString("key1", testStringValue)
    value1, ok := config.GetString("key1")
    if !ok || value1 != testStringValue {
        t.Errorf("expected value of key1 to be %s", testStringValue)
    }
    _, hasIntValue1 := config.GetInt("key1")
    if hasIntValue1 {
        t.Error("expected no int value for key1")
    }

    testIntValue := 100
    config.PutInt("key2", testIntValue)
    value2, ok := config.GetInt("key2")
    if !ok || value2 != testIntValue {
        t.Errorf("expected value of key2 to be %d", testIntValue)
    }

    _, hasStringValue2 := config.GetString("key2")
    if hasStringValue2 {
        t.Error("expected no string value for key2")
    }
}

go test実行すると下記のようになれば問題なく完成

$ cd /path/to/your/mypkg
$ go test -v -run=GetConfig
=== RUN   TestGetConfig
--- PASS: TestGetConfig (0.00s)
PASS
ok      /path/to/your/mypkg 0.003s

まとめ

goに関してはまだ勉強することいっぱいあるが、こういう感じでデザインパターンを復習しながら新しい言語を覚えるのも面白いと思った。次回まだ別のデザインパターンを紹介しながらgoを覚えていこう!