オウム返しチャットボット LINE API＋Lambda＋Go言語(フレームワーク利用なし)

# これなに？
LINEで、メッセージをオウム返しして返答するだけのチャットボット（良くあるやつ）。
Lambda＋Go言語で、フレームワーク(`serverless`)を**利用しない**サンプルコードが見つからなかったので投稿。

![go.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263163/8cb393f5-f0dd-d23e-f840-bae61be5cc70.png)

# 環境
Windows環境で開発・コンパイル（build）して、AWS LambdaのWeb画面でZIPファイルをuploadする、というスタイルで構築。

### 開発・コンパイル環境
- Windows 10 64bit
- go1.11.9.windows-amd64
    - 記事執筆時点の最新は1.12.4だけれど、後述の通りbuildがうまくいかなかったので、1.11.9を利用
    - フレームワークは特に利用していない
- IDE：Visual Studio Code
    - 今回の記事に関係ないので、詳細割愛。

### 実行環境
- LINE Messaging API
- AWS Lambda(言語：Go 1.x)
- Amazon API Gateway


# コード
LINE Messaging APIの設定、Lambda・API Gateway・CloudWatchの詳細な設定は特にややこしいところがないので割愛。

- Amazon API Gatewayから受け取ったイベントは、LINEのSDKを利用せずにパースしている。
    - SDKを使った方がよかったのかもしれないが、GO＋Lambda環境用のちょうどよいサンプルが見つからず、SDKの使い方がよくわからなかったため、結局自力でJSONをパース（自動生成されたコードだけど）。
    - 本当はCHANNELSECRETとACCESSTOKENのチェックをしないとダメだけど、動作検証用なのでいったん割愛。
- main()のreturnが、Amazon API Gateway経由でLINEに返る。ここは200 OKを返せばOK。
- リプライメッセージはLINE SDKの`linebot.ReplyMessage()`を利用している。
- Lambdaの環境変数として、CHANNELSECRETとACCESSTOKENを設定している。

```line.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/line/line-bot-sdk-go/linebot"
)

// API Gatewayから受け取ったevents.APIGatewayProxyRequestのBody（JSON）をパースする
// https://app.quicktype.io/　に、実際に受け取ってたJSONメッセージを張り付けて、コード自動生成。

// ▼▼▼ https://app.quicktype.io/で自動生成したコード：ここから ▼▼▼
func UnmarshalLineRequest(data []byte) (LineRequest, error) {
	var r LineRequest
	err := json.Unmarshal(data, &r)
	return r, err
}

func (r *LineRequest) Marshal() ([]byte, error) {
	return json.Marshal(r)
}

type LineRequest struct {
	Events      []Event `json:"events"`
	Destination string  `json:"destination"`
}

type Event struct {
	Type       string  `json:"type"`
	ReplyToken string  `json:"replyToken"`
	Source     Source  `json:"source"`
	Timestamp  int64   `json:"timestamp"`
	Message    Message `json:"message"`
}

type Message struct {
	Type string `json:"type"`
	ID   string `json:"id"`
	Text string `json:"text"`
}

type Source struct {
	UserID string `json:"userId"`
	Type   string `json:"type"`
}

// ▲▲▲ https://app.quicktype.io/で自動生成したコード：ここまで ▲▲▲

// Handler
// fmt.Printlnやlog.Fatalは、CloudWatchのログで確認可能
func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

	// 受け取ったJSONメッセージをログに書き込む（デバッグ用）
	fmt.Println("*** body")
	fmt.Println(request.Body)

	// JSONデコード
	fmt.Println("*** JSON decode")
	myLineRequest, err := UnmarshalLineRequest([]byte(request.Body))
	if err != nil {
		log.Fatal(err)
	}

	// ボットの定義
	fmt.Println("*** linebot new")
	bot, err := linebot.New(
		os.Getenv("CHANNELSECRET"),
		os.Getenv("ACCESSTOKEN"),
	)
	if err != nil {
		log.Fatal(err)
	}

	// リプライ実施
	fmt.Println("*** reply")
	var tmpReplyMessage string
	tmpReplyMessage = "回答：" + myLineRequest.Events[0].Message.Text
	if _, err = bot.ReplyMessage(myLineRequest.Events[0].ReplyToken, linebot.NewTextMessage(tmpReplyMessage)).Do(); err != nil {
		log.Fatal(err)
	}

	// 終了
	fmt.Println("*** end")
	return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil
}

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

#### buildコマンド (PowerShell)
```powershell
> $env:GOOS="linux"
> go env GOOS
linux
> go build -o line01 line01.go
> build-lambda-zip.exe -o line01.zip line01
```

# 実行結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263163/3ef58925-2547-9df8-caa0-4250d87f6245.png" width=50%>


# ハマったポイント
Go言語もVisual Studio Codeも初めてだったので、かなり初歩的だけれども。。

### ①Go言語用のLambdaパッケージ(`github.com/aws/aws-lambda-go/lambda`)がbuild時に読み込まれない

#### 事象
- go1.12 (Windows)
- `$GOPATH = C:\users\45lb_plates\go`
- `$GOROOT = C:\Go`
- `go get -u github.com/aws/aws-lambda-go/lambda`は実行済み

という環境でbuildを実行すると、以下のエラーとなる。

```powershell
> go build -o line line.go
line.go:4:8: cannot find package "github.com/aws/aws-lambda-go/lambda" in any of:
 C:\Go\src\github.com\aws\aws-lambda-go\lambda (from $GOROOT)
 C:\users\45lb_plates\go\src\github.com\aws\aws-lambda-go\lambda (from $GOPATH)
exit status 1
Process exiting with code: 1
```

#### 解決策
go1.12を諦め、go1.11.9をインストールしなおしたらあっさり解決。

- 実際にパッケージ`github.com/aws/aws-lambda-go/lambda`がインストールされていたのは`C:\Users\45lb_plates\go\pkg\mod\github.com\aws\aws-lambda-go@v1.10.0\lambda`だった。`$GOROOT\pkg\mod`配下にインストールされるのは、go1.12からの仕様？らしい。
- 一方、build時に読み込もうとしているフォルダは`$GOROOT\src`配下で、ここが不整合となっている。
- go1.12で、どうやったらbuild時に`$GOROOT\pkg\mod`を読み込ませるのかわからなかった。環境変数`GO111MODULE`を`on`にしたり`off`にしたりは試してみたが、改善せず。

### ②Windows環境でLinux用のクロスコンパイルができない

#### 事象
コンパイル対象のOSをSETコマンドでWindowsからLinuxへ変更したいが、されない。

```powershell
> set GOOS=linux
> go env GOOS
Windows
```

#### 解決策
Visual Studio Codeのターミナルが「PowerShell」になっていたため、SETコマンドを受け付けてなかった。
SETコマンドはDOSプロンプトで環境変数を変更するコマンド。PowerShellでSETコマンドを打ってもエラーは出ないが、環境変数は変わらない。（Goのクロスコンパイルについて検索すると、みんなSETコマンドだったので全然気づかなかった。。）

PoweShell用の環境変数を変更するコマンドを改めて実行し、解決。

```powershell
> $env:GOOS="linux"
> go env GOOS
linux
```


# 参考にした資料・利用したツール等
- [[Qiita] Golang + lambda + lineBot(serverless)](https://qiita.com/Yamashou/items/ae600c75ce91e123c579) 
    - 当初このページを参考にしていたが、`serverless`フレームワークを使わない場合は、コード等々が全然違くなることに途中で気づく。最終的にはコードは参照せず。
- [quicktype](https://app.quicktype.io/)
    - JSONメッセージを張り付けると、パースするコードを自動生成してくれるWEBツール
    - LINE Messaging APIから送られてくるJSONメッセージをパースするコードを生成するのに使用
- [[Qiita]Go言語でJSONを扱う](https://qiita.com/nayuneko/items/2ec20ba69804e8bf7ca3)、[[Qiita] Goで値が文字列のJSONを構造体にパースする](https://qiita.com/kmagai/items/b0bac178e69f59557504)
    - JSONについてまとまってました。結局最終的には自分でコードを書かず、上記のquicktypeでコードを自動生成。


# 参考：Lambda受け取ったJSONメッセージを確認するには
LINE API GatewayからAWS API Gateway経由でLambda受け取ったJSONメッセージを確認する方法。
このJSONメッセージを[quicktype](https://app.quicktype.io/)に張り付けると、JSONメッセージをパースするコードが自動生成される。便利！

```go:コード
package main

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

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    fmt.Println(request.Body)
    return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil
}

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

```json:実行結果例（受け取ったJSONメッセージ）
{
    "events": [
        {
            "type": "message",
            "replyToken": "704xxxx4024cxxxx9c86xxxxcdf0xxxx",
            "source": {
                "userId": "U8845xxxx971bxxxx6f98xxxxa428xxxx",
                "type": "user"
            },
            "timestamp": 1599999999999,
            "message": {
                "type": "text",
                "id": "9809999999999",
                "text": "てすと"
            }
        }
    ],
    "destination": "Uc082xxxxe925xxxx0a19xxxx5e31xxxx"
}
```

おわり。