AlexaスキルをGoとServerlessで書いてみた

# 背景
昨年のAWS re:InventでアナウンスされたLambdaのGoサポートが[リリース](https://aws.amazon.com/jp/about-aws/whats-new/2018/01/aws-lambda-supports-go/)され、これを機にGoに触れてみたいと思いつつ、Alexaも日本でリリースされてから作ってなかったので、Go/LambdaでAlexaスキルを作ってみることにしました。SAMを使ったブログ「[Announcing Go Support for AWS Lambda](https://aws.amazon.com/jp/blogs/compute/announcing-go-support-for-aws-lambda/)」がリリースと同時にポストされましたが、今回は[Serverless Framework](https://serverless.com/blog/framework-example-golang-lambda-support/)を使うことにしました。

# 作ったもの
好きな色を答えさせるサンプルスキルである`alexa-skills-kit-color-expert-nodejs`と`alexa-skills-kit-color-expert-python`をGoで書き換えてみることを今回やってみました。言語を替えての写経となりました^^;

最終的な成果物はGitHub（[yamaryu0508/alexa-skills-kit-color-expert-go](https://github.com/yamaryu0508/alexa-skills-kit-color-expert-go)）にアップしています。

# 作成手順
今回の作成手順を辿っていきます。次のものはインストール・アカウント設定済みとします。

- Go言語開発環境
- AWS CLI
- Serverless Framework

## Serverless Frameworkの設定
「[Serverless Framework example for Golang and Lambda](https://serverless.com/blog/framework-example-golang-lambda-support/)」を参考に進めます。

まずはサービス作成です。

```
$ serverless create -u https://github.com/serverless/serverless-golang/ -p alexa-skills-kit-color-expert-go
```

ディレクトリを移動しておきます。

```
$ cd alexa-skills-kit-color-expert-go
```

出来上がった `serverless.yml` を整えます。

```yaml:serverless.yml
service: alexa-skills-kit-color-expert-go

provider:
  name: aws
  runtime: go1.x
  stage: dev
  region: ap-northeast-1

package:
 exclude:
   - ./**
 include:
   - ./bin/**

functions:
  ask-go:
    handler: bin/main
    memorySize: 256
    timeout: 10
    events:
      - alexaSkill
```

serverlessの準備はこれでOKです。

## Alexaスキルの作成 （Goコーディング）
Go/Lambdaを書くために、まずは`github.com/aws/aws-lambda-go/lambda`を`go get`します。

```
$ go get github.com/aws/aws-lambda-go/lambda
```

そして、まず最初に`event`オブジェクトを入れずにレスポンスが思い通りに返せるかを簡単なコードで確認しました。

```go:main.go
package main

import (
	"errors"
	"github.com/aws/aws-lambda-go/lambda"
)

// Response is object-type
type Response struct {
	Version           string `json:"version"`
	Response struct {
		ShouldEndSession bool `json:"shouldEndSession"`
		OutputSpeech     struct {
			Type string `json:"type"`
			Text string `json:"text"`
		} `json:"outputSpeech"`
	} `json:"response"`
}

// Handler is main
func Handler() (Response, error) {
	return Response{
		Version: "1.0",
		Response: {
			ShouldEndSession: true,
			OutputSpeech: {
				Type: "PlainText",
				Text: "Hello",
			},
		},
	}, nil
}

func main() {
	lambda.Start(Handler)
}

```

[サンプル](https://aws.amazon.com/jp/blogs/compute/announcing-go-support-for-aws-lambda/)や[LambdaのGoモデルのドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/go-programming-model.html)の見様見真似で書いてみましたが、**返したいJSONに対応する構造体を`return`すれば良い**ことをここでまず理解しました。同時に`Handler`**への引数もAlexaのリクエストのJSONに対応する構造体**になることに気づきました（Javaもこんな感じなんだろうが、触ったことない）。Lambdaへのトリガーサービスから渡ってくる`event`オブジェクトについては、`github.com/aws/aws-lambda-go/events`で[API Gateway等幾つか定義されている](https://github.com/aws/aws-lambda-go/tree/master/events)ので、ここで定義されているものはそのまま使うと良いかと思います。ただ、Alexaのモデルは定義されてなかったので、自分で書いてあげる必要があります（`request.go`に相当）。

ということで、とりあえずレスポンスを返すことに成功したところで、写経を進めていきます（確認方法は後述の[コンパイルとデプロイ](#コンパイルとデプロイ)と同様です）。

リファレンス「[カスタムスキルのJSONインターフェースのリファレンス](https://developer.amazon.com/ja/docs/custom-skills/request-and-response-json-reference.html)」を確認しながらAlexaスキルのリクエストとレスポンスの構造体をそれぞれ準備します。**今回は写経に必要ない**`object`**を結構省いています**(音声再生のための`SSML`の扱い等)ので網羅的には出来ていません。

また、データ構造をJSONと整合させるための定義なので、JSONをGoの構造体に変換してくれる「[JSON-to-Go](https://mholt.github.io/json-to-go/)」等を使いながら作業を進めると良さそうです。

```go:alexa/request.go
package alexa

import "time"

/*
 * define structs for Alexa Request
 */

// Session is object-type
type Session struct {
	New         bool   `json:"new"`
	SessionID   string `json:"sessionId"`
	Application struct {
		ApplicationID string `json:"applicationId"`
	} `json:"application"`
	Attributes map[string]interface{} `json:"attributes"`
	User       struct {
		UserID      string `json:"userId"`
		Permissions struct {
			ConsentToken string `json:"consentToken"`
		} `json:"permissions"`
		AccessToken string `json:"accessToken"`
	} `json:"user"`
}

// Slot is object-type
type Slot struct {
	Name               string                 `json:"name"`
	Value              string                 `json:"value"`
	ConfirmationStatus string                 `json:"confirmationStatus"`
	Resolutions        map[string]interface{} `json:"resolutions"`
}

// RequestIntent is object-type
type RequestIntent struct {
	Name               string `json:"name"`
	ConfirmationStatus string `json:"confirmationStatus"`
	Slots              map[string]Slot
}

// RequestDetail is map-type
type RequestDetail struct {
	Locale    string        `json:"locale"`
	Timestamp time.Time     `json:"timestamp"`
	Type      string        `json:"type"`
	RequestID string        `json:"requestId"`
	Intent    RequestIntent `json:"intent"`
}

// Request is object-type
type Request struct {
	Version string  `json:"version"`
	Session Session `json:"session"`
	Context struct {
		System struct {
			Application struct {
				ApplicationID string `json:"applicationId"`
			} `json:"application"`
			User struct {
				UserID      string `json:"userId"`
				Permissions struct {
					ConsentToken string `json:"consentToken"`
				} `json:"permissions"`
				AccessToken string `json:"accessToken"`
			} `json:"user"`
			Device struct {
				DeviceID            string `json:"deviceId"`
				SupportedInterfaces struct {
					AudioPlayer struct {
					} `json:"AudioPlayer"`
				} `json:"supportedInterfaces"`
			} `json:"device"`
			APIEndpoint string `json:"apiEndpoint"`
		} `json:"System"`
		AudioPlayer struct {
			Token                string  `json:"token"`
			OffsetInMilliseconds float32 `json:"offsetInMilliseconds"`
			PlayerActivity       string  `json:"playerActivity"`
		} `json:"AudioPlayer"`
	} `json:"context"`
	Request RequestDetail `json:"request"`
}
```

レスポンスについては、`alexa-skills-kit-color-expert-python`を参考に`BuildSpeechletResponse`や`BuildResponse`と言った関数も定義しました。

```go:alexa/response.go
package alexa

/*
 * define structs for Alexa Response
 */

// OutputSpeech is object-type
type OutputSpeech struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

// Card is object-type
type Card struct {
	Type    string `json:"type"`
	Title   string `json:"title"`
	Content string `json:"content"`
}

// Reprompt is object-type
type Reprompt struct {
	OutputSpeech OutputSpeech `json:"outputSpeech"`
}

// SpeechletResponse is object-type
type SpeechletResponse struct {
	ShouldEndSession bool         `json:"shouldEndSession"`
	OutputSpeech     OutputSpeech `json:"outputSpeech"`
	Card             Card         `json:"card"`
	Reprompt         Reprompt     `json:"reprompt"`
}

// SessionAttributes is map-type
type SessionAttributes map[string]interface{}

// Response is object-type
type Response struct {
	Version           string            `json:"version"`
	SessionAttributes SessionAttributes `json:"sessionAttributes"`
	Response          SpeechletResponse `json:"response"`
}

// BuildSpeechletResponse is function-type
func BuildSpeechletResponse(title string, output string, repromptText string, shouldEndSession bool) SpeechletResponse {
	return SpeechletResponse{
		ShouldEndSession: shouldEndSession,
		OutputSpeech: OutputSpeech{
			Type: "PlainText",
			Text: output,
		},
		Card: Card{
			Type:    "Simple",
			Title:   "SessionSpeechlet - " + title,
			Content: "SessionSpeechlet - " + output,
		},
		Reprompt: Reprompt{
			OutputSpeech: OutputSpeech{
				Type: "PlainText",
				Text: repromptText,
			},
		},
	}
}

// BuildResponse is function-type
func BuildResponse(sessionAttributes SessionAttributes, speechletResponse SpeechletResponse) Response {
	return Response{
		Version:           "1.0",
		Response:          speechletResponse,
		SessionAttributes: sessionAttributes,
	}
}
```

そして、リクエストとレスポンスが定義できたら、Alexaスキルのメイン部分の写経に入ります。

```go:main.go
package main

import (
	"errors"
	"fmt"

	"./alexa"
	"github.com/aws/aws-lambda-go/lambda"
)

var (
	// ErrInvalidIntent is error-object
	ErrInvalidIntent = errors.New("Invalid intent")
)

/*
 * Functions that control the skill's behavior
 */

// GetWelcomeResponse is function-type
func GetWelcomeResponse() alexa.Response {
	sessionAttributes := make(map[string]interface{})
	cardTitle := "Welcome"
	speechOutput := "Welcome to the Alexa Skills Kit sample. Please tell me your favorite color by saying, my favorite color is red"
	repromptText := "Please tell me your favorite color by saying, my favorite color is red."
	shouldEndSession := false
	return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}

// HandleSessionEndRequest is function-type
func HandleSessionEndRequest() alexa.Response {
	sessionAttributes := make(map[string]interface{})
	cardTitle := "Session Ended"
	speechOutput := "Thank you for trying the Alexa Skills Kit sample. Have a nice day! "
	repromptText := ""
	shouldEndSession := true
	return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}

// CreateFavoriteColorAttributes is function-type
func CreateFavoriteColorAttributes(favoriteColor string) alexa.SessionAttributes {
	sessionAttributes := make(map[string]interface{})
	sessionAttributes["favoriteColor"] = favoriteColor
	return sessionAttributes
}

// SetColorInSession is function-type
func SetColorInSession(intent alexa.RequestIntent, session alexa.Session) alexa.Response {
	cardTitle := intent.Name
	sessionAttributes := make(map[string]interface{})
	shouldEndSession := false
	speechOutput := ""
	repromptText := ""

	if color, ok := intent.Slots["Color"]; ok {
		favoriteColor := color.Value
		sessionAttributes = CreateFavoriteColorAttributes(favoriteColor)
		speechOutput = "I now know your favorite color is " + favoriteColor +
			". You can ask me your favorite color by saying, " +
			"what's my favorite color?"
		repromptText = "You can ask me your favorite color by saying, " +
			"what's my favorite color?"
	} else {
		speechOutput = "I'm not sure what your favorite color is. " +
			"Please try again."
		repromptText = "I'm not sure what your favorite color is. " +
			"You can tell me your favorite color by saying, " +
			"my favorite color is red."
	}
	return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}

// GetColorFromSession is function-type
func GetColorFromSession(intent alexa.RequestIntent, session alexa.Session) alexa.Response {
	cardTitle := intent.Name
	sessionAttributes := make(map[string]interface{})
	shouldEndSession := false
	speechOutput := ""
	repromptText := ""

	if favoriteColor, ok := session.Attributes["favoriteColor"].(string); ok {
		speechOutput = "Your favorite color is " + favoriteColor + ". Goodbye."
		shouldEndSession = true
	} else {
		speechOutput = "I'm not sure what your favorite color is, you can say, my favorite color is red"
	}
	return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}

// GetNoEntityResponse is function-type
func GetNoEntityResponse() alexa.Response {
	cardTitle := ""
	sessionAttributes := make(map[string]interface{})
	shouldEndSession := false
	speechOutput := ""
	repromptText := ""
	return alexa.BuildResponse(sessionAttributes, alexa.BuildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession))
}

/*
 * Events
 */

// OnSessionStarted is function-type
func OnSessionStarted(sessionStartedRequest map[string]string, session alexa.Session) (alexa.Response, error) {
	fmt.Println("OnSessionStarted requestId=" + sessionStartedRequest["requestId"] + ", sessionId=" + session.SessionID)
	return GetNoEntityResponse(), nil
}

// OnLaunch is function-type
func OnLaunch(launchRequest alexa.RequestDetail, session alexa.Session) (alexa.Response, error) {
	fmt.Println("OnLaunch requestId=" + launchRequest.RequestID + ", sessionId=" + session.SessionID)
	return GetWelcomeResponse(), nil
}

// OnIntent is function-type
func OnIntent(intentRequest alexa.RequestDetail, session alexa.Session) (alexa.Response, error) {
	fmt.Println("OnLaunch requestId=" + intentRequest.RequestID + ", sessionId=" + session.SessionID)
	intent := intentRequest.Intent
	intentName := intentRequest.Intent.Name

	if intentName == "MyColorIsIntent" {
		return SetColorInSession(intent, session), nil
	} else if intentName == "WhatsMyColorIntent" {
		return GetColorFromSession(intent, session), nil
	} else if intentName == "AMAZON.HelpIntent" {
		return GetWelcomeResponse(), nil
	} else if intentName == "AMAZON.StopIntent" || intentName == "AMAZON.CancelIntent" {
		return HandleSessionEndRequest(), nil
	}
	return alexa.Response{}, ErrInvalidIntent
}

// OnSessionEnded is function-type
func OnSessionEnded(sessionEndedRequest alexa.RequestDetail, session alexa.Session) (alexa.Response, error) {
	fmt.Println("OnSessionEnded requestId=" + sessionEndedRequest.RequestID + ", sessionId=" + session.SessionID)
	return GetNoEntityResponse(), nil
}

// Handler is main
func Handler(event alexa.Request) (alexa.Response, error) {
	fmt.Println("event.session.application.applicationId=" + event.Session.Application.ApplicationID)

	eventRequestType := event.Request.Type
	if event.Session.New {
		return OnSessionStarted(map[string]string{"requestId": event.Request.RequestID}, event.Session)
	} else if eventRequestType == "LaunchRequest" {
		return OnLaunch(event.Request, event.Session)
	} else if eventRequestType == "IntentRequest" {
		return OnIntent(event.Request, event.Session)
	} else if eventRequestType == "SessionEndedRequest" {
		return OnSessionEnded(event.Request, event.Session)
	}
	return alexa.Response{}, ErrInvalidIntent
}

func main() {
	lambda.Start(Handler)
}
```

やはり、実際の作業では`alexa/request.go`、`alexa/response.go`と`main.go`の間で行き来しながら、書きました。ただ、VSCodeに入れていたLintツールがコンパイルレベルのチェックはかけてくれていたので、実際にコンパイルやデプロイを経る途中確認はあまり行っていません。

ここまでの作業を終えると、次のようなディレクトリ・ファイル構造になっていると思います。

alexa-skills-kit-color-expert-go
┣ alexa
┃　┣ request.go
┃　┗ response.go
┣ main.go
┣ serverless.yml
┗ README.md

## コンパイルとデプロイ
Lambda(Linux)の実行ファイルにコンパイルします。

```
$ GOOS=linux go build -o bin/main
```

ここで、試しにローカル実行してみたのですが、Goは未対応でした。

```
$ sls invoke local --function ask-go --path event.json

  Serverless Error ---------------------------------------
 
  You can only invoke Node.js, Python & Java functions locally.
 
  Get Support --------------------------------------------
     Docs:          docs.serverless.com
     Bugs:          github.com/serverless/serverless/issues
     Forums:        forum.serverless.com
     Chat:          gitter.im/serverless/serverless
 
  Your Environment Information -----------------------------
     OS:                     darwin
     Node Version:           6.10.0
     Serverless Version:     1.26.0
```

ですので、このままAWSにデプロイします。

```
$ sls deploy
```

AWSにデプロイできたら、出来上がったLambda関数のテストを行っておきましょう。`Alexa Intent - MyColors` を使います。

![スクリーンショット 2018-01-31 18.05.43.png](https://qiita-image-store.s3.amazonaws.com/0/43383/b5ace2f4-cd03-3484-c751-9017dfa168d0.png)

テストの実行結果がこんな具合に返ってくればOKです。

![スクリーンショット 2018-01-31 18.08.18.png](https://qiita-image-store.s3.amazonaws.com/0/43383/f185bda3-482c-247b-16d7-61ce564ba698.png)

そして、出来上がったLambda関数の`ARN`（`arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:ask-kintone-dev-ask-go`）をメモっておきます。

![スクリーンショット_2018-01-31_18_11_47.jpg](https://qiita-image-store.s3.amazonaws.com/0/43383/749056ae-7363-c6ea-aa9c-d9e39c5ffab8.jpeg)

## Alexaスキルの設定
developer.amazon.com のコンソールから、Alexaスキルの設定を行って完成です。設定は[チュートリアル](https://developer.amazon.com/ja/alexa-skills-kit/alexa-skill-quick-start-tutorial)のSTEP2からの手順を進めていきます。メモっておいた`ARN`はSTEP2-14の設定で使います。

## 確認
無事全ての設定を終えたら、実機や[Echosim.io](https://echosim.io/welcome)等で、サンプル通りに設定できているか確認します。

# 所感とまとめ
## Go/Lambda

- 十数年振りの静的型付けがある言語はちょっとしんどかったが、新しい学びがあって楽しかった
- Goの強みとされる速さ、軽さ、並行処理といった部分が生きてくるケースではLambda前提で実際に使ってみたい（ただ、まぁまぁな学習コストになりそう）
- 逆にGoの強みが顕在化しない、必要ないケースでは、自分は書き慣れたNode.jsやPythonを選択すると思う
- `event`オブジェクトと`return`するオブジェクトは、`interface{}`にしておけば、JSONと構造体の突き合わせは不要だったかもしれないと思った（サンプルでは定義されていたので、同じようにやってみたものの）
- [go-kintone](https://github.com/kintone/go-kintone)をLambdaで試してみたい

## Alexa

- 久々にAlexaスキルのロジック書いて、理解が深まった
- スキルビルダーで`インテント`や`スロット`、`サンプル発話`が設定しやすくなっていた
- Alexa開発のための言語選択という点では、Node.jsについては、SDK（[Alexa Skills Kit SDK for Node.js](https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs)）の登場で非常に楽に書けるようになってきたので、あえてGoを使うというケースはレアケースになるように思う。作りたいスキルとそれを実現するための開発ライブラリの充実に寄るところもあると思うが（Lambdaを使わずにAlexaのサーバーサイドロジックを構築するというのであれば・・・いやそれでもどうだろう）

# 免責

こちらは個人の意見で、所属する企業や団体は関係ありません。

# 参考にした情報
- [Announcing Go Support for AWS Lambda](https://aws.amazon.com/jp/blogs/compute/announcing-go-support-for-aws-lambda/)
- [Serverless Framework example for Golang and Lambda](https://serverless.com/blog/framework-example-golang-lambda-support/)
- [Programming Model for Authoring Lambda Functions in Go](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/go-programming-model.html)
- [Alexa Skills Kitによるスキルの作成](https://developer.amazon.com/ja/docs/ask-overviews/build-skills-with-the-alexa-skills-kit.html)
- [カスタムスキルのJSONインターフェースのリファレンス](https://developer.amazon.com/ja/docs/custom-skills/request-and-response-json-reference.html)
- [カスタムスキルのAWS Lambda関数を作成する](https://developer.amazon.com/ja/docs/custom-skills/host-a-custom-skill-as-an-aws-lambda-function.html)
- [標準のリクエストタイプのリファレンス](https://developer.amazon.com/ja/docs/custom-skills/request-types-reference.html)
- [Alexaから送信されたリクエストを処理する](https://developer.amazon.com/ja/docs/custom-skills/handle-requests-sent-by-alexa.html)
- [https://github.com/alexa](https://github.com/alexa)
- [Alexaスキル開発トレーニング](https://developer.amazon.com/ja/alexa-skills-kit/training/building-a-skill)
- [Go言語の初心者が見ると幸せになれる場所　#golang](https://qiita.com/tenntenn/items/0e33a4959250d1a55045)
- [はじめてのGo―シンプルな言語仕様，型システム，並行処理](http://gihyo.jp/dev/feature/01/go_4beginners)