読者です 読者をやめる 読者になる 読者になる

GO x デザインパターン ~ Singleton編

デザインパターンから学ぶGO

最近、業務でGOを触る機会が出てきました。学習の為にGOを使った教材を探していましたが、ここは1つGOを使ったデザインパターンの実装を学んでいきたいと思います。

まず初めに。

GOにはクラスや継承といったものはありません(というのは聞いてました)。 実際に触ってみると、クラスや継承といったものはありませんが、それに近い事は出来るようです。 type xxxxx struct で構造体を定義し、その構造体に対してメソッドを追加していく形になります。

package main

import (
    "fmt"
)

// Sampleという名前の構造体を定義し、
// 構造体の中にはmessageというフィールドを文字列型で定義しています
type Sample struct {
    message string
}

// Sample構造体に対して、printlnというメソッドを定義
func (s *Sample) println(message string) {
    fmt.Println(message)
}

// Sample.println("Hello") => Hello

ここで構造体の名前についてですが、最初の文字を大文字にするとエクスポートされ外部ライブラリからも利用可能になり、 小文字で始めるとエクスポートされない為、他パッケージからは参照できなくなるようです。 また、構造体の名前だけではなく、フィールド名については大文字で始めるとpublicメソッド的な扱いになり、小文字で始めるとprivate的な扱いになるようです。

それでは早速

Singletonパターン

特徴
  • 生成されるオブジェクトの数を制限出来る
  • 使い回せるオブジェクトは使い回すので無駄なリソースを浪費しない
  • インスタンスを生成する為のメソッドを持つ

シングルトンの場合、インスタンスの生成は公開せずインスタンス生成メソッドを公開したいので、 構造体の名前は小文字で始め、インスタンス生成関数を大文字で始めれば実現できそうです。

よく見かける記述
package singleton

// 構造体の名前。小文字で始める事で外部へのエクスポートを行わない
type singleton struct {
}

// インスタンス保持変数。小文字で始める事で外部へのエクスポートを行わない
var instance *singleton

// singletonという構造体を返すGetInstance関数を定義
func GetInstance() *singleton {
    if instance == nil {
        instance = new(singleton)
    }   
    return instance
}

一見問題なさそうですが、この方法だと複数のスレッドから呼ばれた際に複数インスタンスが生成されてしまう可能性があるようです。 実際に試してみます。

singleton/singleton.go

package singleton

import (
    "fmt"
    "time"
)

// 構造体の名前。小文字で始める事で外部へのエクスポートを行わない
type singleton struct {
}

// インスタンス保持変数。小文字で始める事で外部へのエクスポートを行わない
var instance *singleton

// singletonという構造体を返すGetInstance関数を定義
func GetInstance() *singleton {
    if instance == nil {
        // わざと遅延させて、次の処理でコールされるのを待つ
        time.Sleep(1 * time.Second)
        instance = new(singleton)
        fmt.Println("Create Singleton")
    } else {
        fmt.Println("Not Create Singleton")
    }   
    return instance
}

main.go

package main

import (
    "./singleton"
    "time"
)

func main() {
    ch := make(chan interface{})
    go run(ch)
    go run(ch)
    go run(ch)
    <-ch

    // goroutineはmainが終わると終了となるので敢えて遅延
    time.Sleep(5 * time.Second)
}

func run(ch chan interface{}) {
    ch <- singleton.GetInstance()
}

実行

[~]$ go run main.go
Create Singleton
Create Singleton
Create Singleton
[~]$

確かに!

複数スレッドからの参照時に異なるインスタンスでもOKな場合は問題ありませんが、 同一インスタンスのリソースを使い回したい場合はこれだと問題が出てきてしまいます。

複数スレッドで同一のインスタンスを参照

複数スレッドからでも同一のインスタンスを参照する場合は、 プロパティの定義を行う際にインスタンスを同時に作る事で、多重インスタンスの生成を防げそうです。

var instance *singleton = new(singleton)

singleton/singleton.go

package singleton

import (
    "fmt"
    "time"
)

type singleton struct {
}

var instance *singleton = new(singleton) // ← インスタンス変数を設定

func GetInstance() *singleton {
    if instance == nil {
        time.Sleep(1 * time.Second)
        instance = new(singleton)
        fmt.Println("Create Singleton")
    } else {
        fmt.Println("Not Create Singleton")
    }   
    return instance
}
実行
[~]$ go run main.go
Not Create Singleton
Not Create Singleton
Not Create Singleton
[~]$

プロパティの定義時にインスタンス生成したものを登録しているので、 GetInstance()では登録されなくなり、期待通りの動作となりました。

まとめると

GetInstance関数の中での生成が不要となったので、最終的にはこんな感じでもよさそうです。

singleton/singleton.go

package singleton

type singleton struct {
}

var instance *singleton = new(singleton)

func GetInstance() *singleton {
    return instance
}

おめぇ、随分すっきりしちまったな、と、第三形態のフリーザのようです。

フィールド定義時 Or GetInstanceがコールされた時、どちらのタイミングでインスタンスを生成するかは、別スレッドが走った時に同一インスタンスの参照を許可するかどうかで判断すればいいんぢゃないかな