##概要
初投稿になります。
筆者はJavaをメインで勉強してきて、新しい言語(Go)を学びたい!と思って勉強しました。
その過程で自分が躓いたところや参考にした記事を共有できればと思いました。
他の言語をメインとしてきて、Goを学んでみようかな!と思う人がいて、基礎的な知識を身に着ける参考になれば幸いです。
Goのシステムを作成するにあたって、下記サイトGoの基礎を勉強しました。
A Tour of Go
今回作成したコードです。
天気予報ボット
##背景 (天気予報ボットにした理由)
何か新しいことを勉強しようと思った際に、自分が日常で困っている問題を考え、
その中でも頻度が多い問題を解決できるシステムを作成しようと思いました。
家を出るギリギリまで寝たい自分にとって、朝支度はほぼ時間がありません笑
天気予報を確認しようと思うのですが、いつも忘れてしまい、夜に雨が降った日には。。。笑ということが度々起こっていました。
アラームは携帯で鳴らしているので、スマホに通知が来ていれば必ず確認できる!と思い天気予報のボットを作成しようと思いました!
##LINEのMessage API
ボットがいくつかある中で、ラインの使用頻度が高いので、ラインボットを使用しました。
###ラインボットの使用方法
LINE Developers
上記のURLに飛んでアカウントを作成してください。
躓いた際は他の方が記事を出しているので、参考にしてください。
アカウント作成・ログイン後、新規チャネル作成をしましょう。
チャネルの種類はMessaging APIで、
必須項目を埋めて作成しましょう。(任意項目は埋めなくて大丈夫です。
作成後このような画面に遷移されると思います。
この後のシステムで使用される情報
CHANNEL_SECRET
、USER_ID
、CHANNEL_TOKEN
はこの画面から確認できます。
ユーザーのメッセージに応じて、メッセージを返す機能を設定することもできますが
今回はプッシュメッセージなので、その設定は省略します。
CDKでインフラを定義
cdk init app --language=typescript
CLIでAWSの環境設定ができているかを確認しましょう
aws configure list
access_key、secret_key、default_regionが設定できていないと、デプロイ作業ができません。
AWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYが設定されていない場合は
AWSコンソールにログインし、新規IAMユーザーを作成してください
(既存ユーザーでsecret_keyが確認できる場合はそちらを使用しても構いません。)
新規作成したら、AWS_ACCESS_KEY_IDとAWS_SECRET_ACCESS_KEYを取得できるので
aws configureで各項目の入力が求められるので、設定をお願いします。
default_regionはお好みのリージョンで。
aws configure
AWS Access Key ID [****************GJQN]:
AWS Secret Access Key [****************RXDp]:
Default region name [ap-northeast-1]:
Default output format [None]:
##ファイルを保存するS3バケットを作成
cdk bootstrap
初めてデプロイする際はcdk bootstrap
コマンドにより、CloudFormationで利⽤するデプロイ⽤のS3 バケットを作成します。
##デプロイ
cdk deploy
先にデプロイ作業できるか確認しておきます。
成功したら、AWSコンソールのサービスタブでS3
と検索します。
S3のページに遷移して、新規のバケットが作成されていることを確認できたら、デプロイ成功です!
##ライブラリをインストール
npm install @aws-cdk/aws-lambda @aws-cdk/aws-events @aws-cdk/aws-events-targets
##AWS
import cdk = require("@aws-cdk/core");
import lambda = require("@aws-cdk/aws-lambda");
import events = require("@aws-cdk/aws-events");
import eventsTargets = require("@aws-cdk/aws-events-targets");
export class WeatherForcastStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
const postLineMessage = new lambda.Function(
this,
"PostLineMessageHandler",
{
code: lambda.Code.fromAsset("lambda"),
handler: "postLineMessage",
runtime: lambda.Runtime.GO_1_X,
environment: {
CHANNEL_SECRET: "YOUR_CHANNEL_SECRET",
CHANNEL_TOKEN: "YOUR_CHANNEL_TOKEN",
USER_ID: "YOUR_USER_ID",
API_FORCAST_KEY: "YOUR_API_FORCAST_KEY",
},
}
);
new events.Rule(this, "WeatherForcastEvent", {
schedule: events.Schedule.cron({
minute: "30",
hour: "22",
day: "*",
month: "*",
year: "*",
}),
targets: [new eventsTargets.LambdaFunction(postLineMessage)],
});
}
}
###Lambdaの定義
code:
はLambda関数のソースコードを指定します。今回はlambdaというディレクトリを作成し、そこにmain.goを作成することにします。
handler:
Lambdaが関数を実行するために呼び出すコード内のメソッドの名前を指定します。
runtime:
アップロードするLambda関数のランタイム環境を指定します。
environment:
キーと値で環境変数を指定できます。ここに非公開の認証系の情報を設定します。
###CloudWatch Eventsの定義
定期実行の時間はGMTで指定なので、日本時間の9時間前を指定しましょう。
出勤時間に合わせて7時30分にしたかったので、22時30分を記載。
targets:
でlambda関数を指定します。
AWS Lambda/CloudWatch Eventsで参考にさせて頂いたサイト
[AWS CDKでCloudWatch EventsでLambdaを定期実行(TypeScript版)]
(https://itotetsu.hatenablog.com/entry/cloudwatch-events-via-aws-cdk)
Lambdaの定義でcodeやhandler以外にも色々定義できます。
class Function
##Open Weather Map
Open Weather API
天気予報のボットを作成するにあたって、一番欲しい情報は毎時間の天気でした。
天気APIはいくつかあるものの、無料枠で毎時間の情報を提供しているのは自分が探す限り少なかったです。
その中で人気もあり、最近APIの更新もしたOpen Weather MapのOne Call APIを使用しました。
このAPIは本日と明日の毎時間の天気情報や1週間の天気情報を提供頂けるので、自分にはぴったりと思いました。
今回使用しなかったデータに、体感温度・湿度・降水量など面白いデータもあったので詳しい天気予報を提供できると感じました。
##Goプログラム
まずはGoをインストールしましょう!
$ brew install go
インストール確認できているか確認するために
$ go version
go version go1.15 darwin/amd64
インストールできてますね!
ルート直下に/lambdaディレクトリを作成し、main.goファイルにプログラムを書いていきます。
各機能毎にコードを説明するので、全部のコードをみたい方はgithubをご覧ください。
##ライブラリのインストール
go get github.com/aws/aws-lambda-go/lambda
go get github.com/line/line-bot-sdk-go/linebot
main関数に項:AWS
のhandler:
で指定したメソッド名と同じにします。
そのメソッドの中に具体的なコードを記載していきます。
func main() {
lambda.Start(postLineMessage)
}
##LINEのMessagingAPI
まずはLINEのMessagingAPIについて
公式のサンプルを参考に作成しました!
https://github.com/line/line-bot-sdk-go
func postLineMessage() {
cityNameInfo, weatherForcastInfo, message := createWeatherForcast()
bot, err := linebot.New(os.Getenv("CHANNEL_SECRET"), os.Getenv("CHANNEL_TOKEN"))
if err != nil {
fmt.Println(err)
}
if _, err := bot.PushMessage(os.Getenv("USER_ID"), linebot.NewTextMessage(cityNameInfo), linebot.NewTextMessage(weatherForcastInfo), linebot.NewTextMessage(message)).Do(); err != nil {
fmt.Println(err)
}
}
Developersコンソールに遷移して、[チャネル設定]タブでチャネルを選択して、[チェネル基本設定]CHANNEL_SECRET
とUSER_ID
を確認でき、[Messaging API設定]タブでCHANNEL_TOKEN
を発行できます。
この情報が漏れてしまうと、よからぬ事態になってしまう可能性があるので、外部ファイルなどに切り出してGithubとかに公開しないようにしてください。
切り出した情報はos.Getenv
で環境変数を取得できるので、別ファイルから引っ張るようにしてください。
linebot.NewTextMessage()
で1つのプッシュメッセージが送れ、繋げることによって複数送れます。
###天気情報を取得
func getWeatherForcast() *Forcast {
//緯度
const LATITUDE = "35.465786"
//経度
const LONGITUDE = "139.622313"
base_url := "https://api.openweathermap.org/data/2.5/onecall"
url := fmt.Sprintf("%s?lat=%s&lon=%s&exclude=current&units=metric&lang=ja&appid=%s", base_url, LATITUDE, LONGITUDE, os.Getenv("API_FORCAST_KEY"))
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
var forcast Forcast
json.Unmarshal([]byte(body), &forcast)
return (&forcast)
}
使用するOne Call APIは緯度経度で設定した場所の天気情報を取得できます。
今回は横浜駅の緯度経度を指定してます。
Open Weather APIに遷移して、APIKEYを取得するために登録をします。ログインできたらAPI keys
タブがあるので、クリックすると自分のAPIKEYを取得することができます。
fmt.Sprintf()
を使用することで任意の型と文字列を1つの文字列にしてくれます。
http.NewRequest("GET", url, nil)
で、httpリクエストを生成して
http.DefaultClient.Do(req)
で実際にリクエストを送っています。
res.Body.Close()
で終わったことを宣言しましょう。宣言しないとKeep-Alive(デフォルトで有効)のためにTCPコネクションが再利用されないそうです。
ioutil.ReadAll(res.Body)
でファイルを読みこんでいます。
go のhttpリクエストの実装方は下記も参考にしてみてください!
Go net/httpパッケージの概要とHTTPクライアント実装例
##構造体にマッピング
###レスポンス内容(JSON)
{
"lat": 35.46,
"lon": 139.62,
"daily": [
{
...(略)...
},
]
}
###構造体
//一部抜粋
type Forcast struct {
Hour []Hour `json:"hourly"`
Day []Day `json:"daily"`
}
type Day struct {
Time int64 `json:"dt"`
Temp struct {
MinTemp float32 `json:"min"`
MaxTemp float32 `json:"max"`
} `json:"temp"`
Weather []Weather `json:"weather"`
}
APIで取得したJSONのデータは入れ子状態で配列もありましたので、構造体は少し複雑になっています。
JSON入れ子状態の場合は構造体の中に構造体を定義します。
配列の場合は新たに構造体を作成し、大元の構造体に配列で宣言するとマッピングされます。
json.Unmarshal([]byte(body), &forcast)
先ほど読み込んだデータを構造体にマッピングします。
json.Unmarshalは、構造体のjsonタグがあればその値を対応するフィールドにマッピングしてくれます。
該当フィールドが存在しなかったら、エラーは起きずマッピングされないだけなので、欲しい情報がマッピングされてない!ってなった際は、構造体が正しいか確認してください。
##ポインタ
goで重要な概念であり、自分が躓いた箇所です。
ポインタは「アドレス」を格納するための変数でああり、アドレスが保有するデータではないです。
アドレスが保有するデータにアクセスするためには「*」をつける必要があります。
&
を使うことで、ポインタ型を生成することができます。
func getWeatherForcast() *Forcast {
json.Unmarshal([]byte(body), &forcast)
return (&forcast)
}
ポインタに関してわかりやすく解説しているので、是非参考にしてください。
Goで学ぶポインタとアドレス
##データ加工
for _, list := range forcast.Hour {
//略
}
for ... range で配列の要素分繰り返してくれます。Javaでいう拡張for文に該当します。
今回はForcastの中にあるHourの配列を回して、1つずつの要素がlistに該当します。
func changeWeatherName(Weather *Weather) int {
hasUmbrella := 0
var weatherNameList map[string]string = map[string]string{"Clear": "晴れ", "Clouds": "曇り", "Rain": "雨", "Drizzle": "霧雨", "Thunderstorm": "雷雨", "Snow": "雪"}
for key, value := range weatherNameList {
if Weather.Info == key {
Weather.Info = value
if key == "Rain" || key == "Drizzle" || key == "Thunderstorm" || key == "Snow" {
hasUmbrella++
}
}
}
return hasUmbrella
}
天気名称が英語なので、日本語に変換します。
このアプリを作成した理由である、雨の日に傘を忘れてしまうことを解消したいので
雨の予報の場合はカウントして返却します。
本当はRain
やDrizzle
を配列にして、配列に要素が存在するか確認したかったのですが(Javaでいうcontains)、残念ながらGoにはないそうなので、止む無く論理和(||)で1つずつ検証しています。
現在は雨のデータが1件でもあれば傘を持っていくようにメッセージを送っているが、雨データの件数や時間帯でメッセージを変更してもいいと思います。
forcastInfo = fmt.Sprintf("%d:%s,気温は%s˚C", time.Hour(), list.Weather[0].Info, fmt.Sprintf("%.1f", list.Temp))
項:天気情報を取得
でもfmt.Sprintf()は使用しましたが、その際は文字列だけの結合でした。
今回は整数や小数も扱い型によって表記の仕方が異なります。
%s ⇨ 文字列
%d ⇨ 整数
fmt.Sprintf("%.1f", list.Temp)では小数点第2位を四捨五入して、第1位までを文字列として変換してくれます。文字列にするので、大元のfmt.Sprintf()では%sで結合してます。
他にも表記があるので、参考にしてみてください。
fmt.Printfなんかこわくない
##出来上がったファイルをコンパイル
GOOS=linux GOARCH=amd64 go build -o postLineMessage
main.goをコンパイルして、バイナリを生成します。
その際にバイナリの種類を決定します。
GOOS=linux GOARCH=amd64
がバイナリ種類の指定です。
他のバイナリの種類については下記を参照してください。
Go のクロスコンパイル環境構築
buildするとファイル名はデフォルトで元のファイル名を元に決められます。
build後のファイル名を決めたい場合には、-oをつけます。
ファイル名とlambda.Start()で呼び出しているハンドラ名は同じにします。
##デプロイ
cdk deploy
デプロイ成功しているか、AWSコンソール上で確認してみましょう!
コンソールにログインしたら、サービスタブからLambdaと検索してデータが存在していますか?
存在していたら、デプロイしたデータの詳細に遷移してテストをしてみましょう!
テストイベントの選択タブをクリックし、イベント名をつけて作成ボタンをクリックします。
(テストコードは修正しなくて大丈夫です。)
そして、テストボタンをクリックすると、無事ラインにメッセージが送られます!
なぜか期待の結果を得られることができませんでした。。。
day := time.Now()
if day == time.Day(){
//略
}
取得した情報をif文でフィルタリングしているのですが、
原因は項:AWS
でCloudWatch EventsのscheduleでUTCの時間で指定するので、22時30分時点で関数が実行されてしまい、取得したい前日のデータが対象となってしまいました。
なので、条件を1日進ませ、7時からの1日の情報が取得できるように変更しました。
day := time.Now().AddDate(0, 0, 1).Day()
if day == time.Day() && time.Hour() > 6 {
//略
}
##今度こそ
フォーマットや欲しい情報の改善の余地はありますが、無事期待通りの情報を取得することができました!
##作成してみて
元々Javaで半年ほどプログラムを書いていましたが、他のサーバーサイドの言語を勉強するのは初めてでしたが、1週間ほどで作成することができました。ファイルの分割だったり、エラーハンドリングなどまだまだ学ぶことは多いものの1つの言語を習得できれば、他の言語を学ぶ際も抵抗なく学べるとわかりました。
またawsもLINEBOTも初めて使用しましたが、公式ドキュメントや色々な記事がありましたので、躓いても解決することができました。
##記事初投稿を終えて
記事を書くのは思ったより、10倍ぐらい大変でした笑
公開するにあたって、使用している知識が正しいのか調べながら初学者でもわかりやすくしようと思ったので時間がかかってしまいました。でも、何気なく使った知識もちゃんと理解できたし知識の整理にもなったので、記事を書くメリットはあると思いました。
また、他の記事投稿者の偉大さがしみじみと感じました。このアプリを作成するにあたっても色々な記事を参考にしましたし、記事を書く際にも大変お世話になりました。
ありがとうございました。
間違い、ご意見等あればご連絡頂けると嬉しいです!
##今後の展望
今回毎朝にその日の天気予報を送信するラインボットを作成しましたが、ユーザーから地名を入力されたらその地名の天気予報を返信したり,1週間を入力したら週間天気予報を返信するような機能も追加したいと思います。