S3 + Lambda + API Gatewayで プライム市場銘柄ランダムピックアプリ作ってみた vol.1~goでLambda関数作成~

tech article

最近株式投資に興味が出てきています。
毎週土曜にクイズ☆プライム市場名鑑というポッドキャストで雑談を交えた企業分析をしているので聞いてくれると嬉しいです。
話し相手になってくれる方も募集中です!
Twitterとかで連絡待ってます。

で、毎週企業を探すときにどれにしようかな~と迷ってしまうのでランダムピックアプリを作成しようと思い立ちました。
goの勉強もしてみたかったので言語はgoにしました。

S3で静的サイトをホストして、ボタンをクリックしたらプライム市場の企業情報が返ってくるようなアプリを作成しました。
アプリはこちら。

S3で静的サイトをホストして、ボタンをクリックしたらプライム市場の企業情報が返ってくるようなアプリを作成しました。
アプリはこちら
中身はシンプルでこんな感じです。

処理に10秒ほどかかってしまいます。
次の課題としてはもっと早く表示できるようにしたいですね。

上場企業の証券コードリストを取得

現在上場している企業のリストを東証のサイトから取得します。

今回はプライム市場に対象を絞ります。

jQuantsに登録

上場企業の情報はJ-Quants APIを使って取得します。
有料プランもありますが、今回は簡単な情報が分かればいいので無料プランで登録します。
API仕様はこちら

関数作成の事前準備

githubのcodespacesを使って開発を進めていきます。
devcontainerの設定など、詳しくは以下をご覧ください。

GitHub - nasubi3-sleep/random-pick-listed-company: ランダムに上場企業をピックするアプリケーション
ランダムに上場企業をピックするアプリケーション. Contribute to nasubi3-sleep/random-pick-listed-company development by creating an account on GitHub.

関数の説明をする前に、まずgoを動かすのに必要な準備をします。
goを動かすには、Goモジュールの有効化が必要です。

Go ModulesはGoの公式の依存管理ツールです。従来の GOPATH に依存しない形でプロジェクトごとに依存関係を管理できます。
go mod init <project> を実行すると、カレントディレクトリ内にgo.mod ファイルが作成されます。このgo.modファイルに依存関係が記録されていきます。

go get <package>を実行するとパッケージが追加され、go modに追記されます。

こうすることでgo build使ってビルドしたりgo runで関数を実行したりできるようになります。

import

関数の全体像はこちらです。(折りたたんでます。)

go.mod
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"os"
	"time"
	"github.com/aws/aws-lambda-go/lambda"
)

// 認証用構造体
type AuthRequest struct {
	MailAddress string `json:"mailaddress"`
	Password    string `json:"password"`
}

type AuthResponse struct {
	RefreshToken string `json:"refreshToken"`
}

type IDTokenResponse struct {
	IDToken string `json:"idToken"`
}

// J-Quants API用構造体
type JQuantsResponse struct {
	Info []struct {
		Code             string `json:"Code"`
		CompanyName      string `json:"CompanyName"`
		MarketCodeName   string `json:"MarketCodeName"`
		Sector33CodeName string `json:"Sector33CodeName"`
	} `json:"info"`
}

type Event struct{}

// リフレッシュトークンを取得
func GetRefreshToken(email, password string) (string, error) {
	url := "https://api.jquants.com/v1/token/auth_user"
	requestBody := AuthRequest{
		MailAddress: email,
		Password:    password,
	}

	jsonData, err := json.Marshal(requestBody)
	if err != nil {
		return "", fmt.Errorf("failed to marshal request data: %v", err)
	}

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to send request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to authenticate: status code %d", resp.StatusCode)
	}

	var authResponse AuthResponse
	err = json.NewDecoder(resp.Body).Decode(&authResponse)
	if err != nil {
		return "", fmt.Errorf("failed to parse response: %v", err)
	}

	return authResponse.RefreshToken, nil
}

// IDトークンを取得
func GetIDToken(refreshToken string) (string, error) {
	url := fmt.Sprintf("https://api.jquants.com/v1/token/auth_refresh?refreshtoken=%s", refreshToken)

	resp, err := http.Post(url, "application/json", nil)
	if err != nil {
		return "", fmt.Errorf("failed to send request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get ID token: status code %d", resp.StatusCode)
	}

	var idTokenResponse IDTokenResponse
	err = json.NewDecoder(resp.Body).Decode(&idTokenResponse)
	if err != nil {
		return "", fmt.Errorf("failed to parse response: %v", err)
	}

	return idTokenResponse.IDToken, nil
}

// J-Quants APIから銘柄情報を取得
func FetchCompanyInfo(code string, idToken string) (JQuantsResponse, error) {
	apiURL := "https://api.jquants.com/v1/listed/info"
	// APIリクエストの作成	
	req, err := http.NewRequest("GET", apiURL, nil)
	if err != nil {
		return JQuantsResponse{}, fmt.Errorf("failed to create request: %v", err)
	}
	// クエリパラメータとヘッダーを設定
	q := req.URL.Query()
	q.Add("code", code)
	req.URL.RawQuery = q.Encode()
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", idToken))

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return JQuantsResponse{}, fmt.Errorf("failed to fetch data: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return JQuantsResponse{}, fmt.Errorf("failed to fetch data: status code %d", resp.StatusCode)
	}

	var result JQuantsResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return JQuantsResponse{}, fmt.Errorf("failed to parse response: %v", err)
	}

	return result, nil
}

// ラムダハンドラー
func handler(ctx context.Context, event Event) (interface{}, error) {
	// 環境変数から認証情報を取得
	email := os.Getenv("JQUANTS_EMAIL")
	password := os.Getenv("JQUANTS_PASSWORD")

	if email == "" || password == "" {
		return "", fmt.Errorf("email or password not set in environment variables")
	}

	// リフレッシュトークンを取得
	refreshToken, err := GetRefreshToken(email, password)
	if err != nil {
		return "", fmt.Errorf("failed to get refresh token: %v", err)
	}

	// IDトークンを取得
	idToken, err := GetIDToken(refreshToken)
	if err != nil {
		return "", fmt.Errorf("failed to get ID token: %v", err)
	}

	// 銘柄コードリスト
	stockCodes := []string{"1773", 
	"1301", 
	"1332", 	
  ...(多いので省略),
  "9991", 
	"9997"}

	// ランダムに銘柄コードを選択
	rand.Seed(time.Now().UnixNano())
	randomIndex := rand.Intn(len(stockCodes))
	randomCode := stockCodes[randomIndex]

	// J-Quants APIを呼び出して銘柄情報を取得
	info, err := FetchCompanyInfo(randomCode, idToken)
	if err != nil {
		return "", fmt.Errorf("failed to fetch company info: %v", err)
	}

	// 結果をフォーマットして返す
	if len(info.Info) > 0 {
		company := info.Info[0]
		return company, nil
	}

	return nil, fmt.Errorf("no company information found for code: %s", randomCode)
}

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

細かく見ていきます。

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"os"
	"time"
	"github.com/aws/aws-lambda-go/lambda"
)

Goのプログラムは package main から始まります。
これは エントリーポイント(実行可能プログラム) であることを示します。
ライブラリを作る場合はpackage utilsにするなどしてmainとは別の言葉を使用します。

importでは、必要なライブラリをインポートします。
外部ライブラリはgo get <Third-party Libraries> でgo.modに追加され、外部ライブラリがプロジェクトに追加されます。

構造体(struct)

// 認証用構造体
type AuthRequest struct {
	MailAddress string `json:"mailaddress"`
	Password    string `json:"password"`
}

type AuthResponse struct {
	RefreshToken string `json:"refreshToken"`
}

type IDTokenResponse struct {
	IDToken string `json:"idToken"`
}

// J-Quants API用構造体
type JQuantsResponse struct {
	Info []struct {
		Code             string `json:"Code"`
		CompanyName      string `json:"CompanyName"`
		MarketCodeName   string `json:"MarketCodeName"`
		Sector33CodeName string `json:"Sector33CodeName"`
	} `json:"info"`
}

type Event struct{}

TypeScriptでいうtypeとかinterfaceみたいなもの。
structはメソッドを持てるのでclass的な役割も果たす。

JSONタグ(json:"mailaddress"など)をつけておくと、APIに送信する際に設定した値をキーとして、JSONに変換してくれるらしい。

リフレッシュトークン取得処理

// リフレッシュトークンを取得
func GetRefreshToken(email, password string) (string, error) {
	url := "https://api.jquants.com/v1/token/auth_user"
	requestBody := AuthRequest{
		MailAddress: email,
		Password:    password,
	}

	jsonData, err := json.Marshal(requestBody)
	if err != nil {
		return "", fmt.Errorf("failed to marshal request data: %v", err)
	}

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to send request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to authenticate: status code %d", resp.StatusCode)
	}

	var authResponse AuthResponse
	err = json.NewDecoder(resp.Body).Decode(&authResponse)
	if err != nil {
		return "", fmt.Errorf("failed to parse response: %v", err)
	}

	return authResponse.RefreshToken, nil
}

戻り値の定義

func GetRefreshToken(email, password string) (string, error) {

成功したらstring, 失敗したらerrorを返す
……と思いきゃ常にどちらも返すらしい。

セイウチ演算子

url := "https://api.jquants.com/v1/token/auth_user"

:= を使うことで、goが型を自動推論して変数を宣言してくれるらしい。
その見た目から、セイウチ演算子と呼ばれている。(ナイス命名)

関数の外では使えない・再代入では使えないという制限があります。
ほかの言語ではnullとして使われるやつは、goではnilになるらしいです。

戻り値の定義再び

		return "", fmt.Errorf("failed to marshal request data: %v", err)

fmt.Errorf(string, any)として使うのかな?
%vにはanyで入れた変数の値を出力するっぽい。anyの型は何でもOKみたい。

なんで空文字返してんの?って思ったけど、最初に出てきた(string, error)がstringもerrorも返す、って意味やから、returnでは常にどちらも返さないといけない。
納得。

defer

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("failed to send request: %v", err)
	}
	defer resp.Body.Close()

deferを使うことで、どこでreturnが発生しても最終的にcloseできるらしい。
deferは延期する、遅らせる、という意味。
関数の終了時まで処理を延期する、という意味で使われている。

IDトークン取得処理

// IDトークンを取得
func GetIDToken(refreshToken string) (string, error) {
	url := fmt.Sprintf("https://api.jquants.com/v1/token/auth_refresh?refreshtoken=%s", refreshToken)

	resp, err := http.Post(url, "application/json", nil)
	if err != nil {
		return "", fmt.Errorf("failed to send request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get ID token: status code %d", resp.StatusCode)
	}

	var idTokenResponse IDTokenResponse
	err = json.NewDecoder(resp.Body).Decode(&idTokenResponse)
	if err != nil {
		return "", fmt.Errorf("failed to parse response: %v", err)
	}

	return idTokenResponse.IDToken, nil
}

値の格納

	var idTokenResponse IDTokenResponse
	err = json.NewDecoder(resp.Body).Decode(&idTokenResponse)
	if err != nil {
		return "", fmt.Errorf("failed to parse response: %v", err)
	}

	return idTokenResponse.IDToken, nil
}

これどこで値格納してんの?って思ったけど、json.NewDecoder(resp.Body).Decode(&idTokenResponse)
でidTokenResponseに値を格納しているみたい。
errorが発生したらerrにその内容が格納される、errorが発生しなければerrは空。

銘柄情報取得処理

// J-Quants APIから銘柄情報を取得
func FetchCompanyInfo(code string, idToken string) (JQuantsResponse, error) {
	apiURL := "https://api.jquants.com/v1/listed/info"
	// APIリクエストの作成	
	req, err := http.NewRequest("GET", apiURL, nil)
	if err != nil {
		return JQuantsResponse{}, fmt.Errorf("failed to create request: %v", err)
	}
	// クエリパラメータとヘッダーを設定
	q := req.URL.Query()
	q.Add("code", code)
	req.URL.RawQuery = q.Encode()
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", idToken))

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return JQuantsResponse{}, fmt.Errorf("failed to fetch data: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return JQuantsResponse{}, fmt.Errorf("failed to fetch data: status code %d", resp.StatusCode)
	}

	var result JQuantsResponse
	err = json.NewDecoder(resp.Body).Decode(&result)
	if err != nil {
		return JQuantsResponse{}, fmt.Errorf("failed to parse response: %v", err)
	}

	return result, nil
}

クエリパラメータとヘッダーの設定

	// クエリパラメータとヘッダーを設定
	q := req.URL.Query()
	q.Add("code", code)
	req.URL.RawQuery = q.Encode()
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", idToken))

req.URL.Query() で URL にクエリパラメータを追加するための変数qを作成します。
q.Add("code", code)code(銘柄コード)クエリを追加します。
req.URL.RawQuery = q.Encode() で パラメータを URL にセットします。
req.Header.Set(“Authorization”, “Bearer “) でリクエストにヘッダーをセットします。

クライアント作成

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return JQuantsResponse{}, fmt.Errorf("failed to fetch data: %v", err)
	}
	defer resp.Body.Close()

Go で HTTP リクエストを送るとき、http.Client を使うとリクエストの送信を管理できる

client := &http.Client{} でクライアント(サーバーと通信できるインスタンス)を作成する。
&はアドレス(ポインタ)を取得しているらしい。
ポインタを渡すとHTTPセッションやTCP接続の管理が効率的になるらしい。
ポインタを使うと、毎回 client をコピーせずに同じ http.Client インスタンスを使えるので便利!らしい。
ちょっと難しい。
多分ポインタじゃない場合は中身をコピーしていて、ポインタの場合は同じ中身のものを使ってるんやろうな。
多分参照渡しと値渡しの話やんな。
これマジで理解できる気がしない。
純文学よりも難しい。

resp, err := client.Do(req)生成したクライアント(インスタンス)に対して処理を実行させている。

なんでほかの関数でもHTTP通信してるのに出てきてないの?と思った。
いままで出てきてなかったのはhttp.Postが内部的にクライアントインスタンスを作成していたからだった。
今回はhttp.NewRequestを使用してカスタマイズしたリクエストを作成する必要があり、その場合は別途クライアントを作成し、client.Do(req) でリクエストを送信しないといけなかった。

Lambda handler

// ラムダハンドラー
func handler(ctx context.Context, event Event) (interface{}, error) {
	// 環境変数から認証情報を取得
	email := os.Getenv("JQUANTS_EMAIL")
	password := os.Getenv("JQUANTS_PASSWORD")

	if email == "" || password == "" {
		return "", fmt.Errorf("email or password not set in environment variables")
	}

	// リフレッシュトークンを取得
	refreshToken, err := GetRefreshToken(email, password)
	if err != nil {
		return "", fmt.Errorf("failed to get refresh token: %v", err)
	}

	// IDトークンを取得
	idToken, err := GetIDToken(refreshToken)
	if err != nil {
		return "", fmt.Errorf("failed to get ID token: %v", err)
	}

	// 銘柄コードリスト
	stockCodes := []string{"1773", 
	"1301", 
	"1332", 	
  ...(多いので省略),
  "9991", 
	"9997"}

	// ランダムに銘柄コードを選択
	rand.Seed(time.Now().UnixNano())
	randomIndex := rand.Intn(len(stockCodes))
	randomCode := stockCodes[randomIndex]

	// J-Quants APIを呼び出して銘柄情報を取得
	info, err := FetchCompanyInfo(randomCode, idToken)
	if err != nil {
		return "", fmt.Errorf("failed to fetch company info: %v", err)
	}

	// 結果をフォーマットして返す
	if len(info.Info) > 0 {
		company := info.Info[0]
		return company, nil
	}

	return nil, fmt.Errorf("no company information found for code: %s", randomCode)
}

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

Lambda実行

func handler(ctx context.Context, event Event) (interface{}, error) {

これはLambda実行のおまじない(見ていくと沼になりそうなので今回はスキップ)

ランダムピックアップ処理

	// ランダムに銘柄コードを選択
	rand.Seed(time.Now().UnixNano())
	randomIndex := rand.Intn(len(stockCodes))
	randomCode := stockCodes[randomIndex]

乱数の種を現在時刻をもとにして毎回設定している。
そのおかげでrand.Intn()で出力される値が毎回ランダムになる。

これで関数の作成は完了。

Lambdaで動かす

どうやら、現在のLambdaではgoをLambda上でコーディングしてそのままデプロイって形で使えないらしい。

Pythonみたいにその場で書いてテストしてデプロイみたいなことできると思ってた。
めっちゃやりづらかった……

で、どうするかというとLambda作成時にランタイムでAmazon Linux2023を選択します。(provided.al2023とか公式に書いてるけど何のことかよくわかってない。)

で、つくった関数をビルドします。

OS=linux GOARCH=amd64 go build -o bootstrap main.go

Amazon Linux2023(provided.al2023)で動かす場合は、bootstrap という名前の実行ファイルがエントリーポイントとなるらしいのでファイル名をbootstrapにします。

zip function.zip bootstrap

Lambdaにはzip形式でアップロードするのでzip形式に圧縮します。

あとはLambdaで必要な環境変数(JQUANTS_EMAIL、JQUANTS_PASSWORD)を設定して、zipファイルをアップロードすればOKです。

環境変数の設定

アップロード

テストが成功しました!
イベントJSONは空のものでOKです。

最後にもう一度宣伝

ここまで読んでくれたあなたはきっと真剣に読んでくださった優しい方なので、ぜひクイズ☆プライム市場名鑑聴いてください!

聴くの面倒なら、感想や記事の間違いの指摘、全く関係ない雑談でもよいのでメッセージください!
Twitterとかで連絡待ってます。
フォローしてくれるだけでも嬉しいです!

タイトルとURLをコピーしました