20200803のGoに関する記事は5件です。

AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた

この記事について

Developers.IO 2020のサーバーレスセッションに触発されました。
[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020

というわけで、Golangを用いてAWSで基本的なサーバーレスをやってみたその手順をまとめました。
具体的には以下の手順を紹介します。

  1. GolangでLambdaを動かしAPI Gatewayと連携させる
  2. LambdaとDynamoDBと連携してAPIを作る

使用する環境・バージョン

  • OS : macOS Mojave 10.14.5
  • Golang : version go1.14 darwin/amd64

読者に求める前提知識

Golangの基本的な文法がわかること。

Lambda関数の作成

コンソールで関数を作成

AWS Lambdaのコンソールを開くと、以下のような画面になります。
スクリーンショット 2020-07-28 19.21.38.png
右上にある「関数の作成」ボタンをクリックします。
すると、以下のような関数作成画面に遷移します。

スクリーンショット 2020-07-28 19.23.07.png
「一から作成」を選択し、関数名・ランタイムを記入します。今回は以下のように設定しました。

  • 関数名: 好きな名前を入力(今回はmyTestFunction)
  • ランタイム: Go1.x

次にLambda関数のアクセス権限の設定をします。
「実行ロールの選択または作成」のプルダウンを開くと、以下のようなフォームが表れます。
スクリーンショット 2020-07-28 19.24.25.png
今回Lambdaを動かすのは初めてなので、「基本的なLambdaアクセス権限で新しいロールを作成」を選択します。
これで、Lambda関数作成時に、関数のログをCloudWatchに出力するためのロールが作られます。

ここまでの入力が終わったら、関数を作成します。
正常に作成されたら、以下のような画面になります。
スクリーンショット 2020-07-28 19.25.31.png
補足: このとき関数と同時に作られたロールと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.go
package 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.Marshalstringと変換

コードをLambdaにアップロード

Lambdaにアップロードするのはコンパイル済みの実行ファイルである必要があるので、上で書いたhello.goをビルドしてバイナリファイルhelloを作ります。
また、アップロードの形式がzipファイルなので、ビルド後にhelloをzip圧縮します。

$ GOOS=linux GOARCH=amd64 go build -o hello hello.go
$ zip hello.zip hello

参考:AWS Lambda× Goを試す

先ほど作ったmyTestFunction関数をLambdaコンソールで開き、以下の設定画面からhello.zipをアップロードします。
スクリーンショット 2020-07-28 21.35.42.png

注意:アップロードする実行ファイルの名前

「一から作成」のオプションから作成したLambda関数に渡す実行ファイルの名前は、必ずhelloである必要があります。
これは一から作成のLambda関数がデフォルトで「helloという名前のバイナリファイルを実行する」という設定になっているため、他の名前だと以下のようなPathErrorが起きます。

{
   “errorMessage”: “fork/exec /var/task/binaryname: permission denied”, 
   “errorType”: “PathError”
}

Lambdaのテスト

Lambda関数は、Webコンソール上でテストを実行することができます。
スクリーンショット 2020-08-01 14.32.53.png
コンソール上で、右上の「テスト」のボタンをクリックします。
すると、テストリクエストを編集する画面が表れます。
スクリーンショット 2020-07-29 14.28.35.png
デフォルトだとこのような状態です。このまま名前をつけて保存します。
スクリーンショット 2020-07-29 15.14.14.png
この状態で「テスト」ボタンをクリックすると、テストが実行・結果が表示されます。
スクリーンショット 2020-08-01 14.33.50.png
きちんとステータスコード200が返ってくることが確認できました。

補足: このテストはAPI Gateway経由のリクエストを送っているわけではないので、httpメソッドやボディなどのリクエスト情報が空のときの結果が表示されています。

API Gatewayと連携

APIの作成

スクリーンショット 2020-07-28 21.43.51.png
開始画面から、「今すぐはじめる」ボタンをクリックします。
すると、以下のようなAPI作成画面になります。
スクリーンショット 2020-07-28 21.45.29.png
以下のような設定を入力して作成します。

  • プロトコル: REST
  • 新しいAPIの作成: 新しいAPI
  • API名: 好きな名前をつける
  • 説明: 好きな説明文を書く
  • エンドポイントタイプ: リージョン

スクリーンショット 2020-07-28 21.46.25.png
APIを作成したら、「どのパス・どのメソッドにどの処理を結びつけるか」の設定を行う画面が表示されます。

URLリソースの作成

まずは、URLリソースの作成を行います。「アクション」→「リソースの作成」を選択します。
スクリーンショット 2020-07-28 21.57.13.png
以下のような、パスパラメータの名前等を設定する画面になります。
スクリーンショット 2020-07-28 22.04.28.png

  • プロキシリソース: なし
  • リソース名: 好きな名前をつける
  • リソースパス: {pathparam}
  • API Gateway CORS : なし

以上の設定でリソースを作成します。

メソッドの作成

次に、httpリクエストメソッドーLambda関数の紐付けを行います。
スクリーンショット 2020-07-28 22.05.23.png
/{pathparam}を選択した状態で、「アクション」→「メソッドの作成」を選択します。
スクリーンショット 2020-08-01 15.21.17.png
プルダウンから、設定を行いたいリクエストメソッドを選択します。GETやPOSTなどの特定メソッドの選択はもちろん、全てのメソッドに対しての設定を行いたい場合はANYという選択肢もあります。

今回はANYを選択します。
メソッドを選択したら、その選択メソッドにどんな処理(Lambda関数)を紐づけるのかの設定画面が表示されます。
スクリーンショット 2020-08-01 15.21.44.png

  • 統合タイプ: Lambda関数
  • Lambdaプロキシ統合の使用: あり
  • Lambdaリージョン: ap-northeast-1
  • Lambda関数: myTestFunction
  • デフォルトタイムアウトの使用: あり

以上の設定で保存をクリックすると、以下のような確認画面が出ます。
スクリーンショット 2020-07-28 21.58.34.png
問題ないので、OKを選択します。結果は以下の通り。
スクリーンショット 2020-08-01 15.22.24.png

テストの実行

上の画面で「テスト⚡️」をクリックすることで、API Gatewayからリクエストを送ったときにきちんと動くかどうかのテストを実行することができます。

スクリーンショット 2020-07-31 14.54.26.png
このように、パスパラメータ・クエリパラメータ・リクエストメソッド・ボディなどを自由にコンソール上で設定して、それに対してどのような応答が返ってくるのかを確認することができます。

APIのデプロイ

実際にAPIをデプロイするには、「アクション」→「APIのデプロイ」を選択します。
スクリーンショット 2020-07-28 22.22.33.png
すると、デプロイステージを指定する画面になります。
スクリーンショット 2020-07-28 22.24.51.png
ステージを指定して「デプロイ」ボタンを押すと、以下のような画面に遷移します。
ここで、APIが公開されているURLを確認することができます。
スクリーンショット 2020-07-28 22.25.01.png

参考:【AWS】API Gateway + LambdaでAPIをつくる

DynamoDBと連携

ここからは、[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020で紹介された、以下のようなCRUDを行うDB連携APIを新しく作っていきます。
スクリーンショット 2020-07-31 15.13.36.png

テーブル作成

スクリーンショット 2020-07-31 15.42.54.png
テーブル作成画面で、以下のような設定を入力して作成します。

  • テーブル名: 好きな名前(ここではuser)
  • プライマリーキー: userid(数値)
  • ソートキー: 追加しない
  • テーブル設定: デフォルト設定の使用

補足: プライマリーキーに設定できるデータ型は文字列orバイナリor数値です。

スクリーンショット 2020-07-31 15.58.43.png
作成に成功すると、以下のような画面に遷移します。

データ項目の追加

作成直後は、プライマリーキー以外の属性が存在しないので、他のキーが欲しいのならば手動で追加する必要があります。
テーブルの項目タブを開きます。
スクリーンショット 2020-07-31 16.04.50.png
「項目の作成」ボタンをクリックすると、以下のような項目編集画面が表示されます。
スクリーンショット 2020-07-31 16.05.09.png
+をクリックします。
スクリーンショット 2020-07-31 16.05.20.png
Appendを選択します。
スクリーンショット 2020-07-31 16.05.27.png
すると、どういう型のフィールドを追加するかを選択できます。
今回はStringを選択しました。
スクリーンショット 2020-07-31 16.06.07.png
すると、String型のフィールドが追加されました。好きにフィールド名をつけたり、値も設定を行ったりします。
スクリーンショット 2020-07-31 16.08.41.png
項目の追加を複数回行い、最終的にこうなりました。これで保存します。
スクリーンショット 2020-07-31 16.08.47.png
無事にプライマリーキー以外の項目が作成されたことが確認できます。

参考:初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~

Lambda用のロールを作成

Lambda関数がDynamoDBにアクセスできるように、Lambda関数に付与するロールを作成します。

IAMコンソール→ロールから作成画面を開きます。
スクリーンショット 2020-07-31 16.26.23.png
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/dynamodb

Create(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)
}

ここでやっていることは以下の操作です。

  1. DBに接続
  2. リクエストボディからItem構造体を作る
  3. Item構造体から、DBにinsertするためのデータを作る
  4. insertを実行する
  5. レスポンスを作成

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のときと同じなので割愛

ここでやっていることは以下の操作です。

  1. DBに接続(説明割愛)
  2. DBに問い合わせる検索条件を作る
  3. DBに問い合わせてデータを取得する
  4. レスポンスを作成(説明割愛)

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

参考:AWS公式ドキュメント AttributeValue

また、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のときと同じなので割愛

ここでやっていることは以下の操作です。

  1. DBに接続(説明割愛)
  2. リクエストボディからItem構造体を作る
  3. DBのデータをどう更新するかを指定する
  4. update実行
  5. レスポンスを作成(説明割愛)

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

KeyTableNameについては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のときと同じなので割愛

ここでやっていることは以下の操作です。

  1. DBに接続(説明割愛)
  2. deleteするデータを指定する
  3. delete実行
  4. レスポンスを作成(説明割愛)

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の構築が完了です。お疲れ様でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon Cognitoの認証情報をAmazon API Gateway+AWS Lambdaで取得

Goで取得してみます。

APIGatewayの統合リクエストで Lambdaプロキシ統合の使用 にチェックを入れてください。

Go側では、github.com/aws/aws-lambda-go/eventsevents.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)
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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-core

X Serverインストール

choco install -y vcxsrv

image.png
image.png
image.png
image.png

設定

export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0.0
source $HOME/.bashrc

動作確認

xeyes

image.png

追加パッケージインストール

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-ja

fcitx-mozcインストール

sudo apt install -y fcitx fcitx-mozc

環境変数

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export DefaultIMModule=fcitx
sudo apt install -y fonts-noto-cjk fonts-noto-color-emoji
sudo update-locale LANG=ja_JP.UTF8

Mozcの追加

fcitx-autostart
fcitx-config-gtk3

image.png

エディタで日本語入力ができるか確認

pluma
  • Ctrl + Space を押して日本穂入力ができることを確認する

image.png

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 1

toolboxからのインストール

起動

jetbrains-toolbox

画面からインストール

image.png
image.png
image.png

GOland起動

  • インストールパスはtoolboxの右上の設定ボタンから確認・変更ができる
~/.local/share/JetBrains/Toolbox/apps/Goland/ch-0/202.6397.65/bin/goland.sh

image.png

Alias設定

.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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ゆっくり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.go
package 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.go
package 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についての説明が欠片もないので、

少し大変かなぁと。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む