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{}
末尾の代入がコメントアウトされていますが、ここをアンコメントして imitateDeeeet
が Deeeeter
を満たしているか確認してみましょう。すると以下のように怒られます。残念でした。
% 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上のサンプルコードも修正しました。