emahiro/b.log

Drastically Repeat Yourself !!!!

Elastic Container Registory に Image をアップロードする

Overview

Lambda on Container を試す際に Elastic Container Registory (以下 ECR) にコンテナイメージをアップロードする必要がありました。 このエントリではその ECR に Docker イメージをアップロードする方法について記載します。

以下に書かれてる内容と大体同じです。

aws.amazon.com

前提

以下が前提になります。

  1. AWS ECR を操作できる権限を持った Role でログインしていること。
  2. ログインした Role でクレデンシャルを取得し、aws コマンドを叩く権限を手に入れていること。

2 については権限を持ってるユーザーであれば local から aws コマンドを叩いても可能ですが、権限を持った Role でログインして CloudShell を使うのがいいかもしれません。

ちなみに僕は AWS に慣れていないのでクレデンシャルの取得方法で悩みました。

dev.classmethod.jp

イメージをアップロードする

# アップロードしたいイメージのディレクトリを掘る
$ aws ecr create-repository --repository-name $DirName/$imageName --image-scanning-configuration scanOnPush=true

# 手元のイメージを ECR 用のイメージとして作成する(タグ切る)
$ docker tag $localImageName:$Tag $ECR_URL:$Tag

# ecr のクレデンシャルを取得して docker にログインする
$ aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URL

# Image を push する。
$ docker push $ECR_URL:$Tag

$ECR_URL$AccountID.dkr.ecr.$Region.amazonaws.com になります。

ちなみに $AccountID は以下から確認できます。

f:id:ema_hiro:20201223155815p:plain

Go で unused なコードを検出する

Overview

staticcheck を使って unused なコードを一括で検査します。

Install

$ go get honnef.co/go/tools/cmd/staticcheck

ref: https://staticcheck.io/docs

検出する

U1000 がどこからも参照されていないコードの警告コードになるので、検査結果を grep で引っ掛ければ一覧を取得できます。

$ staticcheck ./... | grep "U1000"

その他のコードの一覧は https://staticcheck.io/docs/checks に記載されてます。

おまけ

golangci-lint を使うと「よろしくない」コードを一括で検査して警告してくれます。Reviewdog と組み合わせて CI で利用するケースは多いかと思いますが、普通に手元でコードを検査できるのも便利です。

$ GO111MODULE=on go get github.com/golangci/golangci-lint/cmd/golangci-lint@master
golangci-lint run --config ./.github/.golangci-lint --tests=false ./...

各オプションの詳細については golangci-lint run -h で調べると良いです。

ref: https://golangci-lint.run

AWS Lambda 向けに trace 付き Logger を作った

Overview

業務で AWS Lambda を使用してとある実装をしたのでその紹介です。

github.com

なぜ作ったのか?

Lambda 上でログを吐くとデフォルトで Cloud Watch Logs(以下 CW Logs) にログが飛ぶのですがこの CW Logs の生ログ検索が非常に使いづらく、また Lambda は非同期でバンバン実行されるので、どのログがいつ実行されたLambdaに紐づいているのかわからず、ログは追えるけどどういう流れでそのログが出力されたのがわからない、という問題を抱えてました。 そこで Stackdriver logging でやっていたようなログの構造化ができないのか調査したところ 公式ドキュメントにある https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData-discoverable-fields.html によると、デフォルトのCW Logs のフィールドに加えて、JSON文字列を出力した場合にその key がそのままCW Logs のフィールドとして使えることがわかったので trace をつけた json 文字列をログとして出力して、CW Logs Insight で trace でフィルタして引っ掛けると実行ごとのログの一覧できる Logger を作りました。

どう作ったか?

以前似たような Loggerを作ったことがあったので、それと同じようなロガーを実装しました。
具体的には Opencensus の分散トレーシングのライブラリ(https://pkg.go.dev/go.opencensus.io/trace) を使用して Lambda が起動するごとに一意な traceID と spanID を発行し、それを一連の処理の context として引き継いでいくことで Lambda の起動ごとにログをまとめられるようにしています。

How to Use

Lambda のエントリーポイント( Go の場合は main.go) で lambda.Start を Call する前に SpanContext をセットし、その Context を log の出力の時に使用します。 詳細は Readme を参照してください。

結果

このロガーを使用するようになったことにより、CW Logs Insight で以下のようなクエリを叩くと、特定の trace に紐づく一連のログを全て出力できようになりました。

fields @timestamp, @message
 | filter trace="service/$ServiceName/trace/xxxxxxxxxxxxxxx..."
 | sort @timestamp desc

Lambda の起動ごとの一連の流れをログで確認することができるようになったことに加えて CWLogs Insight は非常に高速にフィルタした結果を取り出してくれるので、サービスの中で何が起きているのかをすぐにわかるようになりました。

その他

実はこのロガーは Lambda の起動ごとに一つの処理しか行わない想定で、起動につき一つのライフサイクルのログのしか表示されないものかと思っていましたが、どうやら実際のログを追ってみると別のライフサイクルの Lambda のログも表示されることがわかりました。
Lambda の一度の起動につき、1つの処理しか行われない(Lambda が立ち上がっているインスタンスの使い回しは発生しない)と思っていました、実際にはインスタンスを使いまわしてるケースが存在することがわかりました。
ログはグルーピングされているので運用上特に困ることはないんですが、Lambda の振る舞いを知る機会にもなりました。

Firestore が絡んだテストで事前にデータを生成し終わったら削除する

Overview

テストを書くときにテストの実行前にデータを作成し、終わったら削除したくなる時がありますね?僕はあります。
Firestore が絡んだテストを書くときに Firestore Emulator を起動した状態で実際にデータを作成して、終わったら削除する場合の実装方法について記載します。

Sample

事前に Firestore Emulator を起動した状態で、愚直に Add してテストが完了したら defer で Truncatate します。

$gcloud beta emulators firestore start --host-port=localhost:$PORT

実装のサンプルは以下のようになると思います。かなり長ったらしいですが、他のテストのケースに影響を与えないように愚直にやるとこんな感じになるかなと思います。

tx := context.Background()
fs, err := firestore.NewClient(ctx, "$ProjectName")
coll := fs.Collection("$CollectionName")

tests := []struct{
    name string
    // テストデータ
    data interface{}
}{
    name: "case_1"
    data: inteface{}{}
}

for _, tt := range tests {
    tt := tt
    t.Run(tt.name, func(t *testing.T){
        // setup data
        if _, _, err := coll.Add(ctx, tt.data); err != nil {
            t.Fatal(err)
        }
        // data の削除
        defer func(){
            refs, err := coll.DocumentRefs(ctx).GetAll()
            if err != nil {
                t.Fatal(err)
            }
            for _, ref := range refs {
                batch := fs.Batch().Delete(ref)
                if _, err := batch.Commit(ctx); err != nil {
                    t.Fatal(err)
                }    
            }    
        }
    }
}

Refs

See Also

Firestore の js SDK には clearPersisitence というメソッドを教えてもらいまいたが、Go の Firestore SDK には実装されていなかったので使えませんでした...

Eureka Advent Calendar 2020 に投稿しました。

所属してる企業のアドベントカレンダーに記事を投稿しました。

Lambda にガッツリ触れた際の振り返りについて書いています。

興味がありましたら是非ご一読ください。

medium.com

MOKUMOKU ONSEN #3 に行ってきました

f:id:ema_hiro:20201214205434j:plain

Overview

定期的に行っている開発合宿に行ってきたログです。

行き先

今回は函館です。
GoTo キャンペーンを使ったので、普段なら考えられないリゾートホテルに宿泊できました。

www.hotespa.net

進捗

ずっっっっっと停滞していたアウトプットをやってました。
計5エントリくらい(会社のアドベントカレンダー含)書き貯めたので年内少しずつ吐き出していきます。

その後副業で関わってるサービスの仕事を少し進めました。

ほんとは自分のサイトの Nuxt のバージョンも上げる、くらいまでできるかなと思ったんですが、流石にそこまではいけなかったので、それは冬休みの宿題に持っていきます。

振り返り

Good

仕事を言い訳にブログが書けずに溜まっていた系の進捗が出たので2020年をスッキリ終われそうです。
(ほんとよかった...)

こんな時期なので、旅行なんて...とも少し思いましたが、少人数 && 感染防止対策を徹底していたので比較的安全に旅行できたのではないかと思います。
この時期の函館は寒すぎて、市内に降り立った瞬間に外を出歩こうなんて気持ちは折れました(この時期に最高気温 -4 度叩き出す北海道は流石にレベルが違いました...)

あとご飯は美味すぎました。
価格は同じくらいなのに、味の質と量が東京で食べるそれとは段違いでした。
なぜか毎回食と酒へのモチベーションも高い「もくもく温泉」ですが、その中でも最も満足度の高い合宿になりました笑

More

今回残念だったのはホテルの Wi-fi がそんなに強くなく、まぁまぁな頻度でインターネットが切れてしまったことですね。今までのところは比較的回線が安定していたので差を感じてしまいました。
外寒いし吹雪だし、ホテルに引きこもって Youtube でも見てたんですかね。。。

あと桃鉄持っていったのに、進捗に出すのに夢中で結局できませんでした(いいことです)

See Also

僕らの知見は以下のツイートの主のエントリにまとまってるので一読してみてください。

Github Actions のビルドマトリックスを使う

Overview

Github Actions のビルドマトリックスを使って複数の Go のバージョンでテストを行う方法について記載します。

ビルドマトリックスとは?

Github Actions のドキュメントに書いてあります。

docs.github.com

strategy.matrix を指定すると matrix で指定された値(配列なら要素分) を ${{ matrix.value }}value に入れて Actins でビルドしてくれる機能です。
これにより、例えば今まで複数の言語のバージョンをサポートしたいケース(Goなら直近2バージョン)で2つ step を記載していたのをstep を1つにまとめることが可能になります。

Samples

以下のような感じで設定します。

https://github.com/emahiro/glc/blob/master/.github/workflows/gotest.yml#L6-L9

バージョンの指定方法は Github Actions の言語ごとの設定を参考にしました。

ex.

ビルドマトリックスを使えばOSSを作ってるケースで複数バージョン対応をシュッと書けるだけでなく、OS のディストリビューションGithub Actions に用意されてるものについてはシュッと書けるのでおすすめです。
コマンドラインツールとか作ってる方向けにも便利な機能ですね。

VSCode の Explorer でファイルやディレクトリを非表示にする

Overview

タイトルの通りです。
node_modules などのディレクトリがあると一度開くだけで Explorer がとても広がってしまうのが鬱陶しなくなってしまったので非表示にすることにしました。

手順

Setting.json に以下を追加するだけです。

  "files.exclude": {
      // 略
      "**/node_modules":true, // 追加
      "**/.vscode":true, // 追加
  },

僕は VSCode の設定ディレクトリもコードを書く時には特にいらないなと思ったので外しています。

A little copying is better than a little dependency について考えてみる

これはたまに書いてる自分自身の思考の dump の一つです。
思考を吐き出してるだけなので、所々話が飛んでるかもしれないですがご愛嬌で。

サマリ

Go では A little copying is better than a little dependency.  という諺があり、これは依存 < 冗長を選択した方がいいケースがあることを示している。
プログラミングのお作法としての DRY を否定しているかのような諺であるが、実際にプロダクト開発をしていくにつれてこの諺の示すところの片鱗が少し見えてきたので、実体験を元にして文章にまとめてみる。

ref: https://go-proverbs.github.io

そもそも依存が嫌われるわけ

言ってしまえば、プロダクトを実際に作っている時には、DRY の原則に基づいた方が効率がいいと思う。
同じコードを2度以上書くのは面倒くさい(※1)

ただ、DRY の原則に則ってプロダクトのコードを書いてきたにもかかわらず、ある時からこの DRY の原則に従って生み出されたものに嫌気がさすタイミングがある。 それはどういうタイミングなのか、と考えると依存が大きく、かつ依存先の処理を理解しないと影響が範囲が読めずに「コードを簡単に変更できない」という事態に遭遇した時だ。

プロダクトは長く使われる中で改善され、改変されていく。刹那的なプロダクトや個人開発で自由気ままに開発しているプロダクトであればまぁ好きに書いてくれていいが、社会に対して何らかの価値を提供し、継続的に運用されているプロダクトは、常に何らかの改善や機能の追加がなされていく。

この改善や機能追加のタイミングで既存のコードに手を加える時、あるいは自分たちが使ってるライブラリや環境が古く、ソフトウェアが危険(セキュリティなど)に晒される可能性がある時に、既存のコードを変更したりライブラリを入れ替えたりする作業が必要になる。

これはソフトウェアエンジニアとして仕事をしていれば当然直面する課題であり、都度対応していかないといけない問題でもある。

しかしながら、一度コードを変更するときにその影響範囲について必ず調査をする。 なぜなら、いくらソフトウェアが危険に晒されてるからといって、プロダクトの振る舞いを変えたり、不具合を誘発しては事業そのものに影響が出る。あくまで事業に影響が出ないように、安全にプロダクトを改善するためにも、実際自分が今から変えようとしてるコードはどこに影響が出るのか?ということは必ず事前に調査し、明らかにしておく必要がある。

ちょっとしたコードの変更やロジックの修正、もしくはコードの削除、局所的にしか使われていないライブラリや共通処理の改善であれば話は早い。そこだけ入れ替えれば作業は完了する。

しかし、ちょっとした修正をするつもりが、該当箇所の依存先の実装に問題があったとしたらどうだろう?依存先のコードは、対応したかった箇所とは別の箇所でも使われているかもしれない。依存先の依存先でまた問題のある実装が見つかるかもしれない。 もしかしたら依存先のコードの修正はプロダクト全体に影響が出るところかもしれない。

プロダクトを開発する時には DRY の原則にしたがって実装したコードが、プロダクトを運用する時になって足枷になることは実は頻繁に発生する。

コピペを嫌って脳死で共通化した処理や、DRY の原則に基づいて開発当時は有用だった依存が運用フェーズになって重荷になる、要はプロダクトの中の情報のあり方やドメインが変わってきたときに脳死で DRY したり便利だと思っていた汎用処理がプロダクトの変更の重荷になる、ということが発生する。

※1. そもそもの DRY の原則とは「単なる重複をなくすことでない」ので誤認なきよう -> Don't repeat yourself - Wikipedia

意味が違うコピペ

繰り返しになるが、そう入っても同じ処理を何度も書くのは面倒くさい。それはそうだと思う。

意味で考えるにはコードそのものよりも HTML で考えてみるとわかりやすい。
(これは前に僕の尊敬するエンジニアから教わった例えをそのまま記載している。)

<h1>見出し1</h1>
<h2>見出し2_1</h2>
<h2>見出し2_2</h2>

という HTML の構造を考えた時に h2は同じことが書かれてるな? と思って

<h1>見出し1</h1>
<h2>
    見出し2_1
    見出し2_2
</h2>

こんな風に共通化する人はいないだろう?
DRY は極端にいえばこういうことすら肯定してしまう原則とも捉えられかねない。

h1 と h2 で意味するところが違う。それどころかページのレイアウトそのものにも影響を与えてしまう。

同じ処理だから脳死で共通化するとこういうことすら肯定してしまう。
重要なのは「共通化の意味」をちゃんと考えることだ。分かれていることに意味があるならそれは「意味のある冗長」であり、共通化するべきでない。

HTMLでの例えは多少乱暴かもしれないが、プロダクトのコードを書いてるとふとこうしたことを平気でしてしまいそうなくらい、共通化の悪魔の引力は強い。

処理として同じ文脈で同じ処理を書いているなら共通化するのが原則だが、文脈が違うなら共通化はするべきではない。

依存先の継続的な更新がある場合にのみ依存する

通化処理については上記に述べたとおりであるが、広く使われているOSSや自作ライブラリに依存するケースについて考えてみる。

自分はこれについては1つの見解を持っていて、見出しにあるように「継続的な更新が提供される場合のみ汎用的なライブラリに依存するべき」と考える。 継続的な更新が提供されない限りは、自作ライブラリすら作らない方がいい、という見解で、もし作るのであれば、プロダクト開発から離れてもなお、メンテナンスされ続ける体制を作っておくくらいのことをして初めて自作ライブラリをプロダクトに適用良いのだと思う。

おそらくこれを満たすものというのは、AWSGCP謹製のライブラリや、OSSとして広く定着しているものに限られる。個人で作ってるプロジェクト等のライブラリに依存するのは極力避けておくほうが、開発時期は負担が大きいかもしれないが、長期的な運用まで見据えると旨味が大きい。

結局大事なのは捨てやすさ

まぁそうは言ってもスケールする、継続するかもわからないプロダクトの開発において大事なのはスピードで、運用を見据えて多少冗長な実装をとって時間を食ってしまうことがそもそも事業的に許容されないケースというのは絶対に存在する。
※ 自作のライブラリのメンテナンスなんか考えてる人はさらに稀かもしれない。

実際に困った時になって「依存してるのが辛いな〜」とか「冗長に書いても良かったんじゃないかな〜」と思うことがほとんどだと思う。とすれば1つの観点として持っておきたいのは、依存や共通化はしてもいいけど、それが「どれだけ捨てやすい」のか?ということではないだろうか?

影響範囲を局所的にしておく、捨てやすい(or 入れ替えやすい)ように作っておくなど、プロダクトを開発するときに頭の片隅に置いておきたい観点は存在するので、それを引き出しとして持っておくことが大事なのかなと思う。

まとめ

当初書きたかった内容からは多少逸れてしまったが、自分のキャリアで大規模なプロダクトの開発を経験する中で Go の A little copying is better than a little dependency. という諺の言わんとしてるところの片鱗を考え始めるようになった。

ほんと、プロダクト開発は奥が深くて面白い。

プライベートリポジトリを go get する

Overview

go get でプライベートリポジトリを fetch するのに、コケてその調査で時間を溶かすことが何度か重なったので対応方法について記載します。

またプライベートリポジトリに依存してるプロジェクトで Github Actions を回すときもプライベートリポジトリの取得部分でハマったのでそちらについても記載します。

プライベートリポジトリを go get する

何もしないままプライベートリポジトリを go get すると

go get -u github.com/PRIVATE_REPO
github.com/PRIVATE_REPO@vX.X.X: verifying module: github.com/PRIVATE_REPO: reading https://sum.golang.org/lookup/github.com/PRIVATE_REPO: 410 Gone
    server response: not found: github.com/PRIVATE_REPO: invalid version: unknown revision v0.0.1

というようにエラーが出て module を fetch することはできません。

これを解消するには以下の3つの設定を行います。

  • https の代わりに ssh を使用。
    • git config --add --global url."git@github.com:".insteadOf https://github.com
  • GOPROXY を direct に設定。
    • export GOPROXY=direct
  • GOPRIVATE にプライベートリポジトリを設定。
    • export GOPRIVATE=github.com/PRIVATE_REPO

Github Actions でプライベートリポジトリを go get する

Github Actions でも何もせずにプライベートリポジトリを pull しようとすると以下のエラーに遭遇してmodule を取得することができせん。

go: github.com/PRIVATE_REPO: invalid version: git fetch -f origin refs/heads/*:refs/heads/* refs/tags/*:refs/tags/* in /home/runner/go/pkg/mod/cache/vcs/3703b101c339d9fd970d99639a980444379be4a036303c73173fe24bc3ab8ac5: exit status 128:
    remote: Invalid username or password.
    fatal: Authentication failed for 'https://github.com/PRIVATE_REPO'

プライベートリポジトリを fetch するためには Actions がプライベートリポジトリへのアクセス権限を持っているが必要になります。
このため、Actions でビルドされるステップの中でビルドに使われているコンテナ内の git の設定でアクセス権を付与します。

アクセス権についてはプライベートリポジトリを見ることができるユーザーごとの Personal Access Token を取得します。 取得方法については Setting > Developer setting > Personal access token からユーザーごとのアクセストークンを払い出すことができます。この時に repo にチェックをつけて access token を生成し、それを Actions の secrets に加えます。

Actions を定義している yml には以下の設定が追加されます。

jobs:
  build:
    steps:
      - name: github private modules access
        run: git config --global url."https://${{ secrets.GO_MODULES_TOKEN }}:x-oauth-basic@github.com/".insteadOf "https://github.com/"

リポジトリの Settings > Secrets で生成した個人の Access Token を GO_MODULES_TOKEN を key にして追加します。

これで Github Actions 上でもプライベートリポジトリを fetch できるようになります。

See Also

python のバージョン起因で gcloud コマンドの実行に失敗する

Overview

タイトルの通りです。

gcloud components update したら python のバージョン違いで gcloud コマンドが動かなくなったのでその解決方法について記載します。

エラーの内容

Traceback (most recent call last):
  File "/Users/$UserName/google-cloud-sdk/lib/gcloud.py", line 104, in <module>
    main()
  File "/Users/$UserName/google-cloud-sdk/lib/gcloud.py", line 62, in main
    from googlecloudsdk.core.util import encoding
  File "/Users/$UserName/google-cloud-sdk/lib/googlecloudsdk/__init__.py", line 23, in <module>
    from googlecloudsdk.core.util import importing
  File "/Users/$UserName/google-cloud-sdk/lib/googlecloudsdk/core/util/importing.py", line 23, in <module>
    import imp
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/imp.py", line 23, in <module>
    from importlib import util
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/importlib/util.py", line 2, in <module>
    from . import abc
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/importlib/abc.py", line 17, in <module>
    from typing import Protocol, runtime_checkable
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 26, in <module>
    import re as stdlib_re  # Avoid confusion with the re we export.
  File "/usr/local/Cellar/python@3.9/3.9.0_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/re.py", line 124, in <module>
    import enum
  File "/Users/$UserName/google-cloud-sdk/lib/third_party/enum/__init__.py", line 26, in <module>
    spec = importlib.util.find_spec('enum')
AttributeError: module 'importlib' has no attribute 'util'

要は python 3.9 には Google Cloud SDK は対応してねーよってことらしいです。

stackoverflow.com

対応

どうやら brew を使って python をインストールしていると意図せず裏側で最新の python が最新にあってしまっていたようで( brew upgrade した時とかの弊害) gcloud コマンドから参照する python のバージョンが最新にあってしまっていました。

これを解決するためにたまたま pyenv を入れていたいので pyenv 経由で 3.8 をインストールして合わせました。

pyenv install 3.8.0
pyenv global 3.8.0
source .zshrc
python --version
Python 3.8.0

これで再度 gcloud コマンドを実行したら正常に動作しました。

Firestore の Go SDK でドキュメントを操作する

Overview

firestore でドキュメントを操作するベースとなる方法を記載します。 ※ Go の実装で書いてます。

ref: https://godoc.org/cloud.google.com/go/firestore

Firestore の基本的な操作

取得

Get

ctx := context.Background()
client, err := firestore.NewClient(ctx, "$projectID")
if err != nil {
    // TODO: Handle error.
}
ref, err := client.Collection("$CollectionName").Doc("$DocumentID").Get(ctx)
if err != nil {
    // TODO: Handle error
}

ref: https://godoc.org/cloud.google.com/go/firestore#DocumentRef.Get

GetAll

ctx := context.Background()
client, err := firestore.NewClient(ctx, "$projectID")
if err != nil {
    // TODO: Handle error.
}
dss, err := client.Collectipn("$CollectionName").Documents(ctx).GetAll()
if err != nil {
    // TODO: Handle error
}

dsts := make([]*DistStruct, len(refs))
for i, ss := range dss {
    var dst = DistSturct{}
    if err := ss.DataTo(&dst); err != nil {
        // Handle Error
    }
    dsts[i] = &dst
}

ref:

追記

公式のドキュメントに記載されている実装方法は https://godoc.org/cloud.google.com/go/firestore#DocumentIterator を取得してからループで一つ一つマッピングしていく実装方針が記載されていますが、https://godoc.org/cloud.google.com/go/firestore#DocumentSnapshot を先に取り出したほうがその後 Slice にマッピングする時に Slice を length 指定でメモリ効率化できるので実装方針としてはそちらを採用する方がいいのでは?と思いました。

Save

ctx := context.Background()
client, err := firestore.NewClient(ctx, "$projectID")
if err != nil {
    // TODO: Handle error.
}
src := map[string]interface{}{}
ref, result, err := client.Collection("$CollectionName").Add(ctx)

ref: https://godoc.org/cloud.google.com/go/firestore#CollectionRef.Add

更新

ctx := context.Background()
client, err := firestore.NewClient(ctx, "$projectID")
if err != nil {
    // TODO: Handle error.
}
updates := []firestore.Update {
    {Path: "$updateTargetField", Value: interface{}{} }
}
if err := client.Collection("$DocumentName").Update(ctx, updates); err != nil {
    // TODO: Handle error.
}

ref: https://godoc.org/cloud.google.com/go/firestore#DocumentRef.Update

firestore.FieldPath

Update 構造体の中に FieldPath がありますが、ある Document 内部の Filed が入れ子の場合に特定の field を更新したい場合に使います。

ref: https://firebase.google.com/docs/reference/node/firebase.firestore.FieldPath

 // A FieldPath is a non-empty sequence of non-empty fields that reference a value.
 //
 // A FieldPath value should only be necessary if one of the field names contains
 // one of the runes ".˜*/[]". Most methods accept a simpler form of field path
 // as a string in which the individual fields are separated by dots.
 // For example,
 //   []string{"a", "b"}
 // is equivalent to the string form
 //   "a.b"
 // but
 //   []string{"*"}
 // has no equivalent string form.
 type FieldPath []string

ref: https://github.com/googleapis/google-cloud-go/blob/master/firestore/fieldpath.go#L31-L43

そのため以下のような構造のドキュメントを考えた時に

type Article struct {
    User User `json:"user" firestore:"user"`
}

type User struct {
    Name string `json:"name" firestore:"name"`
    Age    int64  `json:"age" firestore:"age"`
}

Article 内部のユーザーの名前を変更したい時に以下のような FieldPath を組み立てることになります。

fp := []string{"user", "name"}
update := []firestore.Update {FieldPath: fp, Value: "Taro"}
if err := client.Collection("Article").Update(ctx, update); err != nil {
    // TODO: Handle error.
}

Transaction

if err := client.RunInTransaction(ctx, func(ctx context.Context, tx *firestore.Transaction){
    // Transaction 
}); err != nil {
    // TODO: Handle error.
}

ref: https://godoc.org/cloud.google.com/go/firestore#Transaction

指定したディレクトリを goimports の対象から外す

import 順を自動で sort するときに、goimports の対象から外したいディレクトリがあるときに $GOPATH/src/.goimportsignore を設定するといいらしいけど、リポジトリごとのディレクトリ分けに依存したり、個人の開発環境に依存するかなーと思ったで、なんとかならないなかーと思ってとりあえずの対策として

gist.github.com

というのを作りました。

MySQL Driver で time.Time 型をパースする

以下のような Struct を考える。

type User struct {
    ID int64
    Name String
    CreatedAt time.Time
    UpdatedAt time.Time
}

time.Time の時刻型をもつフィールドがある時、MySQL の時刻型のカラムをそのまま Scan しようとすると以下のエラーが出る。 sql: Scan error on column index 3: unsupported Scan, storing driver.Value type []uint8 into type *time.Time

解決策としては接続する時に ?parseTime=true をつける。

https://stackoverflow.com/questions/45040319/unsupported-scan-storing-driver-value-type-uint8-into-type-time-time が参考になる。

久しぶりに sqlx を使ったら色々忘れてた

Overview

すごい久しぶりに sqlx を使ったら色々忘れてたので備忘録です。

github.com

driver は blank import しておかないといけない

driver の種類は下記を参照 https://github.com/golang/go/wiki/SQLDrivers

例えば MySQL を Driver として選択したい場合には MySQL を使うパッケージで

import _ "github.com/go-sql-driver/mysql"

とする必要があります。
これは別に sqlx に限った話じゃないですが完全に忘れてました笑

StructScan に Slice は当てられない

sqlx のメリットの1つに db.Query を使ったときにマッピングするカラムを全て指定しないといけないと言う標準の sql/database のデメリットを回避し、DB のテーブルに対応する struct を定義してき、db.StructScan を使うとマッピングを自動でやってくれると言うのがあると思いますが、この db.StructScan には Struct 以外を当てることはできません。

Slice で取り出したいときは以下のようにします。

type User struct {
    id ing64 `db:"id"`
    name string `db:"name"`
    age int64 `db:"name"`
}

rows, err := db.Queryx("SELECT * FROM user WHERE age > 20")

users := make([]*User, 0, 0)

for rows.Next() {
    user := User{}
    if err := db.StructScan(&user); err != nil {
        // handle error
    }
    users = append(users, &user)
}

追記: README 見たら db.Select が使える。ORM っぽく使いたいならアリかも。

フィールドを指定しないと missing destination name エラーが発生する

取り出したいカラムは指定しろってことですね。 つまり以下のようなクエリは発行しても struct にマッピングできません。

テーブル: User
- id INT
- name VARCHAR
- age INT
type User struct {
    ID int64 `db:"id"`
    Name string `db:"name"`
}
 
rows, err := db.Queryx("SELECT * FROM user where id = ?", 1)
if err != nil {
    // error handling
}
user := User{}
for rows.Next() {
    if err := rows.SturctScan(&user); err != nil {
        // missing destination name age が発生する 
    }
}

取り出したいカラムを制限したい場合はクエリで取り出すフィールドを指定する必要があります。

type User struct {
    ID int64 `db:"id"`
    Name string `db:"name"`
}
  
rows, err := db.Queryx("SELECT id, name FROM user where id = ?", 1)
if err != nil {
    // error handling
}
user := User{}
for rows.Next() {
    if err := rows.SturctScan(&user); err != nil {
      // missing destination name age が発生する 
    }
}