- 投稿日:2020-08-03T19:47:37+09:00
AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた
この記事について
Developers.IO 2020のサーバーレスセッションに触発されました。
[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020というわけで、Golangを用いてAWSで基本的なサーバーレスをやってみたその手順をまとめました。
具体的には以下の手順を紹介します。
- GolangでLambdaを動かしAPI Gatewayと連携させる
- LambdaとDynamoDBと連携してAPIを作る
使用する環境・バージョン
- OS : macOS Mojave 10.14.5
- Golang : version go1.14 darwin/amd64
読者に求める前提知識
Golangの基本的な文法がわかること。
Lambda関数の作成
コンソールで関数を作成
AWS Lambdaのコンソールを開くと、以下のような画面になります。
右上にある「関数の作成」ボタンをクリックします。
すると、以下のような関数作成画面に遷移します。
「一から作成」を選択し、関数名・ランタイムを記入します。今回は以下のように設定しました。
- 関数名: 好きな名前を入力(今回はmyTestFunction)
- ランタイム: Go1.x
次にLambda関数のアクセス権限の設定をします。
「実行ロールの選択または作成」のプルダウンを開くと、以下のようなフォームが表れます。
今回Lambdaを動かすのは初めてなので、「基本的なLambdaアクセス権限で新しいロールを作成」を選択します。
これで、Lambda関数作成時に、関数のログをCloudWatchに出力するためのロールが作られます。ここまでの入力が終わったら、関数を作成します。
正常に作成されたら、以下のような画面になります。
補足: このとき関数と同時に作られたロールとCloudWatchのロググループは、このLambda関数を削除しても消されず残ったままになります。つまり、ロール・ロググループの削除は、Lambda関数の削除とは別に手動で行う必要があるということです。関数のコードを作成
作ったばかりの関数の中身は「hello,world」を返すだけのデフォルト状態なので、これからLambda上で動かしたいプログラムを別に書いてやる必要があります。
今は手始めに「httpリクエストを受けたら、httpメソッド・リクエストボディ・パスパラメータ・クエリパラメータをjsonにして返す」という関数を作成してみます。
まずは、ローカルに必要なライブラリをインストールします。
go get -u github.com/aws/aws-lambda-go/lambda go get -u github.com/aws/aws-lambda-go/eventsインストールしたら、コードを書いていきます。
hello.gopackage main import ( "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) type Response struct { RequestMethod string `json:RequestMethod` RequestBody string `json:"RequestBody` PathParameter string `json:"PathParameter"` QueryParameter string `json:"QueryParameter"` } func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // httpリクエストの情報を取得 method := request.HTTPMethod body := request.Body pathParam := request.PathParameters["pathparam"] queryParam := request.QueryStringParameters["queryparam"] // レスポンスとして返すjson文字列を作る res := Response{ RequestMethod: method, RequestBody: body, PathParameter: pathParam, QueryParameter: queryParam, } jsonBytes, _ := json.Marshal(res) // 返り値としてレスポンスを返す return events.APIGatewayProxyResponse{ Body: string(jsonBytes), StatusCode: 200, }, nil } func main() { lambda.Start(handler) }参考:AWS公式ドキュメント Go の AWS Lambda 関数ハンドラー
参考:Go+Lambdaで最速サーバレスチュートリアルここで、コードについていくつか解説します。
main関数について
Lambdaで実行されるのはmain関数です。そのため、Lambdaにアップロードするコードには必ずmain関数を用意してやる必要があります。
今回は、handler
というAPIハンドラ(関数)を起動する操作をmain関数に書きました。API Gatewayからhttpリクエストの情報を取得する方法
ハンドラは
events.APIGatewayProxyRequest
型の変数request
を引数にとっています。この変数request
の中に、どのようなhttpリクエストを受け取ったかの情報が格納されています。
例えば、今回の場合は以下のように情報を取得しています。
- リクエストメソッド:
request.HTTPMethod
で取得- リクエストボディ:
request.Body
で取得- パスパラメータ(
/{pathparam}
とAPI Gatewayで設定した部分):equest.PathParameters["pathparam"]
で取得- クエリパラメータ(
/?queryparam=
の部分):request.QueryStringParameters["queryparam"]
で取得
events.APIGatewayProxyRequest
型の定義をGo Docで確認すると、他にもどのようなフィールドがあるのかがわかります。やりたい処理に合わせて活用すればよいでしょう。type APIGatewayProxyRequest struct { Resource string `json:"resource"` // The resource path defined in API Gateway Path string `json:"path"` // The url path for the caller HTTPMethod string `json:"httpMethod"` Headers map[string]string `json:"headers"` MultiValueHeaders map[string][]string `json:"multiValueHeaders"` QueryStringParameters map[string]string `json:"queryStringParameters"` MultiValueQueryStringParameters map[string][]string `json:"multiValueQueryStringParameters"` PathParameters map[string]string `json:"pathParameters"` StageVariables map[string]string `json:"stageVariables"` RequestContext APIGatewayProxyRequestContext `json:"requestContext"` Body string `json:"body"` IsBase64Encoded bool `json:"isBase64Encoded,omitempty"` }参考:GoDoc package aws/aws-lambda-go/events
API Gatewayにレスポンスを返す方法
ハンドラは返り値に
events.APIGatewayProxyResponse
型をとります。なので、所望のレスポンス内容に沿ったこの型の変数を作成するのが、ハンドラ内で行う処理内容です。
events.APIGatewayProxyResponse
型の定義は以下のようになっています。type APIGatewayProxyResponse struct { StatusCode int `json:"statusCode"` Headers map[string]string `json:"headers"` MultiValueHeaders map[string][]string `json:"multiValueHeaders"` Body string `json:"body"` IsBase64Encoded bool `json:"isBase64Encoded,omitempty"` }参考:GoDoc package aws/aws-lambda-go/events
今回の場合は、以下のようにレスポンスを作っています。
- StatusCode: httpレスポンスコード200を指定
- Body: 自作の構造体(
Response
)からjson.Marshal
→string
と変換コードをLambdaにアップロード
Lambdaにアップロードするのはコンパイル済みの実行ファイルである必要があるので、上で書いた
hello.go
をビルドしてバイナリファイルhello
を作ります。
また、アップロードの形式がzipファイルなので、ビルド後にhello
をzip圧縮します。$ GOOS=linux GOARCH=amd64 go build -o hello hello.go $ zip hello.zip hello先ほど作った
myTestFunction
関数をLambdaコンソールで開き、以下の設定画面からhello.zip
をアップロードします。
注意:アップロードする実行ファイルの名前
「一から作成」のオプションから作成したLambda関数に渡す実行ファイルの名前は、必ず
hello
である必要があります。
これは一から作成のLambda関数がデフォルトで「hello
という名前のバイナリファイルを実行する」という設定になっているため、他の名前だと以下のようなPathErrorが起きます。{ “errorMessage”: “fork/exec /var/task/binaryname: permission denied”, “errorType”: “PathError” }Lambdaのテスト
Lambda関数は、Webコンソール上でテストを実行することができます。
コンソール上で、右上の「テスト」のボタンをクリックします。
すると、テストリクエストを編集する画面が表れます。
デフォルトだとこのような状態です。このまま名前をつけて保存します。
この状態で「テスト」ボタンをクリックすると、テストが実行・結果が表示されます。
きちんとステータスコード200が返ってくることが確認できました。補足: このテストはAPI Gateway経由のリクエストを送っているわけではないので、httpメソッドやボディなどのリクエスト情報が空のときの結果が表示されています。
API Gatewayと連携
APIの作成
開始画面から、「今すぐはじめる」ボタンをクリックします。
すると、以下のようなAPI作成画面になります。
以下のような設定を入力して作成します。
- プロトコル: REST
- 新しいAPIの作成: 新しいAPI
- API名: 好きな名前をつける
- 説明: 好きな説明文を書く
- エンドポイントタイプ: リージョン
APIを作成したら、「どのパス・どのメソッドにどの処理を結びつけるか」の設定を行う画面が表示されます。URLリソースの作成
まずは、URLリソースの作成を行います。「アクション」→「リソースの作成」を選択します。
以下のような、パスパラメータの名前等を設定する画面になります。
- プロキシリソース: なし
- リソース名: 好きな名前をつける
- リソースパス: {pathparam}
- API Gateway CORS : なし
以上の設定でリソースを作成します。
メソッドの作成
次に、httpリクエストメソッドーLambda関数の紐付けを行います。
/{pathparam}
を選択した状態で、「アクション」→「メソッドの作成」を選択します。
プルダウンから、設定を行いたいリクエストメソッドを選択します。GETやPOSTなどの特定メソッドの選択はもちろん、全てのメソッドに対しての設定を行いたい場合はANYという選択肢もあります。今回はANYを選択します。
メソッドを選択したら、その選択メソッドにどんな処理(Lambda関数)を紐づけるのかの設定画面が表示されます。
- 統合タイプ: Lambda関数
- Lambdaプロキシ統合の使用: あり
- Lambdaリージョン: ap-northeast-1
- Lambda関数: myTestFunction
- デフォルトタイムアウトの使用: あり
以上の設定で保存をクリックすると、以下のような確認画面が出ます。
問題ないので、OKを選択します。結果は以下の通り。
テストの実行
上の画面で「テスト⚡️」をクリックすることで、API Gatewayからリクエストを送ったときにきちんと動くかどうかのテストを実行することができます。
このように、パスパラメータ・クエリパラメータ・リクエストメソッド・ボディなどを自由にコンソール上で設定して、それに対してどのような応答が返ってくるのかを確認することができます。APIのデプロイ
実際にAPIをデプロイするには、「アクション」→「APIのデプロイ」を選択します。
すると、デプロイステージを指定する画面になります。
ステージを指定して「デプロイ」ボタンを押すと、以下のような画面に遷移します。
ここで、APIが公開されているURLを確認することができます。
参考:【AWS】API Gateway + LambdaでAPIをつくる
DynamoDBと連携
ここからは、[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020で紹介された、以下のようなCRUDを行うDB連携APIを新しく作っていきます。
テーブル作成
- テーブル名: 好きな名前(ここではuser)
- プライマリーキー: userid(数値)
- ソートキー: 追加しない
- テーブル設定: デフォルト設定の使用
補足: プライマリーキーに設定できるデータ型は文字列orバイナリor数値です。
データ項目の追加
作成直後は、プライマリーキー以外の属性が存在しないので、他のキーが欲しいのならば手動で追加する必要があります。
テーブルの項目タブを開きます。
「項目の作成」ボタンをクリックすると、以下のような項目編集画面が表示されます。
+をクリックします。
Appendを選択します。
すると、どういう型のフィールドを追加するかを選択できます。
今回はStringを選択しました。
すると、String型のフィールドが追加されました。好きにフィールド名をつけたり、値も設定を行ったりします。
項目の追加を複数回行い、最終的にこうなりました。これで保存します。
無事にプライマリーキー以外の項目が作成されたことが確認できます。参考:初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~
Lambda用のロールを作成
Lambda関数がDynamoDBにアクセスできるように、Lambda関数に付与するロールを作成します。
IAMコンソール→ロールから作成画面を開きます。
AWSサービス、Lambdaを選択します。
そして、以下のアクセス権限を追加して、ロールを作成します。
- AmazonDynamoDBFullAccess
- AWSLambdaDynamoDBExecutionRole
DynamoDBとの接続を必要とするLambda関数には、今作ったロールを付加します。
参考:初めての、LambdaとDynamoDBを使ったAPI開発
Lambda関数のコードを作成
いよいよDBにアクセスしてCRUD操作を行う関数コードを書いていきます。
まず、必要なパッケージをダウンロードします。
$ go get -u github.com/aws/aws-sdk-go/aws $ go get -u github.com/aws/aws-sdk-go/aws/session $ go get -u github.com/aws/aws-sdk-go/service/dynamodbCreate(POST)の作成
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" ) // Item DBに入れるデータ type Item struct { UserID int `dynamodbav:"userid" json:userid` Address string `dynamodbav:"address" json:address` Email string `dynamodbav:"email" json:email` Gender string `dynamodbav:"gender" json:gender` Name string `dynamodbav:"name" json:name` } // Response Lambdaが返答するデータ type Response struct { RequestMethod string `json:RequestMethod` Result Item `json:Result` } func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { method := request.HTTPMethod // DBと接続するセッションを作る→DB接続 sess, err := session.NewSession() if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } db := dynamodb.New(sess) // リクエストボディのjsonから、Item構造体(DB用データの構造体)を作成 reqBody := request.Body resBodyJSONBytes := ([]byte)(reqBody) item := Item{} if err := json.Unmarshal(resBodyJSONBytes, &item); err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } // Item構造体から、inputするデータを用意 inputAV, err := dynamodbattribute.MarshalMap(item) if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } input := &dynamodb.PutItemInput{ TableName: aws.String("user"), Item: inputAV, } // insert実行 _, err = db.PutItem(input) if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } // httpレスポンス作成 res := Response{ RequestMethod: method, } jsonBytes, _ := json.Marshal(res) return events.APIGatewayProxyResponse{ Body: string(jsonBytes), StatusCode: 200, }, nil } func main() { lambda.Start(handler) }ここでやっていることは以下の操作です。
- DBに接続
- リクエストボディからItem構造体を作る
- Item構造体から、DBにinsertするためのデータを作る
- insertを実行する
- レスポンスを作成
1. DBに接続
以下のコードが該当します。
sess, err := session.NewSession() if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } db := dynamodb.New(sess)これは、DynamoDBに接続する処理を行うときには必ず必要な定型句といってもいいでしょう。
2. リクエストボディからItem構造体を作る
httpリクエストボディに、DBに挿入したいデータがjson形式で格納されています。
example-requestbody.json{ "userid": 2, "address": "Osaka", "email": "bbb.jp", "gender": "F", "name": "Nancy" }
request.Body
で得られるリクエストボディはstring型なので、これをjson.Unmarshal
でパースして構造体形式(上だとItem構造体)に変換することで、ボディに格納されているデータを扱えるようにします。3. Item構造体から、DBにinsertするためのデータを作る
DynamoDBにデータを挿入する関数は、
db.PutItem()
です。func (c *DynamoDB) PutItem(input *PutItemInput) (*PutItemOutput, error)参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#DynamoDB.PutItem
しかし、見ての通りこの関数の引数はdynamodb.PutItemInput
型なので、Item型をそのままDBに渡すことはできません。そのため、Item型をdynamodb.PutItemInput
型に変換する必要があります。
dynamodb.PutItemInput
型の定義を見てみましょう。type PutItemInput struct { // 今回関係ないフィールドを省略 Item map[string]*AttributeValue `type:"map" required:"true"` TableName *string `min:"3" type:"string" required:"true"` }参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#PutItemInput
TableName
は、データを追加したDynamoDBのテーブル名を指定するフィールドです。
追加するデータの内容を入れるフィールドは、map[string]*AttributeValue
型のItem
です。つまり、2でリクエストボディから作ったItem
型構造体を、このmap[string]*AttributeValue
型に変換してやる必要があるわけです。まさに、この変換を行う関数が公式から提供されています。
dynamodbattribute.MarshalMap
という関数です。// Item型のitem変数を、map[string]*AttributeValue型のimputAVに変換 inputAV, err := dynamodbattribute.MarshalMap(item)この変換を正しく行うためには、Item型構造体に指定のメタタグをつける必要があります。
(json.Unmarshal
でjsonを構造体にパースするために、構造体にjsonタグをつけたのと同じ論理です)
ここで、Item型を以下のように定義していました。type Item struct { UserID int `dynamodbav:"userid" json:userid` Address string `dynamodbav:"address" json:address` Email string `dynamodbav:"email" json:email` Gender string `dynamodbav:"gender" json:gender` Name string `dynamodbav:"name" json:name` }この構造体の各フィールドにつけている
dynamodbav
タグは、「各フィールドがDynamoDBのどのキーに対応しているか」ということを示しています。例えば、UserIDフィールドは、DynamoDBではuseridキーに紐づいています。
dynamodbattribute.MarshalMap
を実行するためには、各フィールドにこのdynamodbav
タグを確実につけましょう。4. insertを実行する
db.PutItem()
を実行します。5. レスポンスを作成
APIを作成したときと要領は同じなので割愛します。
Read(GET)の作成
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" ) // Item構造体とResponse構造体は、Createのときと同じなので割愛 func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { method := request.HTTPMethod pathparam := request.PathParameters["userid"] // DB接続 sess, err := session.NewSession() if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } db := dynamodb.New(sess) // 検索条件を用意 getParam := &dynamodb.GetItemInput{ TableName: aws.String("user"), Key: map[string]*dynamodb.AttributeValue{ "userid": { N: aws.String(pathparam), }, }, } // 検索 result, err := db.GetItem(getParam) if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 404, }, err } // 結果を構造体にパース item := Item{} err = dynamodbattribute.UnmarshalMap(result.Item, &item) if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } // httpレスポンス作成 res := Response{ RequestMethod: method, Result: item, } jsonBytes, _ := json.Marshal(res) return events.APIGatewayProxyResponse{ Body: string(jsonBytes), StatusCode: 200, }, nil } // main関数はCreateのときと同じなので割愛ここでやっていることは以下の操作です。
- DBに接続(説明割愛)
- DBに問い合わせる検索条件を作る
- DBに問い合わせてデータを取得する
- レスポンスを作成(説明割愛)
2. DBに問い合わせる検索条件を作る
DBに問い合わせてデータを取得する関数は
db.GetItem()
です。func (c *DynamoDB) GetItem(input *GetItemInput) (*GetItemOutput, error)参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#DynamoDB.GetItem
この引数としてとるのはdynamodb.GetItemInput
型なので、この型の変数を作成します。
dynamodb.GetItemInput
型の定義を確認します。type GetItemInput struct { // 今回関係ないフィールドを省略 Key map[string]*AttributeValue `type:"map" required:"true"` TableName *string `min:"3" type:"string" required:"true"` }
TableName
はCreateのときと同様に、対象テーブルの名前を指定するフィールド、Key
は、取得したいレコードのプライマリーキーを指定するフィールドです。
そのため、db.GetItem()
に渡すdynamodb.GetItemInput
型引数を以下のように作成します。getParam := &dynamodb.GetItemInput{ TableName: aws.String("user"), Key: map[string]*dynamodb.AttributeValue{ "userid": { N: aws.String(pathparam), }, }, }このコードの意味は以下の通りです。
- TableName: userテーブルを検索
- Key: ここでは、Number型(N)のキーであるuseridの値が、
aws.String(pathparam)
であるデータを検索するという意味
Key
フィールドで、useridキーが数値型であることを、N
という風に指定しています。各データ型がどの表現に対応するのかは以下の表をご覧ください。
データ型 アルファベット バイナリ B ブール型 BOOL バイナリセット BS リスト L マップ M 数値 N 数値セット NS null NULL 文字列 S 文字列セット SS また、
Key
フィールドは、dynamodb.PutItemInput
型のItem
フィールドと同じくmap[string]*AttributeValue
型なのです。しかし、Createのときと同様にKey
フィールドをdynamodbattribute.MarshalMap
関数から作ろうとしてもうまくいきません。// ダメな例 searchItem := Item{UserID: userid} searchAV, _ := dynamodbattribute.MarshalMap(searchItem) getParam := &dynamodb.GetItemInput{ TableName: aws.String("user"), Key: searchAV, }これはおそらく
dynamodb.PutItemInput.Item
フィールドとは異なり、Key
フィールドには「プライマリーキーの情報だけを含めなければいけない」という仕様が関係していると推測されます。
dynamodbattribute.MarshalMap(Item{UserID: userid})
には、主キーであるUserID以外にも、ゼロ値に設定された他フィールドが含まれてしまっているのでうまくいかないんだと思います。3. DBに問い合わせてデータを取得する
db.GetItem()
を実行すればOKです。Update(PUT)の作成
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/expression" ) // Item構造体とResponse構造体は、Createのときと同じなので割愛 func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { method := request.HTTPMethod pathparam := request.PathParameters["userid"] // まずはDBと接続するセッションを作る sess, err := session.NewSession() if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } db := dynamodb.New(sess) // リクエストボディのjsonから、Item構造体を作成 reqBody := request.Body resBodyJSONBytes := ([]byte)(reqBody) item := Item{} if err := json.Unmarshal(resBodyJSONBytes, &item); err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } // updateするデータを作る update := expression.UpdateBuilder{} if address := item.Address; address != "" { update = update.Set(expression.Name("address"), expression.Value(address)) } if email := item.Email; email != "" { update = update.Set(expression.Name("email"), expression.Value(email)) } if gender := item.Gender; gender != "" { update = update.Set(expression.Name("gender"), expression.Value(gender)) } if name := item.Name; name != "" { update = update.Set(expression.Name("name"), expression.Value(name)) } expr, err := expression.NewBuilder().WithUpdate(update).Build() if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } input := &dynamodb.UpdateItemInput{ TableName: aws.String("user"), Key: map[string]*dynamodb.AttributeValue{ "userid": { N: aws.String(pathparam), }, }, ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), UpdateExpression: expr.Update(), } // update実行 _, err = db.UpdateItem(input) if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } // httpレスポンス作成 res := Response{ RequestMethod: method, } jsonBytes, _ := json.Marshal(res) return events.APIGatewayProxyResponse{ Body: string(jsonBytes), StatusCode: 200, }, nil } // main関数はCreateのときと同じなので割愛ここでやっていることは以下の操作です。
- DBに接続(説明割愛)
- リクエストボディからItem構造体を作る
- DBのデータをどう更新するかを指定する
- update実行
- レスポンスを作成(説明割愛)
2. リクエストボディからItem構造体を作る
Createのときとやり方は全く同じです。
今回は、パスパラメータで指定されたuseridレコードの、emailとnameを更新したくて以下のようなリクエストボディがきたと仮定します。
example-requestbody.json{ "email": "ccc.com", "name": "Emily" }3. DBのデータをどう更新するかを指定する
更新を行う
db.UpdateItem
の引数となるdynamodb.UpdateItemInput
型の変数を作成します。
dynamodb.UpdateItemInput
型の定義は以下の通りです。type UpdateItemInput struct { // 今回関係ないフィールドを省略 ExpressionAttributeNames map[string]*string `type:"map"` ExpressionAttributeValues map[string]*AttributeValue `type:"map"` Key map[string]*AttributeValue `type:"map" required:"true"` TableName *string `min:"3" type:"string" required:"true"` UpdateExpression *string `type:"string"` }参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#UpdateItemInput
Key
とTableName
についてはReadのときと意味は同様です。残り3つのフィールドについては、データの更新の種類・やり方について記述する場所です。「データの更新」といっても、ただ今ある値を捨てて新しい値に書き換えるだけではなく、データ型によって様々な操作が考えられます。主たる例を以下に挙げます。
- 数値型を収める属性Aを、Bという値に上書き保存したい
- 属性Aが保持しているリストに、Bという値を追加したい
- 指定したレコードから属性Aを消したい
- 属性Aが保持しているセット型から、Bというセットを消したい
参考:DynamoDBでデータを更新する際に使うUpdateExpressionについて一通りまとめてみた
そのため、「その属性を操作したいか」を
ExpressionAttributeNames
に、「上書きしたり追加したりしたい値」をExpressionAttributeValues
に、「上書きなのか追加なのかという更新の種類」をUpdateExpression
に記述するのです。例えば、今回の「"name"という属性を、変数
name
の値に上書きしたい」という操作をドキュメントどおりに記述するのならば以下のようになります。input := &dynamodb.UpdateItemInput{ TableName: aws.String("user"), Key: map[string]*dynamodb.AttributeValue{ "userid": { N: aws.String(pathparam), }, }, ExpressionAttributeNames: map[string]*string{ // "name"という属性名を以下#nameと扱う "#name": aws.String("name"), }, ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ // 上書きしたい値nameを以下:name_valueとして扱う ":name_value": { S: aws.String(name), }, }, // #name属性を、:name_valueという値に上書き(set)する UpdateExpression: aws.String("set #name = :name_value"), }参考:あえて aws-sdk-go で dynamoDB を使うときの基本操作
しかし、属性名を
#
で指定したり、更新したい値を:
で指定したりするドキュメントどおりの書き方は少々面倒です。
そのため、これらの構造表現をコードベースで生成するパッケージが公式から提供されています(github.com/aws/aws-sdk-go/service/dynamodb/expression)。せっかくなのでその方法に書き換えていきましょう。まずは、
expression.UpdateBuilder{}
という型の構造体を用意して、その型のメソッドを用いて「どう更新したいのか」を記述します。update := expression.UpdateBuilder{} if name := item.Name; name != "" { update = update.Set(expression.Name("name"), expression.Value(name)) }上の部分は、「DBの"name"という属性を、
name
という変数の中身に上書きする」という操作を、expression.UpdateBuilder{}
型のupdate
に記録しています。このupdateの内容を指定し終わったら、updateの内容を
ExpressionAttributeNames
等のフィールドに入れられる形に変換します。expr, err := expression.NewBuilder().WithUpdate(update).Build()このexprを使って、
db.UpdateItem
を作ると以下のようになります。input := &dynamodb.UpdateItemInput{ TableName: aws.String("user"), Key: map[string]*dynamodb.AttributeValue{ "userid": { // Nはnumber型の意味 N: aws.String(pathparam), }, }, ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), UpdateExpression: expr.Update(), }4. update実行
db.UpdateItem
を実行すればOKです。Delete(DELETE)の作成
package main import ( "encoding/json" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/dynamodb" ) // Item構造体とResponse構造体は、Createのときと同じなので割愛 func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { method := request.HTTPMethod pathparam := request.PathParameters["userid"] // まずはDBと接続するセッションを作る sess, err := session.NewSession() if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } db := dynamodb.New(sess) // deleteするデータを指定 deleteParam := &dynamodb.DeleteItemInput{ TableName: aws.String("user"), Key: map[string]*dynamodb.AttributeValue{ "userid": { // Nはnumber型の意味 N: aws.String(pathparam), }, }, } // delete実行 _, err = db.DeleteItem(deleteParam) if err != nil { return events.APIGatewayProxyResponse{ Body: err.Error(), StatusCode: 500, }, err } // httpレスポンス作成 res := Response{ RequestMethod: method, } jsonBytes, _ := json.Marshal(res) return events.APIGatewayProxyResponse{ Body: string(jsonBytes), StatusCode: 200, }, nil } // main関数はCreateのときと同じなので割愛ここでやっていることは以下の操作です。
- DBに接続(説明割愛)
- deleteするデータを指定する
- delete実行
- レスポンスを作成(説明割愛)
2. deleteするデータを指定する
Read(GET)のときと同じ方法で
dynamodb.DeleteItemInput
型の変数を作り、消去したいデータを指定します。3. delete実行
db.DeleteItem()
を実行すればOKです。Lambda関数コードの参考文献
参考:AWS Lambda, API Gateway, DynamoDB, Golang でREST APIを作る
参考:DynamoDB×Go連載#2 AWS SDKによるDynamoDBの基本操作Lambda関数のコードをアップロード→API Gatewayと連携
ここは既にやった手順と同じなので割愛します。
上述した通り、Lambda関数にDynamoDB用のロールを付与するのを忘れないでください。まとめ
これでAPI Gateway-Lambda-Dynamo DBの3つを連携させたサーバーレスAPIの構築が完了です。お疲れ様でした。
- 投稿日:2020-08-03T19:20:40+09:00
Amazon Cognitoの認証情報をAmazon API Gateway+AWS Lambdaで取得
Goで取得してみます。
APIGatewayの統合リクエストで Lambdaプロキシ統合の使用 にチェックを入れてください。
Go側では、
github.com/aws/aws-lambda-go/events
のevents.APIGatewayProxyRequest
で受け取ります。
下記のように取れます。func handler(ctx context.Context, req events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) { claims := req.RequestContext.Authorizer["claims"].(map[string]interface{}) res := &events.APIGatewayProxyResponse{ StatusCode: 200, Body: claims["cognito:username"].(string), } return res, nil } func main() { lambda.Start(handler) }
- 投稿日:2020-08-03T15:43:03+09:00
wsl2からGOlandを起動する
問題点
- Windows上から起動したIDEからwslパス上のファイルを読み込むと不都合があることがある
- このようなエラーが出ていた
go: RLock \\wsl$\Ubuntu-20.04\home\yuta\test\go.mod: Incorrect function.
構成
- Windows 10
- wsl2(ubuntu20.04)
X Server環境 セットアップ
X環境インストール
sudo apt update && sudo apt upgrade -y sudo service dbus start sudo apt install ubuntu-mate-desktop mate-desktop-environment mate-common mate-coreX Serverインストール
choco install -y vcxsrv設定
export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0.0source $HOME/.bashrc動作確認
xeyes追加パッケージインストール
wget -q https://www.ubuntulinux.jp/ubuntu-ja-archive-keyring.gpg -O- | sudo apt-key add - wget -q https://www.ubuntulinux.jp/ubuntu-jp-ppa-keyring.gpg -O- | sudo apt-key add - sudo wget https://www.ubuntulinux.jp/sources.list.d/bionic.list -O /etc/apt/sources.list.d/ubuntu-ja.list sudo apt update sudo apt -y upgrade sudo apt install -y ubuntu-defaults-jafcitx-mozcインストール
sudo apt install -y fcitx fcitx-mozc環境変数
export GTK_IM_MODULE=fcitx export QT_IM_MODULE=fcitx export XMODIFIERS=@im=fcitx export DefaultIMModule=fcitxsudo apt install -y fonts-noto-cjk fonts-noto-color-emojisudo update-locale LANG=ja_JP.UTF8Mozcの追加
fcitx-autostartfcitx-config-gtk3エディタで日本語入力ができるか確認
pluma
- Ctrl + Space を押して日本穂入力ができることを確認する
IDEセットアップ
GOlandインストール
- toolboxというJetbrains製のパッケージ管理ソフトをインストールする
- wgetでできないときは、こちらのURLからダウンロード
https://www.jetbrains.com/toolbox-app/
wget https://download.jetbrains.com/toolbox/jetbrains-toolbox-1.17.7275.tar.gz?_ga=2.107210690.1365384453.1596434887-1054327958.1596158640 sudo tar -xzf jetbrains-toolbox-1.17.7275.tar.gz?_ga=2.107210690.1365384453.1596434887-1054327958.1596158640 -C /usr/local/bin --strip-components 1toolboxからのインストール
起動
jetbrains-toolbox画面からインストール
GOland起動
- インストールパスはtoolboxの右上の設定ボタンから確認・変更ができる
~/.local/share/JetBrains/Toolbox/apps/Goland/ch-0/202.6397.65/bin/goland.shAlias設定
.bashrcに追記
alias goland=~/.local/share/JetBrains/Toolbox/apps/Goland/ch-0/202.6397.65/bin/goland.sh参考
https://bbq-all-stars.github.io/2019/04/30/wsl-ubuntu-intellij-develop-environment.html
https://odaryo.hatenablog.com/entry/2020/01/16/214830
- 投稿日:2020-08-03T13:19:17+09:00
ゆっくりGo vol.2
前回のおさらい
・私にとっての明日は今日なので…(目そらし)
・for
まで習ったぞ!
・個人的に好きな言語!
・型のあたりがもやもやするぞ!ほんへ
if.go//------------------------------------------------ func pow(x, n, lim float64) (float64, float64) { if v := math.Pow(x, n); v < lim { return v,lim } return lim } func main() { fmt.Println( pow(3, 2, 10), pow(3, 3, 20), ) } //----------------------------------------------ifと言うより関数が独特に感じる。
上記のコードを走らせると、こんなエラーが返ってくる。
e1.go./prog.go:12:2: not enough arguments to return have (float64) want (float64, float64) ./prog.go:17:6: multiple-value pow() in single-value context ./prog.go:18:6: multiple-value pow() in single-value context内容としては引数と戻り値の数が一致してないよ!ってことっぽい。
原因としては、ifの外側の
return
に戻り値が一つしかない点にある。それを修正すると、
e2.go./prog.go:17:6: multiple-value pow() in single-value context ./prog.go:18:6: multiple-value pow() in single-value contextが返ってくる。
単一の変数なのに、中に二つも数字があるよ!ってことっぽい。
この原因は
fmt.Println(,)
みたいな書き方をしている所にあるっぽい。色々書き直して、最終的にこんな感じに……
Exacer_if.gopackage main import ( "fmt" "math" ) func pow(x, n, lim float64) (float64, float64) { if v := math.Pow(x, n); v < lim { return v, lim } return lim, n } func main() { fmt.Println(pow(3, 2, 10)) fmt.Println(pow(3, 3, 20)) }一応うまいくいった。
ただ面白いのが、戻り値を省略することができない点。
戻り値の数だけ、きちんと明記しなければならない。
可読性の観点でいえば、非常に重要機能だと思う。
何より、この次も非常に面白かった。
if_else.go//--------------------------------------------- func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } else { fmt.Printf("%g >= %g\n", v, lim) } // can't use v here, though return lim } func main() { fmt.Println( pow(3, 2, 10), pow(3, 3, 20), ) } //-------------------------------------------面白いのって
fmt.Printf("%g >= %g\n", v, lim)
の文。これって
return
だと戻り値の制約に引っかかる所を、表示の文を入れることで、それに対処している。
if-return
みたいな思考回路の私には一生思いつかない書き方。少し詰まったExercise
Newton_Raphson.gopackage main import ( "fmt" ) func Sqrt(x float64) int { var z float64 = 1 var i int = 1 for ; i < 11; i++ { z = z - (z*z-x)/(2*z) fmt.Print(z, "\n") } return (i) } func main() { fmt.Println(Sqrt(2)) }・iは
0+1
からスタート
・zをしくじってfor内に書いてしまうなんてことをしていました。
Switch構文は良いですね!
Fortranの
case
より使い勝手がよさそうです。……気になるのが、Fortranの
case
って数値だけしか取れないんですかね?正直一番気になったのは
defer
便利そうな構文で上手に使えれば面白そうでした。
defer.go//---------------------------------- func main() { fmt.Println("counting") for i := 0; i < 10; i++ { defer fmt.Println(i) } fmt.Println("done") } //---------------------------------普通なら評価が終わった順に何かしらされますが、
終了した全部の評価を逆順ソートして出力するみたいな動きですかね?
何か悪さができそうですね………
感想
Flow control statements: for, if, else, switch, and defer
まで終了しました。
想定していたスピードよりも大分遅いですが、ぼちぼちとしておきます。
A Tour of Go
ですが、色々とimportについての説明が欠片もないので、
少し大変かなぁと。
- 投稿日:2020-08-03T09:31:58+09:00
http sample with go.uber.org/dig
package main import ( "fmt" "net/http" "go.uber.org/dig" ) type Handler struct { Greeting string Path string } func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s from %s", h.Greeting, h.Path) } func NewHello1Handler() HandlerResult { return HandlerResult{ Handler: Handler{ Path: "/hello1", Greeting: "welcome", }, } } func NewHello2Handler() HandlerResult { return HandlerResult{ Handler: Handler{ Path: "/hello2", Greeting: "?", }, } } type HandlerResult struct { dig.Out Handler Handler `group:"server"` } type HandlerParams struct { dig.In Handlers []Handler `group:"server"` } func RunServer(params HandlerParams) error { mux := http.NewServeMux() for _, h := range params.Handlers { mux.Handle(h.Path, h) } server := &http.Server{ Addr: ":8080", Handler: mux, } if err := server.ListenAndServe(); err != nil { return err } return nil } func main() { container := dig.New() container.Provide(NewHello1Handler) container.Provide(NewHello2Handler) container.Invoke(RunServer) }