GolangでNCMBにリクエストを送る

NCMBでは各種言語向けにSDKを提供しています。公式ではない、コミュニティSDKを入れると10以上あるかと思います。

今回はGo言語でNCMBへのリクエストを送信してみました。問題になるのは署名生成プロセスなので、そこを関数化したことで、今後のSDK化も考えられそうです。

## 必要なライブラリ

今回利用したライブラリは以下の通りです。

```go
import (
	"fmt"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"net/url"
	"sort"
	"time"
	"encoding/json"
	"github.com/go-resty/resty/v2"
	"github.com/tidwall/gjson"
)
```

## 定数の定義

利用した、基本となる定数です。

```go
// 定数の定義
const (
	ApplicationKey     = "YOUR_APPLICATION_KEY"
	SignatureMethod    = "HmacSHA256"
	SignatureVersion   = "2"
	FQDN               = "mbaas.api.nifcloud.com"
	ClientKey          = "YOUR_CLIENT_KEY"
)
```

## main関数の流れ

main関数は以下のようになっています。今回はobjectIdやdefinePathは空ですが、汎用的な署名生成時には使える予定です。

```go
func main() {
	t := time.Now()
	where := map[string]string{
		"testKey": "testValue",
	}
	bytes, _ := json.Marshal(where)
	query := map[string]string{
		"where": string(bytes),
	}
	objectId := ""
	definePath := ""
	path := getPath("TestClass", objectId, definePath)
	signature, _ := createSignature("GET", "TestClass", t, query, objectId, definePath)

	// URLを作成
	partofUrl := &url.URL{
		Scheme: "https",
		Host: FQDN,
		Path: path,
		RawQuery: mapToQuery(query),
	}

	// Restyクライアントの生成
	client := resty.New()
	// リクエストの作成と送信
	resp, err := client.R().
      SetHeader("Content-Type", "application/json").
			SetHeader("X-NCMB-Application-Key", ApplicationKey).
			SetHeader("X-NCMB-Signature", signature).
			SetHeader("X-NCMB-Timestamp", t.Format("2006-01-02T15:04:05.999Z0700")).
      Get(partofUrl.String())
	if err != nil {
		fmt.Println(err)
	}
	// レスポンスの解析
	value := gjson.Get(resp.String(), "results")
	fmt.Println(value)
}
```

## 署名生成関数

`createSignature` の内容は以下のようになります。処理内容については [REST API リファレンス : シグネチャの生成方法 \| ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/rest/common/signature.html) を参照してください。

```go
// 署名を作成する関数
func createSignature(method, className string, time time.Time, queries map[string]string, objectId string, definePath string) (string, error) {
	// パスの取得
	var path = getPath(className, objectId, definePath)
	// baseInfoの定義
	baseInfoMap := map[string]string{
		"X-NCMB-Application-Key": ApplicationKey,
		"SignatureMethod":        SignatureMethod,
		"SignatureVersion":       SignatureVersion,
		"X-NCMB-Timestamp":       time.Format("2006-01-02T15:04:05.999Z0700"),
	}

	// クエリが存在する場合は、それをbaseInfoMapに追加
	if queries != nil {
		for k, v := range queries {
			baseInfoMap[k] = url.QueryEscape(v)
		}
	}

	// 自然順序でソート
	var keys []string
	for k := range baseInfoMap {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	// baseInfoの作成
	var baseInfo string
	for _, k := range keys {
		if baseInfo != "" {
			baseInfo += "&"
		}
		baseInfo += k + "=" + baseInfoMap[k]
	}

	// 署名文字列の作成
	signatureString := method + "\n" + FQDN + "\n" + path + "\n" + baseInfo
	// HMACエンコーディング
	h := hmac.New(sha256.New, []byte(ClientKey))
	h.Write([]byte(signatureString))

	// Base64エンコーディング
	return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
}
```

## その他

その他、用意した関数です。

```go
// パスを取得する関数
func getPath(className string, objectId string, definePath string) string {
	if definePath != "" {
		return definePath
	} else if objectId != "" {
		return fmt.Sprintf("/2013-09-01/classes/%s/%s", className, objectId)
	} else {
		return fmt.Sprintf("/2013-09-01/classes/%s", className)
	}
}

// map型のクエリをstring型に変換する関数
func mapToQuery(queries map[string]string) string {
	var query string
	for k, v := range queries {
		if query != "" {
			query += "&"
		}
		query += k + "=" + url.QueryEscape(v)
	}
	return query
}
```

## コード全体

この流れで、NCMBのデータストアを検索する処理ができます。データ投稿などは未対応ですが、GETをPOSTに変えればいけるでしょう。

```go
package main

import (
	"fmt"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"net/url"
	"sort"
	"time"
	"encoding/json"
	"github.com/go-resty/resty/v2"
	"github.com/tidwall/gjson"
)

// 定数の定義
const (
	ApplicationKey     = "YOUR_APPLICATION_KEY"
	SignatureMethod    = "HmacSHA256"
	SignatureVersion   = "2"
	FQDN               = "mbaas.api.nifcloud.com"
	ClientKey          = "YOUR_CLIENT_KEY"
)

// パスを取得する関数
func getPath(className string, objectId string, definePath string) string {
	if definePath != "" {
		return definePath
	} else if objectId != "" {
		return fmt.Sprintf("/2013-09-01/classes/%s/%s", className, objectId)
	} else {
		return fmt.Sprintf("/2013-09-01/classes/%s", className)
	}
}

// map型のクエリをstring型に変換する関数
func mapToQuery(queries map[string]string) string {
	var query string
	for k, v := range queries {
		if query != "" {
			query += "&"
		}
		query += k + "=" + url.QueryEscape(v)
	}
	return query
}

// 署名を作成する関数
func createSignature(method, className string, time time.Time, queries map[string]string, objectId string, definePath string) (string, error) {
	// パスの取得
	var path = getPath(className, objectId, definePath)
	// baseInfoの定義
	baseInfoMap := map[string]string{
		"X-NCMB-Application-Key": ApplicationKey,
		"SignatureMethod":        SignatureMethod,
		"SignatureVersion":       SignatureVersion,
		"X-NCMB-Timestamp":       time.Format("2006-01-02T15:04:05.999Z0700"),
	}

	// クエリが存在する場合は、それをbaseInfoMapに追加
	if queries != nil {
		for k, v := range queries {
			baseInfoMap[k] = url.QueryEscape(v)
		}
	}

	// 自然順序でソート
	var keys []string
	for k := range baseInfoMap {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	// baseInfoの作成
	var baseInfo string
	for _, k := range keys {
		if baseInfo != "" {
			baseInfo += "&"
		}
		baseInfo += k + "=" + baseInfoMap[k]
	}

	// 署名文字列の作成
	signatureString := method + "\n" + FQDN + "\n" + path + "\n" + baseInfo
	// HMACエンコーディング
	h := hmac.New(sha256.New, []byte(ClientKey))
	h.Write([]byte(signatureString))

	// Base64エンコーディング
	return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
}

func main() {
	t := time.Now()
	where := map[string]string{
		"testKey": "testValue",
	}
	bytes, _ := json.Marshal(where)
	query := map[string]string{
		"where": string(bytes),
	}
	objectId := ""
	definePath := ""
	path := getPath("TestClass", objectId, definePath)
	signature, _ := createSignature("GET", "TestClass", t, query, objectId, definePath)

	// URLの一部を作成
	partofUrl := &url.URL{
		Scheme: "https",
		Host: FQDN,
		Path: path,
		RawQuery: mapToQuery(query),
	}

	// Restyクライアントの生成
	client := resty.New()
	// リクエストの作成と送信
	resp, err := client.R().
      SetHeader("Content-Type", "application/json").
			SetHeader("X-NCMB-Application-Key", ApplicationKey).
			SetHeader("X-NCMB-Signature", signature).
			SetHeader("X-NCMB-Timestamp", t.Format("2006-01-02T15:04:05.999Z0700")).
      Get(partofUrl.String())
	if err != nil {
		fmt.Println(err)
	}
	// レスポンスの解析
	value := gjson.Get(resp.String(), "results")
	fmt.Println(value)
}
```

## まとめ

Go言語はWebサービスであったり、CLIなどでも活用されています。NCMBと連携すれば、データを保存・取得できる場所として活用できるでしょう。

関数レベルではありますが、NCMBとGo言語を組み合わせる際の参考にしてください。

[mBaaSでサーバー開発不要！ \| ニフクラ mobile backend](https://mbaas.nifcloud.com/)