おそらくはそれさえも平凡な日々

GoでSingletonぽいことを実現する、とある方法

ちなみに今回のコードはそれほど実用性はありません。ここまで頑張って、シングルトンぽいことを実現する必要性は感じられないからです。サンプルコードはこちら。

https://www.github.com/Songmu/go-sandbox/

Goでシングルトンを実現する方法として以下の様なコードが良く見られます。

package singleton

import "sync"

type singleton struct{
}

var (
    instance *singleton
    once     sync.Once
)

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

このコードのグッドポイントとしては、 sync.Once を使っていること。以下のように素朴に nil チェックをする形だと、マルチスレッドで競合が発生するのでアウトです。

if instance == nil {
    instance = &singleton{}
}

とは言え、初めて評価されるときに代入されることにこだわりが無いのであれば、わざわざ sync.Once を使わず、トップレベルで初期化時に代入してしまって良いとは思います。

var instance = &singleton{}

さて、これで、シングルトンは実現できるのですが、個人的に気になっていることがありました。それは、 golint で怒られるということです。

% golint .
singleton/singleton.go:14:20: exported func GetInstance returns unexported type *singleton.singleton, which can be annoying to use

つまり「GetInstance というパブリックな関数が、 *singleton.singleton というプライベートな型を返すのは紛らわしい」ということです。実際、 *singleton.singleton にパブリックなメソッドが生えていれば、それを呼び出すことはできるのですが、それは、godocなどで抽出されないので困りものです。

dummyメソッドを持ったinterfaceを使うという解法

それをdummyメソッドを持ったinterfaceを使う手でこれを解決してみました。dummyメソッドを持ったinterfaceと言うのは、go/ast パッケージなどで見られますが、パブリックなinterfaceの中に、プライベートなメソッドを埋め込むことで、そのinterface自体はパブリックですが、外のパッケージではそのinterfaceを満たす変数を作れなくするというものです。

go/ast パッケージでの様子などは以下の @haya14busa さんの記事に詳しいです。

Sum/Union/Variant Type in Go and Static Check Tool of switch-case handling

今回のシングルトンの場合、パッケージ自体はこのようになります。

package singleton

import (
    "fmt"
    "strings"
    "sync"
    "sync/atomic"
)

// Deeeeter implements Deeeet() method
type Deeeeter interface {
    Deeeet()
    getAge() // as a dmmuy method
}

type deeeet struct {
    age int64 // accessed atomically
}

var (
    d Deeeeter
    o sync.Once
)

// GetDeeeter gets the Deeeeter
func GetDeeeter() Deeeeter {
    o.Do(func() {
        d = &deeeet{}
    })
    return d
}

// Deeeet desu...
func (de *deeeet) Deeeet() {
    age := int(atomic.AddInt64(&de.age, 1))
    fmt.Printf("d%stです…\n", strings.Repeat("e", age))
}

func (de *deeeet) getAge() {
    fmt.Println(de.age)
}

これを以下のようなコードで実行してみましょう。

package main

import (
    "fmt"

    "./singleton"
)

func main() {
    deeeet := singleton.GetDeeeter()

    deeeet.Deeeet()
    deeeet.Deeeet()
    singleton.GetDeeeter().Deeeet()
    deeeet.Deeeet()
}

これを実行してみると以下のようになります。

% go run singleton.go
detです…
deetです…
deeetです…
deeeetです…

ちゃんと GetDeeeeter が同じ変数を返しており、シングルトンの様な挙動が実現されています。Deeeeter 自体はパブリックなinterfaceですが、それを満たす変数をパッケージ外から作ることができないため、この deeeet は唯一無二の存在となります。

https://godoc.org/github.com/Songmu/go-sandbox/singleton にもちゃんとドキュメントが生成されており、ニッコリ。

本当に、パッケージ外から Deeeeter を作ることはできないのか?

それを試すために、 https://github.com/Songmu/go-sandbox/blob/master/singleton.go の末尾に以下のようなコードがあります。Deeeet メソッドと、 getAge メソッドを実装してそれっぽくなっています。

type imitateDeeeet struct {
}

func (ide *imitateDeeeet) Deeeet() {
    fmt.Println("deeeet(偽)です…")
}

func (ide *imitateDeeeet) getAge() {
    fmt.Println("17歳です♥")
}

// var _ singleton.Deeeeter = &imitateDeeeet{}

末尾の代入がコメントアウトされていますが、ここをアンコメントして imitateDeeeetDeeeeter を満たしているか確認してみましょう。すると以下のように怒られます。残念でした。

% go run singleton.go
# command-line-arguments
./singleton.go:29:5: cannot use imitateDeeeet literal (type *imitateDeeeet) as type singleton.Deeeeter in assignment:
        *imitateDeeeet does not implement singleton.Deeeeter (missing singleton.getAge method)
                have getAge()
                want singleton.getAge()

この _ に代入するというテクは、あるtypeが狙ったinterfaceを満たすかどうかを担保するために実コードでも使われるパターンなので、覚えておくと良いでしょう。

追記: @shogo82148 さんに、 de.age の保護が甘いと指摘を受けたので、エントリ内のサンプルコードを直しました。合わせて、GitHub上のサンプルコードも修正しました。

created at
last modified at

2019-06-01T18:57:39+0900

comments powered by Disqus