Goでcallback patternを考える

プログラミング言語ではおなじみのcallbackについて、Goでどのように実装すると良いか考えた。

Goではcallbackよりもchannelが良い?

まず、非同期な処理からの結果を通知するしくみとして、Goだとchannelが使える。

しかし、chnanelは双方向だったりcloseできたりと高機能なので、単純に完了したことを通知するだけであれば、callbackでいい気がする。

Stackoverflowにも次のような回答があった。

https://stackoverflow.com/questions/25203988/go-and-callbacks

Channels aren't preferred over callbacks, the standard library uses this style of callbacks all over the place, it highly depends on what you need to do.

Using callbacks is very acceptable, specially for that kind of server pattern, net/* uses it all over the place.

netパッケージでコールバックは使われているとあるが、http.Handlerのことだろうか?

funcで渡すか、interfaceを実装するか

Goでcallbackを定義する方法として、次の2つが考えられる。

  • func typeを直接渡すパターン
  • funcを実装したinterfaceを作って満たしたtypeを渡す
// func pattern
type OnProcess func(Result)

func Process(cb OnProcess) {
    ...
}


// interface pattern
type Handler interface {
    OnProcess(Result)
}

func Process(h Handler) {
    ...
}

func patternの方が良いこと

  • 記述量が少ない

interface patternの方が良いこと

  • 受け取るイベントの種類が増えたときに引数の数を変えなくて良い
    • あとからイベントが増えても、interfaceを実装したtypeの実装を書き足すだけでよい
  • func typeにinterfaceを実装することもできる
// このようにすれば、func typeもHandler interfaceにできる
type OnProcess func(Result)

func (f OnProcess) OnProcess(r Result) {
    f(r)
}

というわけで、interface patternにしておくと実質どちらをも内包してるので、迷ったらinterface patternで良さそうと感じた。

複数の通知を受け取る可能性がある場合

ただ、複数の通知を受け取りたい場合だとすこし面倒になってくる。interfaceに複数の関数をつけることになるが、片方だけの通知がほしいといった場合に変な書き方になる。

さらに、満たすべきメソッドが2つ以上あるので、func typeにそのままinterfaceを実装するというやり方が通用しなくなる。

type Handler {
    OnStart()
    OnEnd()
}

// 終了時のみ通知がほしい(開始時はいらない)
type testHandler struct {
    onStart func()
    onEnd   func()
}

func (th *testHandler) OnStart() {
    th.onStart()
}

func (th *testHandler) OnEnd() {
    th.onEnd()
}

h := testHandler{
    // 通知が不要なら、空の関数を渡さなければいけない・・;
    onStart: func() { /* empty body */ },
    onEnd: func() {
        log.Printf("on end!")
    },
}

解決策としては、onStartとonEndでそもそも別のinterfaceにして、きっちり分離するなど??うーん、それでいいのだろうか。

Kubernetesのコードで実例をみた

そう思っていろいろ探していたら、Kubernetesのソースコードにまさにそのパターンに近いような実装をみつけた。

https://github.com/kubernetes/client-go/blob/a432bd9ba7da427ae0a38a6889d72136bce4c4ea/tools/cache/controller.go#L200

// ResourceEventHandler can handle notifications for events that happen to a
// resource. The events are informational only, so you can't return an
// error.
//  * OnAdd is called when an object is added.
//  * OnUpdate is called when an object is modified. Note that oldObj is the
//      last known state of the object-- it is possible that several changes
//      were combined together, so you can't use this to see every single
//      change. OnUpdate is also called when a re-list happens, and it will
//      get called even if nothing changed. This is useful for periodically
//      evaluating or syncing something.
//  * OnDelete will get the final state of the item if it is known, otherwise
//      it will get an object of type DeletedFinalStateUnknown. This can
//      happen if the watch is closed and misses the delete event and we don't
//      notice the deletion until the subsequent re-list.
type ResourceEventHandler interface {
    OnAdd(obj interface{})
    OnUpdate(oldObj, newObj interface{})
    OnDelete(obj interface{})
}

// ResourceEventHandlerFuncs is an adaptor to let you easily specify as many or
// as few of the notification functions as you want while still implementing
// ResourceEventHandler.
type ResourceEventHandlerFuncs struct {
    AddFunc    func(obj interface{})
    UpdateFunc func(oldObj, newObj interface{})
    DeleteFunc func(obj interface{})
}

// OnAdd calls AddFunc if it's not nil.
func (r ResourceEventHandlerFuncs) OnAdd(obj interface{}) {
    if r.AddFunc != nil {
        r.AddFunc(obj)
    }
}

// OnUpdate calls UpdateFunc if it's not nil.
func (r ResourceEventHandlerFuncs) OnUpdate(oldObj, newObj interface{}) {
    if r.UpdateFunc != nil {
        r.UpdateFunc(oldObj, newObj)
    }
}

// OnDelete calls DeleteFunc if it's not nil.
func (r ResourceEventHandlerFuncs) OnDelete(obj interface{}) {
    if r.DeleteFunc != nil {
        r.DeleteFunc(obj)
    }
}

なるほど!

  • 複数のコールバックをまとめた interface を作り
  • それを func として指定できる struct も作り
  • interface の実装で、func が nil であれば呼び出さない

こうすればいいのか〜〜