Pub/SubトリガーのCloud FunctionsをGoで作るときはpubsub.Messageを使おう

# 背景

Cloud FunctionsでGoが使えるようになってからだいぶ経ったので自分も使ってみようかと思ったのですが、Pub/Subのデータ取得がどうにも上手くいかずハマりました。

ということで備忘録を残しておきます。

ちなみにGoは初心者でCloud Functionsで触り始めたくらいのレベルです。

# 勢いfunctionを作成する

Cloud FunctionsでGoを入れようと思ったときはまずCloud Functionsのページから関数の作成を試すかと思います。

ランタイムに `Go 1.11`、トリガーに `Cloud Pub/Sub` を指定して作成するとデフォルトでこんなやつを書いておいてくれます。Googleさん親切！

```go:cloud-functions
// Package p contains a Pub/Sub Cloud Function.
package p

import (
    "context"
    "log"
)

// PubSubMessage is the payload of a Pub/Sub event. Please refer to the docs for
// additional information regarding Pub/Sub events.
type PubSubMessage struct {
    Data []byte `json:"data"`
}

// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m PubSubMessage) error {
    log.Println(string(m.Data))
    return nil
}
```

ちなみにトリガーをPub/Subにするとトピックも指定する必要があります。
今回は適当に `go_pubsub_test` としてfunctionを作成します。

# Pub/Subトピックからpublishテストしてみる

最近のGCPはホント便利で、Pub/Subの画面からmessageのpublishができます。

<img width="825" alt="topic_publish.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/6aaff205-3891-e6a9-dee5-8504865a3eba.png">

テストメッセージということで、メッセージ本文に `hello world`、メッセージ属性のキーに `key`、　値に `value` を突っ込みました。

<img width="607" alt="message_publish.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/46837057-b2a7-ea81-8a2b-fdc1e02a1a6f.png">


functions側でこのメッセージのログを吐き出すようにしているので、下の`公開`ボタンを押してpublishしてみます。

# logを見てみる

publishに成功するとfunctions側の呼び出し回数のグラフがはねます。ログを表示を押してloggingに遷移するとログが出てることが確認できます。

<img width="849" alt="log.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/60dfa0ea-f61b-2a32-c44e-679d2f8c9202.png">

これを見ると、メッセージ本体の`hello world`は出力されていますが、key/valueの情報は見当たりません。

こうなるのは当然で、今回は構造体PubSubMessageでメッセージを受けましたが、PubSubMessageはDataを持たないのでメッセージ以外の情報は出力できません。

# pubsub.Messageを使う

色々調べてみると、タイトルに書いたpubsub.Messageというのが出てきました。

https://godoc.org/cloud.google.com/go/pubsub#Message

```go
type Message struct {
    // ID identifies this message.
    // This ID is assigned by the server and is populated for Messages obtained from a subscription.
    // This field is read-only.
    ID  string

    // Data is the actual data in the message.
    Data []byte

    // Attributes represents the key-value pairs the current message
    // is labelled with.
    Attributes map[string]string

    // The time at which the message was published.
    // This is populated by the server for Messages obtained from a subscription.
    // This field is read-only.
    PublishTime time.Time
    // contains filtered or unexported fields
}
```

ここに色々な情報が入ってるようです。
さきほど設定した`key`と`value`については、`Attributes`で取れるようになります。

# コードを修正する

ということでこれを利用して他のmessage情報も出力できるようにしてみます。

pubsub.Messageは `cloud.google.com/go/pubsub` から取得可能です。

Cloud Functions上で編集を実行し、以下のように書き換えます。

```go
// Package p contains a Pub/Sub Cloud Function.
package p

import (
	"context"
	"log"
	"cloud.google.com/go/pubsub"
)

// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m *pubsub.Message) error {
	log.Println(string(m.Data))
	if len(m.Attributes) > 0 {
		for k, v := range m.Attributes {
			log.Println("key", k)
			log.Println("value", v)
		}
	}
	return nil
}
```

デプロイして完了した後、同様にtopicを送りつけてみます。

<img width="721" alt="スクリーンショット 2019-09-12 14.58.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/adc5b2e7-0fc7-df6e-5904-793b2f15c14f.png">

ちゃんとデータを取得することが出来ました。

公式もこれを定義しておいて欲しいんですが、きっとその辺は自分で調べられるくらいPub/Sub理解してなきゃ使うんじゃないってメッセージなんだと思ってます。きびしめ。

# おまけ: メタデータ情報を取得する

Cloud Functionsでは[metadataライブラリ](https://godoc.org/cloud.google.com/go/functions/metadata)を使って他の情報を取得することができます。

試しにどんなデータが取得できるのかログを出してみます。

```go:cloud-functions
// Package p contains a Pub/Sub Cloud Function.
package p

import (
	"context"
	"log"
    "cloud.google.com/go/pubsub"
    "cloud.google.com/go/functions/metadata"
)

// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m *pubsub.Message) error {
    // metadata
    meta, _ := metadata.FromContext(ctx)
    
    log.Println("EventID", meta.EventID)
    log.Println("Timestamp", meta.Timestamp)
    log.Println("EventType", meta.EventType)
    
    log.Println("Resource.Service", meta.Resource.Service)
    log.Println("Resource.Name", meta.Resource.Name)
    log.Println("Resource.Type", meta.Resource.Type)
    
	return nil
}
```

出力結果は以下のような感じです。

<img width="1166" alt="go_log.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5964/ca545dc9-b730-8a5e-123d-44947baa82a8.png">

pubsubの場合は `Resource.Type` が `type.googleapis.com/google.pubsub.v1.PubsubMessage` となってやってくるようですね。
また、どのtopicから来たかは `Resource.Name` から取れるようなので、topicに応じて処理を分岐させるような書き方もできます。

metadataに関してはpubsub以外のトリガーも扱えるため、Resource.Typeに応じて処理を変えるのが良さそうです。


とりあえず分かったことはここまで。他にも分かったことがあったら追記しようと思います。