Go で Lambda 書いたときの覚え書き

はじめに

Go 言語はほぼ触ったことなかったんですがもうリリースから10年ぐらい経っているらしいです。

AWS Lambda を書くのに少し Go 言語を使ってまた使うかもしれないので覚えたことを書いておきます。

go1.12 のバージョンを使っていました。

まだ少ししか触っていないので色々間違いがあるかもしれません。

またここに書いていることは自分にとって少し分かりにくかったことなどの羅列的なものになります。

CodeCommit や GitLab で使う場合

Go では依存ライブラリを専用のサーバーではなく GitHub などから取得するようになっているようです。

GitHub の場合はあまり問題なく使えると思うんですが CodeCommit やセルフホストしている GitLab などの場合は少し独自の設定が必要なようでした。

これについて詳しく書いているところが見つからず数日はまっていた気がします。

github.com

この issue でなんとなく CodeCommit などでの使い方がわかりました。

デフォルトでは https で依存パッケージを取得するようなんですが、 メタデータを取得する API に対応している必要があって サーバーによってはこれに対応していないので https を使わないようにしてやる必要があるのだと思います。

~/.gitconfig の設定に以下を追加しました。

[url "ssh://git-codecommit.ap-northeast-1.amazonaws.com/"]
    insteadOf = https://git-codecommit.ap-northeast-1.amazonaws.com/

それからモジュール名末尾には .git をいれました。これはいらない場合もあるかもしれません。

go mod init git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/hello.git

パッケージ

下のようにソースの一行目にパッケージの指定があります。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello, world")
}

最初は一つのファイルがパッケージと対応しているのかと思っていましたが、 これは間違いで Go のパッケージは一つのディレクトリと対応しているようです。

同じディレクトリにおいたファイルは同一のパッケージにする必要があって、 ファイルが分かれていても同じパッケージ内であれば import せずに関数などを利用できます。

それから同じリポジトリ内であっても import は相対的パスなどではなくホスト名を含むパッケージ名を使うのが普通のようでした。

Lambda を書いたりする場合に CLI ツールとしても使えるように ライブラリのパッケージ、Lambda パッケージ、CLI ツールパッケージと分けたりすることがあるんじゃないかと思います。

例えば下のようなディレクトリ構成になります。

hello/
├── app
│   └── main.go
├── lambda
│   └── main.go
└── lib
    └── hello.go

lib/hello.go はライブラリとして他のパッケージから使われる関数などを含んでいます。 パッケージで公開される関数や定数の名前は大文字で開始するという決まりになっているようです。 これは人によって好み分かれそうですが export を別に書かなくていいのは楽だなと思いました。

package hello

import (
    "fmt"
)

func Hello() {
    fmt.Println("hello, world")
}

app/main.goCLI ツールなどのパッケージで lib/hello.go を import して使います。 import する名前にはソースリポジトリ名にパッケージのディレクトリのパスを追加して指定します。 ライブラリではなくアプリケーションとして実行するパッケージは main パッケージにして main 関数を定義します。

package main

import (
    "git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/hello.git/lib"
)

func main() {
    hello.Hello()
}

go build コマンドで実行ファイルをビルドします。

go build -o hello ./app

go run コマンドでそのまま実行もできます。

go run ./app

スライス

スライスはランダムアクセスできて要素の追加もできるコンテナです。 C++ の std::vector に近いものだと思います。 内部に要素サイズと、確保したサイズ、ポインタを保持するような実装になってるんじゃないかと思います。 std::vector のように要素をメモリ上に連続で確保するようになっていて、足りない場合は新しくメモリを確保して作り直すようです。 下のようなコードでポインタの値が変わっているのが見られました。

package main

import (
    "fmt"
)

func main() {
    a := make([]int, 2,  3)
    fmt.Println(a) // [0 0]

    a[0] = 1
    a[1] = 2
    fmt.Println(a) // [1 2]

    fmt.Printf("%v %p %p\n", a, &a, &a[0]) // [1 2] 0xc00000c060 0xc000014380

    a = append(a, 3)
    fmt.Printf("%v %p %p\n", a, &a, &a[0]) // [1 2 3] 0xc00000c060 0xc000014380

    a = append(a, 4)
    fmt.Printf("%v %p %p\n", a, &a, &a[0]) // [1 2 3 4] 0xc00000c060 0xc0000181b0
}

素数がわかる場合は指定したほうが余分なメモリ確保やコピーが減るので速くなると思います。 これは std::vector で reserve 使ったりするのと同じ感じだと思います。

マップ

マップはキーと値を持つコンテナです。 大体は C++ の std::map と似ているんですがキーはソートされていない構造のようです。 map の要素をイテレートするとき順序が保証されないことに気を付ける必要があります。

下のコードを実行すると順序がバラバラになっているのが見られました。

順序固定する場合はキーを一度取得してソートしてからキーでアクセスします。

package main

import (
    "fmt"
)

func main() {
    a := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
        "d": 4,
        "e": 5,
    }

    fmt.Println(a)

    for i := 0; i < 10; i++ {
        for k, v := range a {
            fmt.Printf("%s %d\n", k, v)
        }
    }
}

JSON

golang.org

標準で入っている JSON のライブラリが使えます。 json.MarshalJSON への変換、 json.UnmarshalJSON から struct などの変換ができます。

struct はそのまま何もしなくても JSON にできます。

フィールドにタグを付けて少しカスタマイズすることもできます。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string `json:"name"`
    Age int `json:"int"`
    PhoneNumber string `json:"phone_number,omitempty"`
}

func main() {
    a := Person{
        Name: "Taro",
        Age: 3,
    }

    bytes, err := json.Marshal(a)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(bytes))

    b := Person{}
    err = json.Unmarshal(bytes, &b)
    if err != nil {
        panic(err)
    }

    fmt.Println(b)
}

独自に変換処理を書きたい場合は MarshalJSON と UnmarshalJSON を定義してやればいいようです。

json - The Go Programming Language

Example (CustomMarshalJSON) の部分に例がありました。

テスト

go test のコマンドで簡単にテスト実行できるようになっています。 hoge_test.go のようなファイル名にして Test ではじまる名前の関数を定義するとテストとして実行されます。

godoc.org

テストで assert を書くのにこのライブラリが便利でした。

package main

import (
    "github.com/stretchr/testify/assert"
    "testing"
)

func TestA(t *testing.T) {
    a := []string{ "aaa", "bbb" }
    b := []string{ "aaa", "BBB", "ccc" }
    assert.Equal(t, a, b)
}

CLI

godoc.org

CLI ツールを書くときに使えるライブラリも色々あるようなんですが cobra というライブラリを少し使いました。

少し使った感じはサブコマンドも書けて、引数やオプションのキャプチャも簡単だったのでよさそうに感じました。

aws-cli を使ったシェルスクリプトなどを書いていてオプションとか扱うのがなかなか大変なので Go で書いて aws-sdk-go 使うのもいいかもなと思いました。

Lambda や CodeBuild でも多分使えると思うので実行もしやすいと思います。

AWS SAM

aws.amazon.com

docs.aws.amazon.com

Go 言語関係無い話ですが Lambda を書くときに AWS SAM を使うと便利でした。

CloudFormation に変換されるテンプレートで Lambda や CloudWatch イベント、API Gateway、IAM ロール などのリソースを定義して AWS SAM のコマンドでパッケージ化とデプロイが簡単にできるようになっています。

ローカル環境で Docker を使って Lambda の挙動をテストすることもできます。

AWS SDK

Go 言語向けに AWS SDK のライブラリが公開されていてこれを使って AWSAPI を色々利用できます。

aws.amazon.com

aws-sdk-go-v2 というライブラリもあるようなんですがまだ Developer Preview の段階でした。

github.com

基本的に API 呼び出すためのライブラリですが一部ユーティリティの機能も追加されていてページネーションに対応した便利な関数などもあるようでした。

godoc.org

テスト用のモックも用意されているようです。

godoc.org

感想

使う前は機能が色々足りてないような印象だったんですが書いてみると意外と書きやすく感じました。

書きにくいと感じたり他の言語でできることができないために自分で書かなければいけないようなコードが増えたりもするんですが、 こういう言語だから仕方ないとあきらめて書いているとあまり悩まずに書ける気がしました。

コンパイラで静的に色々チェックされるのも安心感があって Python とか シェルスクリプトとかで色々書くのよりも自分にはあってる気がしました。