markdown
はじめに (対象読者・この記事でわかること)
この記事は、Goでバックエンドを構築しており、Firebase Cloud Messaging(FCM)を使った通知配信を検討しているエンジニアを対象にしています。特に「アプリのアップデートを促す通知を、古いバージョンのみに送りたい」「テスト版と本番版で通知を分けたい」といったニーズを持つ方に最適です。
読み進めることで、Firebase Admin SDK for Goを使ってアプリバージョンを条件に絞った通知配信を実装する方法がわかります。トピック購読やデバイスグループを使わず、シンプルにバージョン条件を満たす端末だけに通知を送る実装例を提供します。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - Goの基本的な文法とモジュール管理(go.mod) - Firebaseプロジェクトの作成とサービスアカウントキーの取得 - FCMの基本概念(トークン、メッセージ構成)
Goでバージョン指定通知を実現する設計
FCMには「バージョン指定で通知を絞る」という直接的なAPIは存在しません。しかし、Admin SDKでデバイストークンを管理し、独自にバージョン情報を紐付けることで、事実上のバージョン絞り込みが可能です。
本記事では、Firestoreに保存した「トークン→バージョン」のマッピングを利用して、Go側で配信対象をフィルタリングする方法を解説します。これにより、APNsのテストflight版/App Store版、Androidのdebug/release版といった違いを意識した配信が行えます。
ステップバイステップで実装する
ステップ1:SDKの初期化とトークンレコードの設計
まず、Firebase Admin SDKを初期化し、Firestoreにトークンとバージョンを保存する構造体を定義します。
Gopackage main import ( "context" "log" "os" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" "google.golang.org/api/option" ) type Device struct { Token string `firestore:"token"` Version string `firestore:"version"` Updated time.Time `firestore:"updated"` } func initFirebase() (*firebase.App, error) { ctx := context.Background() sa := option.WithCredentialsFile("path/to/serviceAccountKey.json") app, err := firebase.NewApp(ctx, nil, sa) if err != nil { return nil, err } return app, nil }
Firestoreのコレクションはdevicesとし、ドキュメントIDはトークンの最初16文字+ハッシュにして重複を回避します。
ステップ2:バージョン条件付き通知配信の実装
次に、指定したバージョンのデバイスのみを抽出して、マルチキャスト送信する関数を実装します。
Gofunc SendToVersion(ctx context.Context, app *firebase.App, appVersion string, title, body string) error { client, err := app.Firestore(ctx) if err != nil { return err } defer client.Close() // 1. バージョンに合致するトークンを取得 iter := client.Collection("devices").Where("version", "==", appVersion).Documents(ctx) var tokens []string for { doc, err := iter.Next() if err == iterator.Done { break } if err != nil { log.Printf("iter.Next error: %v", err) continue } var d Device if err := doc.DataTo(&d); err == nil && d.Token != "" { tokens = append(tokens, d.Token) } } if len(tokens) == 0 { return errors.New("no target devices") } // 2. 500件ずつに分割(FCM制限) for i := 0; i < len(tokens); i += 500 { end := i + 500 if end > len(tokens) { end = len(tokens) } batch := tokens[i:end] msg := &messaging.MulticastMessage{ Notification: &messaging.Notification{ Title: title, Body: body, }, Tokens: batch, } fcmClient, err := app.Messaging(ctx) if err != nil { return err } br, err := fcmClient.SendMulticast(ctx, msg) if err != nil { log.Printf("SendMulticast error: %v", err) continue } log.Printf("success: %d, failure: %d", br.SuccessCount, br.FailureCount) } return nil }
ステップ3:クライアント側でのトークン登録
クライアント(iOS/Android)は、トークン取得時にバージョンも一緒に送信する必要があります。以下はAndroid(Kotlin)の例です。
Kotlinval versionName = BuildConfig.VERSION_NAME val token = Firebase.messaging.token.await() val data = hashMapOf( "token" to token, "version" to versionName, "updated" to FieldValue.serverTimestamp() ) Firebase.firestore .collection("devices") .document(token.take(16) + token.hashCode().toString(36)) .set(data, SetOptions.merge())
ハマった点と解決策
トークンが無効になるとFirestoreのゴミが増える
FCMのトークンは、再インストールや長期間放置で無効になります。無効トークンをそのままにしておくと、送信エラーが増えます。
解決策:SendMulticastのレスポンスをチェックし、エラーコードがregistration-token-not-registeredのトークンをFirestoreから削除するクリーナーを定期実行します。
Gofor idx, resp := range br.Responses { if resp.Error != nil && messaging.IsRegistrationTokenNotRegistered(resp.Error) { // 該当トークンをFirestoreから削除 token := batch[idx] client.Collection("devices").Doc(token.take(16)+...).Delete(ctx) } }
バージョン表記の揺れ
iOSだと1.2.3、Androidだと1.2.3.4、さらにビルド番号まで含めると1.2.3(45)など表記がまちまちです。
解決策:保存前に正規化ルールを決めておく(例:メジャー.マイナー.パッチまで統一)。クライアント側でsplit(".")して最初の3要素だけを使う。
大量配信時のFirestore読み込みコスト
トークン数が10万件を超えると、毎回Where("version","==",...)で読みに行くと読み取りコストが高額になります。
解決策:バージョンごとにサブコレクションを分けるか、BigQueryにエクスポートしてからバッチで取得するなど、Firestoreの負荷を下げる工夫が必要です。
まとめ
本記事では、Go + Firebase Admin SDKで「アプリバージョンを条件に通知を絞る」方法を解説しました。
- Firestoreにトークンとバージョンを保存し、Go側でフィルタリング
SendMulticastで500件ずつ配信し、エラーハンドリングで無効トークンを除去- クライアント側でトークン登録時にバージョンも送信し、正規化して保存
この実装により、特定バージョンのみにアップデート促進通知を送る、テスト版と本番版で通知を分ける、といった要件をシンプルに満たせます。今後は、バージョン比較を「以上」「以下」に拡張したり、セグメント(国、言語、デバイス種別)と組み合わせた高度な絞り込みも検討していきます。
参考資料
- Firebase Admin SDK for Go 公式ドキュメント
- FCM マルチキャスト送信 リファレンス
- Go Firestore クライアント ライブラリ
- IsRegistrationTokenNotRegistered エラーハンドリング
