この記事はQiitaで公開されていました
ユーザ操作などで、同じAPIを同時にリクエストされたけれど、例えばGET
メソッドの場合は結果もほとんど同じになるので、リクエストを1回にまとめてしまいたい場合は少なくないと思います。
または、期限付きの認証トークンが必要なAPIを並行して実行しているケースで、トークンの期限が切れた直後で同時に2つのリクエストが行われても、トークンの更新は1回だけに制限したい場合もあるかもしれません。
そういった、「複数の呼び出しが同時に発生しても、結果は同じなので同時に1つだけ行って結果を共有する」という処理に、x/sync/singleflightが使えます。
実装例
重複の排除を行いたい部分を、singleflight.Group
のDo(name, fn)
でラップします。以下の例では、1ミリ秒ごとにcallAPI("work")
が実行されますが、callAPI("work")
は3ミリ秒の時間がかかるので、続く2回の呼び出しが起こった時にはまだ前の処理が終わっていません。そうするとsingleflight.Group
は1つ目の呼び出しが終わるまで待って、1回目の結果を使って、2回目と3回目の呼び出しが行われたかのように振る舞います。しかし実際にAPIが呼ばれるのは1回目だけです。
package main import ( "log" "sync" "time" "golang.org/x/sync/singleflight" ) var group singleflight.Group func callAPI(name string) { v, err, shared := group.Do(name, func() (interface{}, error) { // 具体的に実行したい処理を書く <-time.After(3 * time.Millisecond) return time.Now(), nil }) if err != nil { log.Fatal(err) } log.Println("結果:", v, ", 重複が発生したか:", shared) } func main() { log.SetFlags(0) var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() callAPI("work") }() <-time.After(time.Millisecond) } wg.Wait() }
この実行結果は、おおむね以下のようになります。時刻が完全に一致している点から、結果が再利用されているのがわかると思います。
結果: 2017-06-12 23:53:11.936580392 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.936580392 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.936580392 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.940406256 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.940406256 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.940406256 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.94409058 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.94409058 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.94409058 +0900 JST , 重複が発生したか: true 結果: 2017-06-12 23:53:11.94766342 +0900 JST , 重複が発生したか: false
説明
singleflight.Group
のDo(name, fn)
メソッドは、name
の値が同じ呼び出しが実行中であれば、2回目以降の呼び出しを止めておいて、実行中だった最初のfn
の結果をそのまま共有します。そのため、重複した呼び出しは全て同じ結果となります。(最初がたまたまエラーになったら全てエラーです)
結果が、name
の一致したDo(name, fn)
に共有された後は、name
は未実行の状態に戻るので、次の呼び出しのfn
は待機されずに実行します。また、name
が実行中であっても、異なるname
が使われた場合はそのままfn
が実行されます。
上記の例ではDo(name, fn)
だけ使いましたが、戻り値ではなくチャネル経由で結果を返すDoChan(name, fn)
も用意されています。