Go 言語で Debounce / Throttle を実装してみた
最近この概念を知り、意外とシンプルに実装できることが分かったため記録として。
Debounce / Throttle とは
※生成AIに聞いたほうが分かりやすい説明をしてくれそう
どちらも、複数回呼び出された処理に対して、一定の間隔を開けながら実行する仕組みである。
Debounce は複数回の呼び出しに対して「最後に呼び出されてから、任意の時間経過後に最後の呼び出しを実行する」もの。 時間経過前に別の呼び出しが行われた場合は、実行せずに捨てられる。
Throttle は複数回の呼び出しに対して「任意の時間間隔を必ず空けて実行する」もの。 間隔を空けずに呼び出しが行われた場合は、実行せずに捨てられる。
実装
今回は、呼び出した処理が fmt.Printf を実行するだけなので、かなりシンプルな実装となっている。
Debounce
time.AfterFunc と timer.Stop を用いることで、簡単に実装できた。 timer.Stop ではなく timer.Reset を使ったほうが良かった気はする。
package main import ( "fmt" "math/rand/v2" "sync" "time" ) type Task func() type DebounceFunc func(f Task) func Debounce(duration time.Duration) DebounceFunc { var mu sync.Mutex var timer *time.Timer return func(f Task) { mu.Lock() defer mu.Unlock() if timer != nil { // POINT: 追加で呼び出しされた場合、既存の実行予約を取り消す timer.Stop() } // POINT: 指定時間経過後に実行したい処理を予約しておく timer = time.AfterFunc(duration, f) } } func newTask(i int) Task { return func() { fmt.Printf(" [%d回目] 実行\n", i) } } func main() { debounceDuration := 500 * time.Millisecond debounced := Debounce(debounceDuration) fmt.Printf("デバウンス時間は %v です\n", debounceDuration) for i := range 10 { ms := rand.IntN(501) + 100 sleepTime := time.Duration(ms) * time.Millisecond fmt.Printf("[%d回目] 次の呼び出しまで %v 待機\n", i, sleepTime) debounced(newTask(i)) time.Sleep(sleepTime) } fmt.Println("最後の呼び出しを確認するために1秒待機") time.Sleep(1 * time.Second) }
実行結果: 500 ms 以上の間隔を空けて呼び出された処理のみが実行されている
$ go run ./debounce デバウンス時間は 500ms です [0回目] 次の呼び出しまで 591ms 待機 [0回目] 実行 [1回目] 次の呼び出しまで 491ms 待機 [2回目] 次の呼び出しまで 203ms 待機 [3回目] 次の呼び出しまで 547ms 待機 [3回目] 実行 [4回目] 次の呼び出しまで 471ms 待機 [5回目] 次の呼び出しまで 462ms 待機 [6回目] 次の呼び出しまで 197ms 待機 [7回目] 次の呼び出しまで 238ms 待機 [8回目] 次の呼び出しまで 565ms 待機 [8回目] 実行 [9回目] 次の呼び出しまで 312ms 待機 最後の呼び出しを確認するために1秒待機 [9回目] 実行
Throttle
こちらは更にシンプルで、最後に実行した時刻を記録しておき、指定以上の時間が経過していれば実行する。
package main import ( "fmt" "math/rand/v2" "sync" "time" ) type Task func() type ThrottleFunc func(f Task) func Throttle(duration time.Duration) ThrottleFunc { var mu sync.Mutex var lastRun time.Time return func(f Task) { mu.Lock() defer mu.Unlock() // POINT: 最終実行からの経過時間を確認 if time.Since(lastRun) >= duration { f() lastRun = time.Now() } } } func newTask(i int) Task { return func() { fmt.Printf(" [%d回目] 実行\n", i) } } func main() { throttleDuration := 500 * time.Millisecond throttle := Throttle(throttleDuration) fmt.Printf("スロットル時間は %v です\n", throttleDuration) totalSleepTime := time.Duration(0) for i := range 10 { ms := rand.IntN(251) + 100 sleepTime := time.Duration(ms) * time.Millisecond fmt.Printf("[%d回目] %v 経過\n", i, totalSleepTime) throttle(newTask(i)) totalSleepTime += sleepTime time.Sleep(sleepTime) } fmt.Println("最後の呼び出しを確認するために0.5秒待機") time.Sleep(throttleDuration) throttle(func() { fmt.Println(" 実行") }) }
実行結果: 前回の実行から最低 500 ms 以上の間隔を空けて実行されている
$ go run ./throttle スロットル時間は 500ms です [0回目] 0s 経過 [0回目] 実行 [1回目] 114ms 経過 [2回目] 394ms 経過 [3回目] 503ms 経過 [3回目] 実行 [4回目] 699ms 経過 [5回目] 886ms 経過 [6回目] 1.118s 経過 [6回目] 実行 [7回目] 1.223s 経過 [8回目] 1.44s 経過 [9回目] 1.783s 経過 [9回目] 実行 最後の呼び出しを確認するために0.5秒待機 実行
まとめ
初めてこの概念を知ったときは、実装がとても難しいのではないかと思ったが、最終的にシンプルなものになった。 今回は戻り値もないような単純な処理だったので尚更簡単なものに仕上がった感はあるが、もし今後実装することになったとしても臆することは無くなりそうなのでよかった。 また、改めてGoの標準パッケージは機能が豊富だと感じた。
Debounce/Throttle の概念を教えてくれた同僚に感謝!!
Cloud Scheduler での定期実行ごとに次回のリクエスト内容を変更する
Google Cloud のリソースを整理していたところ、 2020年12月ごろに遊びで構築したものを見つけたので、ここに供養しておきます。
概要
定期実行の処理内で「Cloud Scheduler の設定を更新する処理」を実装してあげればよいです。
実装サンプル
今回は Cloud Scheduler -> Cloud Pub/Sub -> Cloud Run functions (旧 Cloud Functions)という構成です。 重要なのは Cloud Run functions 内での実装内容なので、 Cloud Scheduler のターゲットタイプが Cloud Pub/Sub かどうかは関係なく、 HTTP でも問題ないです。 また、Cloud Scheduler や Cloud Pub/Sub の設定値に関しても重要ではないため、今回は詳しい設定方法やコマンドについては割愛します。
「定期実行ごとにFizzBuzzを実行する関数」を例とし、サンプルコードを以下に示します。
なお、ソースコード中の NOTE コメントに記載したことが全てですが、ざっくり以下の処理を実装するだけです。
package sample import ( "context" "encoding/json" "log" scheduler "cloud.google.com/go/scheduler/apiv1" schedulerpb "google.golang.org/genproto/googleapis/cloud/scheduler/v1" ) type PubSubMessage struct { Data []byte `json:"data"` } type Payload struct { Number int `json:"number"` } func SampleFunc(ctx context.Context, m PubSubMessage) error { var payload Payload if err := json.Unmarshal(m.Data, &payload); err != nil { log.Printf("json unmarshal error: %v", err) return err } // NOTE: Scheduler の payload に設定された値を使う処理。 e.g. FizzBuzzの出力 printFizzBuzz(payload.Number) // NOTE: 次回の Scheduler 呼び出し用に payload の値をインクリメント nextPayload, err := json.Marshal(&Payload{Number: payload.Number + 1}) if err != nil { log.Printf("json marshal error: %v", err) return err } client, err := scheduler.NewCloudSchedulerClient(ctx) if err != nil { return err } // NOTE: 呼び出し元の Scheduler Job を取得 job, err := client.GetJob(ctx, &schedulerpb.GetJobRequest{Name: "projects/{{YOUR_PROJECT_ID}}/locations/asia-northeast1/jobs/{{YOUR_SCHEDULER_JOB_NAME}}"}) if err != nil { log.Printf("get scheduler job error: %v", err) return nil } // NOTE: nextPayload を Scheduler Job の PubSub Target に設定 job.Target = &schedulerpb.Job_PubsubTarget{ PubsubTarget: &schedulerpb.PubsubTarget{ TopicName: "projects/{{YOUR_PROJECT_ID}}/topics/{{YOUR_PUBSUB_TOPIC_NAME}}", Data: nextPayload, Attributes: nil, }, } // NOTE: 呼び出し元の Scheduler Job の更新 if _, err := client.UpdateJob(ctx, &schedulerpb.UpdateJobRequest{Job: job}); err != nil { log.Printf("update scheduler job error: %v", err) return err } return nil }
おわりに
実際に使う機会が来るかどうかは分かりませんが、約5年前のリソースを見つけて懐かしい気持ちになったので記事を書きました。 誰かの役に立てば幸いです。
iPhone 17 Pro Max 購入
iPhone 17 Pro Max を購入した。
- 容量: 512 GB
- カラー: シルバー
予約開始の 2025-09-12 21:00 から約1時間ほど遅れて予約したため、到着予定日は 2025-10-08 -- 2025-10-16 となっていたのだが、2025-10-04 に発送された。 また、iPhone 17 Pro Max 用の MagSafe 対応クリアケースも購入した。(こちらは発売日に到着していた)
その他、購入したもの
| 商品 | URL |
|---|---|
| ガラスフィルム | https://www.yodobashi.com/product/100000001009311511/ |
| カメラカバー | https://www.yodobashi.com/product/100000001009311519/ |
| スマホリング | https://www.ankerjapan.com/products/a25a0 |
今まで使っていた iPhone 13 は 2021-12-25 に購入していたので、今回も4年弱使っていたことになる。
2025-10-18 更新: カメラカバー及びケースを変更
カメラカバーはクリアケースを外す際に干渉し、ガラス片を撒き散らしながら粉砕された…。 仕方がないので、以下のカメラレンズ保護フィルムを購入したものの、レンズを拭くと若干ずれるのが気になる。
https://www.yodobashi.com/product/100000001009311698/
また、ケースに関しては、クリアケースの場合は下部の端子周りの保護がいまいちで、落とした際に傷か汚れがついてしまった。 そのためテックウーブンケースに変更した。これはこれで吸水してしまうためイマイチだが…。
レザーケースが復活して欲しいところである。
AG03MK2用のスタンドをダイソー商品で作成した記録
動機
- デスクが狭く、AG03を普通に置くと若干邪魔だったため、縦置きっぽくしたかった。
- 仕事用PCと私用PCで頻繁にUSBの差し替えを行うため、端子を視認しやすくするために縦置きっぽくしたかった。
必要なもの
- アクリルスタンド(幅3.8cm) x 2個
- AG03本体を立てかけるために使います。

- 漫画を飾れる棚 x 1個
- アクリルスタンドを固定するための土台として使います。
- 手頃なサイズ感のアクリル板であれば何でもよいです。

- クッションゴム 透明 x 1個
- 「漫画を飾れる棚」の底に貼り付け、ズレを防止します。

- アロンアルファ
雑な作り方
- 「漫画を飾れる棚」の出っ張っている方を上向きにして、「アクリルスタンド(幅3.8cm)」を「アロンアルファ」で貼り付けます。
- 「漫画を飾れる棚」の底に「クッションゴム 透明」を貼り付けます。
完成写真はそのうち机が片付いたら貼りたい。
所感
実際にAG03を乗せると、重みでゴム足がいい感じに滑り止めとなり、指一本で電源スイッチを押しても全くズレない。
富山旅行
はじめに
今まで旅行と呼べそうなものは修学旅行、卒業旅行、開発合宿くらいしかなく、自分で新幹線や宿の予約をしたことがなかったため、やってみようと思ったのがきっかけ。 有給休暇もなかなか取れずに溜まってきていたし。 目的地を富山にしたのは、富山出身の友人がいることや、同僚が富山の寿司を推していたため、前々から気にはなっていたし、白えびも旬なのでちょうど良さそうだったから。
日程
新幹線や宿を自力で予約して目的地までたどり着き、寿司が食べられればいいなくらいに考えていたので、一泊二日とした。
宿
畳敷きの和室だった。賃貸を探していて和室があるとムカつくが、こういう場所での畳は嬉しい。冷蔵庫にはウェルカムゼリー?が入っていた。 室内に檜風呂があっていい香りがしたが、試しに湯を張ってみるとあまり使われていないのか浮遊物が多かった。まぁ普通は見向きもせずに大浴場に行くだろうし仕方ないとは思った。 備え付けのテレビではYouTubeやNetflixを見れないタイプだったのでちょっとがっかりした。iPadを持って行っておいてよかった。
大浴場にはサウナが併設されており、久々に良い汗をかけた。また、大浴場でもサウナでもヒノキの良い香りがして心地よく過ごせた。 最近あまり趣味の銭湯に行けていなかったため、本当に良い時間だった。
食事は朝食の有無を予約時に選ぶことができ、いただく場合は追加料金が発生する仕組みだった。 イメージ写真にまんまと釣られて申し込んでしまったが、朝が弱い自分にとってはプレッシャーになってしまい、寝られなくなったので申し込まなければよかったと後悔した。たら汁は美味しかった。
ちょくちょく文句を挟んでしまってはいるが、大満足だった。また富山に来ることがあったらまた予約してもいいかなと思えた。 ただ、夏休み前になってしまったせいで少し料金が高くなってしまったところは事前に考慮できたはずで失敗だった。(ちなみに翌週はガッツリ夏休み開始で更に高くなるため、そういう意味ではギリギリセーフだったかもしれない)
食事
1日目夜
寿司栄 総曲輪店で富山の味にぎりを注文。苦手なものが無いか聞いてくれる優しさがある。(自分は何でも食べるけど) ラストに蟹と味噌の軍艦、白エビの軍艦が出てくるのだが、「蟹味噌で白エビの味が正しく感じられなくなってしまうから、先に白エビを食べてください」とのことだった。説明を聞いた上でなぜか間違えて蟹軍艦から食べてしまい、もったいないことをした…。にぎりセットに加えて他に食べたいネタや白エビを追加でいただき、税込み7,500円くらいだった。寿司はもちろん、ガリもアガリも美味しかった。満足。 どうでもいいが、COVID-19の影響だと思うが、手洗い用の水は流れていなかった。
2日目朝
宿のバイキング。食べすぎた。
2日目昼
富山ブラックラーメンの元祖、西町大喜 西町本店にて中華そば並のライスセットを食べた。 ライスセットを注文すべしと事前に教えてもらっていたので従ったが、本当にライスを頼んでおいてよかった。 驚くほどしょっぱいため、ラーメンをおかずにご飯を食べる感覚だった。ライスが救いだった。 具体的には麺だけなら単体でもいけるが、チャーシューやネギ、メンマなどの具材(とくにメンマが最高にしょっぱい)を食べるためにライスが必要という感じ。 寝不足だったこともあり、少しきつかったが完食できた。いい経験になった。次は体調が良いときに食べたい。 テーブルにレンゲが置いてないところから、誰もスープを飲まないことが察せられて個人的に少しおもしろかった。
その他
- 市内電車という路面電車に乗った
- 富山城を見た(意外と小さかった)
- 元「世界一美しいスターバックス」に行った
- お土産に満寿泉という日本酒を買った。他には白えびせんべい、白えびかき餅、ホタルイカの沖漬け、干しホタルイカ、富也萬、甘金丹など。
おわりに
最高だった。また行きたい。何なら住んでもいい…と思いかけたが冬は雪かきが大変そうなので、旅行で行くくらいがちょうどよさそう。本当にまた富山に行きたいな。もつ煮込みうどんも食べたいし。
振り返ると今回の失敗点は以下の3点くらいかな?
- 夏休み直前で少し料金が高かったこと
- 朝食バイキングを予約したこと
- 富山から東京に着く時間が退勤ラッシュと丸かぶりしてしまったこと(運良く座れたけど)
それはそれとして、次は飛行機の予約とかをやってみたいので来年辺りにまたどこかに行こうかな。
Firestore EmulatorとGoで開発する際に指定するプロジェクトIDはdummy-emulator-firestore-projectがいいかも
はじめに
本記事で指している Firestore Emulator とは gcloud components で提供されているエミュレータではなく、Firebase の Local Emulator Suite から提供されている方です。
また、実行環境の環境変数に FIRESTORE_EMULATOR_HOST が設定されている前提で話を進めます。
何をすればいいのか
- Firestore Emulator 起動時に
--project=dummy-emulator-firestore-projectフラグを追加する。 - Firestore のクライアントライブラリで NewClient を行う際に
DetectProjectIDを利用する。
Go 側の解説
Firestore のクライアントライブラリで NewClient を行う際には引数で ProjectID の指定が必要ですが、私は DetectProjectID という定数を利用しています。(余談ですが Datastore のクライアントライブラリでも同様の定数が利用できます。)
DetectProjectID を指定した場合、実行環境の認証情報からプロジェクトIDを取得してくれるのですが、ローカル環境で特に認証情報を設定していなかった場合、プロジェクトIDにはdummy-emulator-firestore-project という文字列が設定されます。なので Firestore Emulator を起動する際のプロジェクトIDも dummy-emulator-firestore-project に合わせてあげるとシームレスに接続できるようになります。
おわりに
Local Emulator Suite の UI 機能が便利そうだったので gcloud components 版のエミュレータから移行しましたが、 移行前はアプリ側の環境変数に FIRESTORE_EMULATOR_HOST を設定するだけで接続できており、Local Emulator Suite 移行後にアプリから接続出来なくなったため意外なハマりポイントかな?と思って記事を書いてみました。


