Cloud Run を使う

別に目新しいこともないですが、Cloud Run の走りだけ使ってみたので雑な備忘録。

Docker image を用意して gcr に上げる

起動したい Docker image を作成して gcr.io に上げておきます。
Cloud Run を起動するときは gcr.io 上に上がっている image のみ選択できます。

起動

Cloud Run を起動するときには http server になれる && Cloud Run の用意する port が空いている image を用意しないといけません。
試しに適当な nginx の image を Docker Hub から持ってきて gcr に上げて指定 -> 起動を試みましたがダメでした。

カスタムドメイン設定

Cloud Run のコンテナ一覧の画面に MANAGE CUSTOM DOMAIN というボタンがあり、こちらからサブドメインを設定できます。
Mapping Custom Domain の詳細は https://cloud.google.com/run/docs/mapping-custom-domains に記載てあります。

SSLの設定が必要そう。あとで調べる。

はまったところ

Docker にハマる

Docker 使い慣れてないので Cloud Run 以前に Docker にハマりました。。。 特に gcr に上げ直すとき...w

docker tag {{ old name(Docker Hub から pull してきたときの image 名) }} {{ new name (gcr.io.. を指定する) }}

で image の名前を変更しないとそもそも gcr に上げられなかったという....。

権限でハマる

Cloud Run を起動するときに、IAMの権限の大きさによって認証必要なコンテナか無認証でアクセスできるコンテナを起動するのかを選ぶことができます。
また特定の role 以上の権限を持ったアカウントでないと起動直後に発行されるURLへのアクセスすらできません。

role については以下にまとまっています。
https://cloud.google.com/run/docs/reference/iam/roles

Github Actions (β) を使う

Github Actions (β) がリリースされていたので、せっかくなので自分が現在作成しているプロジェクトをネタに Github Actions の設定を行ってみました。

まずはβに参加する

help ページから Github Actions に参加します。

https://help.github.com/ja/articles/about-github-actions#about-github-actions

このページの GitHub Actionsの限定パブリックベータへの参加をリクエストする というところから参加可能です。

使えるようになったら登録してるメールアドレス宛に連絡がきます。

設定

Github Actions が使えるようになるとリポジトリの上部に Actions タブが出てくるのでそこから設定を行います。

今回は今自分で作成している Go で書かれたプロジェクトを使って Github Actions の設定をしてみます。

ます最初に言語ごとに指定された「Set up workflow」を押下します。
押下すると {{ Project Name }}/.github/workflows/{{ $lang }}.yaml というファイルが生成されます。

Go を選択した場合、なぜか標準では dep を使うようになっているので通常の go get で依存関係を取得するように修正します。

デフォルトで作成される Workflow を定義した yaml ファイルは以下

name: Go
on: [push]
jobs:

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:

    - name: Set up Go 1.12
      uses: actions/setup-go@v1
      with:
        go-version: 1.12
      id: go

    - name: Check out code into the Go module directory
      uses: actions/checkout@v1

    - name: Get dependencies
      run: |
        go get -v -t -d ./...
        if [ -f Gopkg.toml ]; then
            curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
            dep ensure
        fi

    - name: Build
      run: go build -v .

Install Dependencies で go modules を使うことと、今回はテストを走らせたいだけなので Build を step に追加するのではなく、Test を step に追加します。

詳細な Workflow syntacs は こちら に記載してあります。

設定して commit を行うとActions を設定したファイルが作成されます。

{{ $Project }}/.github/workflows/{{ $lang }}.yaml があると自動的に Actions が走ります。
(どうやらファイル名はなんでも大丈夫なようです。追記あり)

あとは Actions タブを見ると自動的に設定した通りに自動でワークフローが実行されているのがわかります。

サンプル: https://github.com/emahiro/glc/actions

まとめ

Github Actions は想像以上に簡単にセットアップできました。
Github 謹製のツールなので CI から解放されるのもとてもいいなと思います。

早く正式版のリリースがされてほしいなと感じました。

追記

複数 の Goのバージョンでテストする

複数のGoのバージョンに対応させた設定ファイルを用意します。

ref: https://github.com/emahiro/glc/tree/master/.github/workflows

imageについて

現状 github の公式が指定してる環境しか作ることはできません。
今後 image を指定できるようになってほしいなと思いました。特殊な環境 (appengine とか) を作るのがまだめんどくさそう...

やってる人とかいるのかな。

GoogleCloudSDK アップデート後に aetest でインスタンスの起動失敗時の対処

2019/10/09 Google Cloud SDK を v266.0.0 に更新すると aetestの更新せずとも直ります。 2019/10/08 google.golang.org/appengine を最新版にすると本問題は解決します。
※詳しくは下部の追記参照

Google Cloud SDK を最新版にアップデートした際に aetest を使っていた appengine のテストが unable to find admin server URL と言われて落ちる(= 動かなくなってしまった)ようになってしまったので、その対処方法について記載しいます。
※ 最新版は Google Cloud SDK 265.0.0 です。

gcloud version
Google Cloud SDK 265.0.0

結論から先に言うと、最新の Google Cloud SDK のアップデートによって goapp コマンドが SDK から削除された(= Go1.9 のランタイムのローカルでの非サポートになった)ことにより、 appengine/aetest が壊れたことが原因です。

このエントリでは一時的な対処法と調査方法について記載します。

対処法

先に対処法について記載します。 aetest が起動しなくなったのは Google Cloud SDK が更新されたことによって、ローカルでも第一世代の appengine が起動しなくなったことが原因です。

そのため対処法は以下の2点のどちらかになると思います。

  • aetest のappengine の設定ファイルを Go1.11 の設定に合わせる
  • dev_appserver.py コマンドの内部で goroot-1.9 を使っている箇所にパッチを当てる。

aetest のappengine の設定ファイルを Go1.11 の設定に合わせる

なぜ appengine が起動しなくなったかは aetest 内の以下の設定を見ればわかります。

const appYAMLTemplate = `
application: %s
version: 1
runtime: go
api_version: go1
handlers:
- url: /.*
  script: _go_app
`

ref: https://github.com/golang/appengine/blob/5f2a59506353b8d5ba8cbbcd9f3c1f41f1eaf079/aetest/instance_vm.go#L278-L287

ここを以下のように Go1.11 に合わせた形式で修正します。

runtime: go111

dev_appserver.py コマンドの内部で goroot-1.9 を使っている箇所にパッチを当てる

※ この方法をとるに至った経緯については後述の調査方法に記載します。

Google Cloud SDKをアップデートしたことにより dev_appserver.py ないの goroot-1.9 を参照してる箇所が壊れてる(参照先のディレクトリが削除されている) ので aetest を走らせた時に appengine が起動する前に aetest が落ちてしまう、と言うのが原因だったので dev_appserver.py に置いて goroot-1.9 を参照してる箇所を変更します。

具体的には ~/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/application.py 内の以下の2箇所です。

@staticmethod
def _get_architecture(goroot):
    # 略
    for platform in os.listdir(os.path.join(goroot, 'pkg', 'tool')): # ← ここの goroot に 1.9 が存在しないのでエラーが発生して appengine が起動しない。
        # Look for 'linux_amd64', 'windows_386', etc.
        if '_' not in platform:
        continue
        architecture = platform.split('_', 1)[1]
        if architecture in architecture_map:
        return architecture_map[architecture]
    raise go_errors.BuildError(
        'No known compiler found in goroot (%s)' % goroot)

なのでこのループに入る前に darwin_amd64 を return 指定しまいます。(Macの場合)
Look for ... のコメントにどんな値が変えるのか記載してあったので多分わかるはず。

もう1箇所に多様な処理をしてる箇所がどうファイル内の def _get_pkg_path(goroot) の内部にもあるので、同様にパス検査のループに入る前に darwin_amd64_appengine で return してしまいます。

上記のパッチを dev_appserver コマンドに適用することで aetest のライブラリを修正しなくてもローカルで aetest の中で dev_appserver のインスタンスが起動するようになります。

調査方法

今回の対処を適用するに当たっての調査方法を記載します。

ここから先は個人の調査記録なので、興味のある方は読んでみてください。
内容はほぼ自分の脳内のダンプです。

ログをみる

まず調べるに当たって、エラーメッセージを見ました。

unable to find admin server URL と言うエラーが発生して appengine が起動しておらずこのエラー自体は以前も見かけたことがあって、素の dev_appsever が起動するかを確認するために普通に dev_appserver.py を叩いて appengine が起動するかチェックします。

これは起動したので、aetest を起動させた時のログをチェックすることにします。

aetest には SuppressDevAppServerLog があり、これは testerator がデフォルトで on にしているので aetest を起動した時に、素の dev_appserver を起動したときのようなログは出てきません。

ref: https://github.com/golang/appengine/blob/a37df1387b4521194676d88c79230c613610d5f4/aetest/instance.go#L31-L33

まずはこのフラグを off にしてログを出力する状態にして再度 aetest を走らせてみます。

go test ./app
# 略

Traceback (most recent call last):
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/dev_appserver.py", line 96, in <module>
    _run_file(__file__, globals())
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/dev_appserver.py", line 90, in _run_file
    execfile(_PATHS.script_file(script_name), globals_)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/devappserver2.py", line 600, in <module>
    main()
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/devappserver2.py", line 588, in main
    dev_server.start(options)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/devappserver2.py", line 360, in start
    options.api_host, apiserver.port, wsgi_request_info_)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/dispatcher.py", line 248, in start
    ssl_port)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/dispatcher.py", line 384, in _create_module
    ssl_port=ssl_port)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/module.py", line 1309, in __init__
    super(AutoScalingModule, self).__init__(**kwargs)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/module.py", line 598, in __init__
    self._module_configuration)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/module.py", line 231, in _create_instance_factory
    module_configuration=module_configuration)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/instance_factory.py", line 137, in __init__
    go_config.enable_debugging)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/application.py", line 118, in __init__
    self._arch = self._get_architecture(self._goroot)
  File "{{ $HOME }}/google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/go/application.py", line 213, in _get_architecture
    for platform in os.listdir(os.path.join(goroot, 'pkg', 'tool')):
OSError: [Errno 2] No such file or directory: '{{ $HOME }}/google-cloud-sdk/platform/google_appengine/goroot-1.9/pkg/tool'
Exception TypeError: "'NoneType' object is not callable" in <bound method DatastoreEmulator.__del__ of <google.appengine.tools.devappserver2.cloud_emulators.datastore.datastore_emulator.DatastoreEmulator object at 0x10b88d250>> ignored
unable to find admin server URL

dev_appserver.py にパッチを当てる

OSError: [Errno 2] No such file or directory: '{{ $HOME }}/google-cloud-sdk/platform/google_appengine/goroot-1.9/pkg/tool'

ログをみたら一発で今回の課題となっていた箇所の原因らしきものが出てきました。 ここからどうやら Google Clodu SDK を更新したタイミングで goroot-1.9 が消えていたので appengine が起動しないと言うことがわかりました。

ここから 対処法2 の対応が思い浮かびます。

aetest の設定を修正する

対処法2 についてはログからなんとかく類推可能だと思うのですが、では 対処法1 に到るまでの過程ですが、goroot-1.9 でうまくいかないと言うところまでわかったので「もしかして App Engine の設定周りとか関係してる?」というところを仮説立てして aetest の設定ファイルをみに行きました。

その結果、第一世代 App Engine の設定のままだったのでここを Go1.11 の設定に書き直してみるか、という方法を思いつきました。

フォーラムやコミュニティに投稿がないかを見る

今回はエラーメッセージはすぐにわかっても最新のSDKの問題ですぐには検索には出てこない内容でした。
また、issue にもすぐには上がってこない内容でもあったので、検索から問題を特定するのはほぼできませんでした。

こうなった時はフォーラムなどのコミュニティに何かとっかかりになる情報が転がってないか見に行きます。

今回のケースに関してはすでに issuetracker に話題が上がっていました。解決方法までは上がってませんでしたが、、、

ref: https://issuetracker.google.com/issues/142004500

ハマったところ

dev_appserver のログを出すために aetest ないの SuppressDevAppServerLog をオフにするところで、プロジェクト内の vendor 配下のフラグをオフにしてましたが、aetest を走らせる時はローカルのモジュールキャッシュの中の aetest(testerator) を使っており、 $GOPATH/pkg/mod 配下にキャッシュされてるファイルを編集する必要がありました。

僕の個人環境の問題かもしれません...。

追記

Google に早く対応して欲しいところなので、続報はこちらに記載します。

追記1

PR は出ていたので早く merge されて欲しいです...
https://github.com/golang/appengine/pull/214

2019/10/08 追記 mergeされていましたので appengine のパッケージを更新するとて aetest でインスタンスが起動するようになります。

go get google.golang.org/appengine@latest

追記2

ノウハウにも情報上がってましたね。

github.com

追記3

devappserver の patch を gist に上げておきました。

https://gist.github.com/emahiro/d0b78d40475300561ad424b0b3741a1c#file-application-py-L194-L241

Struct tag の記法を編集する

Go の struct tag の記法を編集する方法を記載します。

IntelliJVSCode の2つでの設定方法を記載しました。

IntelliJ

IntelliJ はデフォルトの自動 struct tag 補完は snake_case が採用されているのですが、仕事ではほぼ camelCase しか使わないのでなんとか camelCase に変更したいと思ったのが今回のモチベーションです。

詳細は以下のFAQに記載してあります。これをみれば大体OKでした。

intellij-support.jetbrains.com

以下の順で Go Struct Tags を編集します。

f:id:ema_hiro:20191004014858p:plain

f:id:ema_hiro:20191004015030p:plain

f:id:ema_hiro:20191004015648p:plain

f:id:ema_hiro:20191004015750p:plain

VSCode

https://github.com/fatih/gomodifytagsを使います。

これは VSCode に限らず各種 Editor 向けにに Go の Struct tag の自動生成機能を提供してる便利ライブラリです。(後で知りました。)

How to use

手元に gomodifytags を落としてきます。

go get github.com/fatih/gomodifytags

使い方は Ctrl + Shift + P で Add Tag.. を打つと 自動生成してくれるコマンドが出てきます。

f:id:ema_hiro:20191004021343p:plain

ただ、なぜかこれもデフォルトが snake_case で、しかも omitempty までついてきてしまいます。

フォーマットやデフォルトの omitempty をつけたくないので setting.json に以下を追記します。
各記法については gomodifytags の README に記載してますのでそちらを参考にしてください。

"go.addTags": {
    "tags": "json",
    "options": "json=",
    "promptForTags": false,
    "transform": "camelcase"
},

options の指定の仕方は以下の issue を参考にしました。

github.com

ソースコードビューについて考えていること

最近、とある機会にソースコードレビューについて質問されたので、考えてること、意識していることをまとめてみました。
なお、これは私自身の考えていることで人によって異なるところは多いと思いますので一個人のお気持ち表明として読んでもらえればと思います。

目次

  • 自分がソースコードレビューのときに心掛けていること
  • テストを書くことについて
  • まとめ
  • 参照

自分がソースコードレビューのときに心掛けているいること

ソースコードレビューはレビュアーになることもレビュイーになることもあります。そのときに心掛けていることについて記載します。

レビュアーとして

  • コードレビューはバグを見つけるものではないと思ってみること
    • バグの混入はあくまで実装者の責任であって、レビュアーの責務ではない(見つけられたらラッキー程度に思っておく)
  • 極力依頼された瞬間にレビューすること
    • ソフトウェアはチームで開発することが多く、チームの全体最適を考えると極力レビューを依頼されたときにみる方がプロダクト全体で見ると速度が出ます。。これは依頼されたレビューがブロッカーになるときもあるので、ブロッカーは素早く取り除く意味でも依頼されたらまずサッと見る、というのはいいプラクティスだと思っています。
    • もちろん業務状況や勤怠状況によるので必ずこれをしないといけないという訳ではありません。
  • 「指摘」「直して」という言葉を極力使わない。
    • レビューはそもそもレビュアーとしてもレビュイーとしてもコストが高い作業なので、強い言葉は極力使わないようにしています。そのため、自分が依頼されたレビューでは以下のように言い換えるように気をつけています。
      • 指摘 -> コメント、フィードバック
      • 直す -> 取り込む

レビュイーとして

レビューを受けるときはレビュアーからのフィードバックは コードという成果物へのフィードバックであって、人格否定ではない ことを肝に銘じています。
このため 自身の成果物としてのコードに人格を載せない。成果物と人格を同一視しない というスタンスで臨んでいます。

また変更差分には

  • この変更は何か?
  • なぜこの変更をするのか?

をセットでPull Request の description に記載するようにしています。

テストを書くことについて

※ 先に言い訳しておくと、自分はテストについての専門家ではないです。

ソースコードレビューはテストコードとセットにすることが多いと思います。 テストを書くそれ自体については、色んな意見があると思いますが、自分は以下の理由からテストを書いています。

  1. デバックコストを下げること
  2. テスト = ソフトウェアの資産
  3. 「期待する動作をすること」をテストなしに説明するのがめんどくさいこと

デバックコストを下げること

実装の実践的な話になるのですが、開発するときは必ず、必要な要件に対して自分の書いたコードが正しい動作をしているかを検証しながらコードを書くことが多いと思います。
自分は普段 Go で API を書く仕事をしているのですが、実装した Endpoint に対して、

  1. 正常系と異常系のリクエストを作る
  2. local で サーバーを立ち上げる or 開発環境に撒く
  3. curl を使って動作検証を行う

という手順で検証を進めることがありますが、これがマイクロサービスでのAPI開発のケースになってくると local でも認証を突破するためのリクエスト作ったり、デバックのためにわざわざアクセストークン発行したり、といったことをする必要があります。
もちろん、最終的な検証ではこの手順を行いますが、モデルを実装しただけとか、とりあえず正常系だけ書いたので Endpoint の動作を一時的にテストしたいケースにおいてははっきり言ってめんどくさいです。

そういうときに Endpoint のテストを先に書いておいて、テストケースを追加していくだけで意図した動作になっているかはシュッと確認できるので、テストを書きます。

この開発方法を取るようになってから、開発速度が上がり始めたので、テスト書くとスピードが落ちる、は私には当てはまりませんでした。

テスト = ソフトウェアの資産

これはそのままです。この考え方を教わってからテストへの考え方が変わりました。

「期待する動作をすること」をテストなしに説明するのがめんどくさいこと

若干脳死気味でもあるんですが、PullRequest を出すときに、「なぜテストがないのか?」を説明するのがめんどくさいのでテストもセットで書いてます。
テストもなしに「自分の書いたコードは必ず正しく動きます」と文章で説明するくらいなら、テスト書いちゃった方が早いです。きっと。

まとめ

こういった考えに到るまでに様々な変更と、そのレビューを受けてきました。
何度か心が折れかけたこともありましたが、コードという成果物と私自身という人格を分けること を会得したことでかなりコードレビューというものに対してのメンタル的なコストが軽減しました。

このエントリで記載した「意識していること」のいくつかは、仕事をしている中で尊敬するエンジニアの方々からインプットされたもので僕個人の考えという訳ではありませんが、私自身としても大切にしたい考えだと思ってここに記載してます。

refs

google の文書の以下の邦訳はとてもいい文章でした。コードレビューをする・受ける側どちらでも参考になることが多いと思います。

shuuji3.github.io

git subtree コマンドを使う

git subtree という便利なコマンドを教えてもらいました。

submodule と異なり、取りこむ外部リポジトリのコミットも取り込み元のリポジトリの commit に入れるので、取り込んだリポジトリの編集を commit に含めることが可能です。

How to use

help コマンド叩けば大体何ができるかわかります。

git subtree -h
usage: git subtree add   --prefix=<prefix> <commit>
   or: git subtree add   --prefix=<prefix> <repository> <ref>
   or: git subtree merge --prefix=<prefix> <commit>
   or: git subtree pull  --prefix=<prefix> <repository> <ref>
   or: git subtree push  --prefix=<prefix> <repository> <ref>
   or: git subtree split --prefix=<prefix> <commit>

    -h, --help            show the help
    -q                    quiet
    -d                    show debug messages
    -P, --prefix ...      the name of the subdir to split out
    -m, --message ...     use the given message as the commit message for the merge commit

options for 'split'
    --annotate ...        add a prefix to commit message of new commits
    -b, --branch ...      create a new branch from the split subtree
    --ignore-joins        ignore prior --rejoin commits
    --onto ...            try connecting new tree to an existing one
    --rejoin              merge the new branch back into HEAD

options for 'add', 'merge', and 'pull'
    --squash              merge subtree changes as a single commit

外部のリポジトリを取りこむときは

git subtree add --prefix=<取り込み先のpath/$directoryName> <取りこむ流リポジトリのURL> <取り込むリポジトリのbranch or commit hashj>

を叩きます。但しこのままだと取り込み先の外部リポジトリの commit がそのまま取り込み元の親リポジトリの commit に含まれるので --squash をつけると1つの commit にまとまってくれます。

// e.g.
git subtree add --prefix=app/vendor/3rdLibSample https://github.com/hoge/3rdLibSample.git master --squash

Usecase

取り込む外部リポジトリの変更を親リポジトリの commit ログに入れることができるので

  • 外部のライブラリにパッチを当てて検証する

などのケースで力を発揮するのかなと思いました。

自分は普段 Go を書いて仕事をしているのですが、外部ライブラリの修正を開発中のサービスで動作検証したいときに vendor の中身をいじっていたのですが、 git subtree コマンドを教えてもらってからは prefix で指定した path に外部のリポジトリをコピってきてそのリポジトリの中身を編集、go module の replace の機能を使ってビルドするときは prefix で指定した修正後のライブラリを使う、というフローになりました。

以下のエントリにも追記しました。

ema-hiro.hatenablog.com

IntelliJ から VSCode に移行してみる

もともと JetBrains に魂を売っていた人間なのですが、最近4Kの外部ディスプレイに繋ぐとよく固まってしまってキーボード入力に遅延が発生して体験が圧倒的に不安定になってきているので、これを機に VSCode に乗りかえました。

普段は Go を書いているので、VSCode で Go を書く環境設定の手順をメモっておきます。 なお、今回 VSCode を使うかなーと思ったのは、VSCode と Go の Language Server の相性が少し前まであまり良くなく定義ジャンプなどがもっさりしていたので、ちょっと使いづらいなと思っていたのですが、最近改善されたのを聞いて、それなら使えるかなーと思ったのも1つあります。

また僕自身は完全に JetBrains にキーバインドに慣れて凝り固まってしまっていたので、キーバインドも全て JetBrains のそれを踏襲しました。

Install VSCode

これは書くまでもないと思いますので、割愛します。

https://code.visualstudio.com からDLします。

IntelliJキーバインドを設定する

https://marketplace.visualstudio.com/items?itemName=k--kato.intellij-idea-keybindings がありましたので、このプラグインをインストールして終了です。

正直これが一番悩みました。VSCodeキーバインドに慣れようとも思ったんですが、今更なかなか手に馴染んだ癖を矯正するのはしんどく、これがなかったら移行は無理だったかもしれません。

Language Server を設定する

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

"go.useLanguageServer": true,

Language Server の細かい設定がしたい場合は go.languageServerExperimentalFeatures に細かい設定を記載することができます。

"go.languageServerExperimentalFeatures": {
        "format": true,
        "autoComplete": true,
        "rename": true,
        // 略
}

Language Server については公式の README にも記載されてます。

ref: https://github.com/microsoft/vscode-go#go-language-server

generate json tag を設定する

type User struct {
    ID   int64  `json:"id"`
    Name string `json:"name"`
}

Go の struct に json タグを指定することで json で出力するときの key 名を指定できますが、IntelliJ だとstruct の field と同じ行で json.... って打ち込むだけで自動で生成されてくれて(スネークケースでしたけど)、非常に重宝していました。

これと同等の機能をなんとか実現できないものかと調べたところ、公式のドキュメントの https://github.com/Microsoft/vscode-go/wiki/Go-tools-that-the-Go-extension-depends-ongomodifytags for modifying tags on structs という項目があってどうやらデフォでできそう、ということまではわかりました。

実際にどうやって使うのかについては https://github.com/fatih/gomodifytags の本家を見に行きます。

(ここで知ったんですが、この gomodifytags という昨日は各種エディタに提供してるタグの自動生成ツールなんですね....)

本家の README には vscode-go with commands Go: Add Tags and Go: Remove Tags とあるので、struct を記載たら Cmd + Shift + P でコマンドパレットを開いて、Go: Add Tags を探して実行するだけです。普通に Add Tags... と打ち込めば出てくるはず。 タグを削除したいときも同等に コマンドパレットで Remove Tags を探して実行するだけで問題ありません。

ただ1つ問題があって、この gomodifytags を使ってタグを生成するとき、 omitempty がデフォルトだとついてきてしまうので gomodifytags の挙動を設定で制御します。

具体的には以下のような感じです。

"go.addTags": {
    "tags": "json",
    "options": "json=", // json の何もつけない e.g omitemptyをつけたいときは json=omitempty となる。
    "promptForTags": false,
    "transform": "camelcase" // キャメルケースで key 名を記載する e.g. userHoge
},

ref: https://github.com/Microsoft/vscode-go/issues/433#issuecomment-287566310

gomodifytags を使い始めて Intellij より便利だなと感じたのは tag の書式はスネークケースしか指定できなかったのですが、 gomodifytags を使う場合には transform で書式を決めることです。

https://github.com/fatih/gomodifytagsREADME にも書式の記載方法が記載されてます。

その他

言語ごとの細かい設定をする

各言語ごとにタブのサイズとか細かく決めたいケースなどは setting.json

"[$langName]": { // $langName に特定の言語を指定する e.g. ruby, typescript など

}

// e.g.
"[typescript]": {
    "editor.tabSize": 2,
}

こんな感じです。

追記

また色々調べてみて追記できそうなことがあったら随時付け足していきます。

Language Server の挙動の不安定さについて(20190924)

gopls を最新にしたら治るかと思ったんですが、どうやら gopls をアップデートした local の go のversionと setting.json に記載している go の version が一致していないと挙動が不安定(Goのファイルを開いたときに毎回再起動してる?)という状態になりました。

自分の環境で調べただけなので、僕の環境に限った話かもしれませんが。

go get でインストールするツール群を最新にする

goimports, gopls などの go get で入れたツール群を最新版にアップデートする方法

# ツールごとに最新版にする
go get -u golang.org/x/tools/cmd/goimports

# cmd で使えるツール群を一括で最新版にする
go get -u golang.org/x/tools/cmd/...

# いっそのツール群全て最新版にする
go get -u golang.org/x/tools/...

./... で指定するとそのディレクトリ配下全てを対象としてくれるの便利。

slice 操作や検証のあれこれ

ある slice の操作や検証をするときにスッキリ書く方法を教えてもらったので備忘録。

※ 随時更新(忘れて新しくエントリ書くこともあるかも)

重複する要素を持つ slice を弾きたいとき

func duplicate(arr []string) bool {
    base := map[string]int{}

    for _, k := range arr {
        if i := base[k]; i != 0 {
            return true
        }
        base[k]++
    }
    return false
}

base のような slice において []string{"a", "a", "b"} みたいな重複する要素を持つ配列を弾きたい時に使います。

ref: https://play.golang.org/p/bW5YFckAZmB

base の部分も引数に渡してもいいかもしれませんが、ユースケースごとに作ってもいいと思います。
int を使用したのは重複チェックするなら重複した要素をカウントするのがいいかなと思ったためですね。

ただ、場合によっては int より効率いいもの使いたい!っていうケースもあると思うのでそしたら bool を使うのがいいと思います。
typeごとのデータサイズは https://golang.org/src/go/types/sizes.gobasicSizes を見るとわかります。

func deplicate(arr []string) bool {
    base := map[string]bool{}

    for _, k := range arr {
        if ok := base[k]; ok {
            return true
        }
        base[k] = true
    }
    return false
}

ref: https://play.golang.org/p/m_g0u9TCWTX

今思うとこっちの方がスッキリしてますね。

意図しない要素をもつ slice を弾きたいとき

func hasInvalidElement(arr []string) bool {
    base := map[string]int{
        "a": 0,
        "b": 0,
        "c": 0,
    }

    for _, k := range arr {
        if _, ok := base[k]; !ok {
            return true
        }
    }
    return false
}

base にない要素をもつ slice を弾きます。
ref: https://play.golang.org/p/Rnc6SFbx1il

このとき要素の重複は許可する場合 e.g. []string{"a", "a", "b", "c"} には個数のカウントはしません。

そしてこれももっとスッキリ書くなら bool にすると良さそうですね、ってことで書き直してみます。

func hasInvalidElement(arr []string) bool {
    base := map[string]bool{
        "a": true,
        "b": true,
        "c": true,
    }

    for _, k := range arr {
        if ok := base[k]; !ok {
            return true
        }
    }
    return false
}

ref: https://play.golang.org/p/SRGe4ZqeTrr

さらにメモリ効率を良くしたい!と言ったときは bool ではなくて struct{} を使用してもいいかもしれません。

func hasInvalidElement(arr []string) bool {
    base := map[string]struct{}{
        "a": struct{}{},
        "b": struct{}{},
        "c": struct{}{},
    }

    for _, k := range arr {
        if _, ok := base[k]; !ok {
            return true
        }
    }
    return false
}

ref: https://play.golang.org/p/Wrpuc0JTupn

組み合わせ

これらを組み合わせて例えば 要素はお互いがユニークかつ決まった要素が入ってるsliceを検証する と言ったslice の検証要件があったときに以下のようなバリデーションを考えてみます。

func isValid(arr []string) bool {
    base := map[string]bool{
        "a": false,
        "b": false,
        "c": false,
    }

    for _, k := range arr {
        if got, ok := base[k]; !ok || got {
            return false
        }
        base[k] = true
    }
    return true
}

map[string]bool を使って上記のように書くことが可能です。
ref: https://play.golang.org/p/r8qBfhhhq6m

過去に書いた slice 操作系のエントリ

slice の操作は色々考えてみると結構面白いです。 また何か追加するかもしれません。

過去書いた slice 周りのエントリは以下です。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

Go で書いた OSS を公開するまでの備忘録

以下のエントリで触れてますが、GitHubOSSを公開したときの手順を備忘録として記録します。

github.com

手順

  1. GitHubに公開用のリポジトリを作成し、コードをPushする
  2. LICENCE を設定する
  3. ciを連携する
  4. Go Report Card を登録する
  5. Godocを書く

GitHubに公開用のリポジトリを作成し、コードをPushする

Pushするまでは割愛します。

一つハマったことはコード書いてる途中に気づいたんですけど、手元の Go のバージョンが Go1.12 以上の場合、go.mod の設定で go1.12 が付いてきてしまって、Go1.11 でビルドしてるプロジェクトで go get ができなくなりました。go.mod ファイルから go1.12 を消しておくか、今のところは go1.11 で go mod init しておくといいかなと思います。

追記: 僕の手元の環境がおかしいだけだったっぽいです。

LICENCE を設定する

ライセンスを登録します。

ライセンスについては全く意識したことなかったのですが、同僚が OSS 公開したら海外の別のエンジニアにコード丸パクリされたというのを見ていたので、ちゃんと設定しないといけないな!と思って設定方法調べました。
どのライセンスが〜みたいな詳細はここでは触れませんが、今回は MIT を選択しておきました。

GitHub の対象リポジトリから「create new file」 を選択して「license」と入力すると license のテンプレートが出てくるのでライセンスにするか選んで commit します。

f:id:ema_hiro:20190813004734p:plain

f:id:ema_hiro:20190813004745p:plain

ちなみに後から知ったんですが、リポジトリ作成時にライセンス選択できるっぽいです。知りませんでした。

CI を設定する

今回は travis.ci を使いました。

使い方は結構簡単で、travis.ciGitHub を連携させて CI を追加したいリポジトリを選択するだけです。

リポジトリを選択するだけだと CI は回らないので、https://docs.travis-ci.com/user/languages/go を参考に設定ファイルを作ってリポジトリに commit します。
自分は初めてだったので簡単な設定にしました。 ref: https://github.com/emahiro/ae-plain-logger/blob/master/.travis.yml

今回は設定が簡単だったので travis を採用しましたが、ちょうど GitHub Actions で CI/CD がサポートされたタイミングだったのでこっちを使ってもよかったかもしれないなーと思いました。

Go Report Card を登録する

プロジェクト内のコードの品質をオンラインで検査してくれる https://goreportcard.comリポジトリを登録します。

上記のURLにアクセスし、検査したいリポジトリGitHub の URL を入力します。

以下のようにプロジェクト内のコードをオンラインで検査してくれます。

f:id:ema_hiro:20190813004811p:plain

Godocを書く

今回はあまりちゃんとは書いてないですが、公開メソッドや sturct に対してコメントを追加する、という最低限の Godoc の記法に則ったものだけは記載しました。
Google のコードとか読んでるよすごい量のコードコメントが書いてあって圧倒されますが、今回はそこまでしなくていいかなーくらいのノリでした。
少しずつちゃんと書いていきたいなと思ってます。

流石に適当な英語が多すぎ、かつ pacic する可能性があるのにそのことが実装を読まないとわからないのは不親切すぎたので以下の PR で Godoc を拡充しました。

github.com

github.com

何をどう書いたらいいかわからなかったのですでにある godoc を参考にして記載しました。

現在の godoc はこのようになっています。

godoc.org

追加で package document を書きました。

以下のようにパッケージ宣言の上にコメントを記載するとパッケージの説明を godoc が拾ってくれます。

/*
    Package log is ~

    Example:
        ctx := request.Context() // タブで一段下がるとコードレイアウトとして描画してくれます。
*/
package log

画像で示すと以下の部分が package document が反映された部分になります。

f:id:ema_hiro:20190816004835p:plain

f:id:ema_hiro:20190816004848p:plain

ここまで書いて最後に godoc の batch を README.md に追加しました。 ?status.svc をつけて挿入します。

[![GoDoc](https://godoc.org/github.com/emahiro/ae-plain-logger?status.svg)](https://godoc.org/github.com/emahiro/ae-plain-logger)

まとめ

外に出して恥ずかしくないコードを書く、というのは色んな手順を踏む必要がありました。
特に Godoc は普段仕事のコードを書く時にはとりあえず lint を防ぐ目的でしか使ってなかったりするので、ドキュメントを使う人のために書く、というのは慣れずにフィードバックをもらいながら調整していきました。
書きたい文章を Google 翻訳にかけてもイマイチピンとこない英訳だったので、これを機会に普段馴染みのある package の Godoc をひたすら漁ったりしてました。結構いい方法だと思いました。

Godoc を書かないと、Godocの正しい書き方がわからない、ということを学びました。

今回初めて色々やってみたので、次からは迷わずにやっていけそうです。

追記: internal package 化

以下のPRで spancontext package を internal に移動しました。理由は外から触れたくないからです。この辺も internal package は外から触れないことは知ってましたが、意図して internal に囲い込むという手法もライブラリを作ることで学べたことでした。

github.com

追記2: タグの切り方

リリースタグを切るときに vX.X.X のように vをつけないと go.mod でコミットハッシュのバージョン管理をされてしまいます。

vなしでタグを切ったとき

// go.mod
github.com/emahiro/ae-plain-logger v0.0.0-20190815145805-39cea2e23c34

vつきでタグを切ったとき

// go.mod
github.com/emahiro/ae-plain-logger v0.2.0

GAE/Go1.12 において構造化ログを出力する

App Engine 2nd Generation で構造化ログを出力するための ae-plain-logger というライブラリを作成しました。

github.com

ae-plain-logger とは?

ログエントリを json ペイロードとして標準出力に出力します。この時 traceID と spanID をログエントリに含めることで stackdriver logging のGUIで構造化ログとして出力することができます。

サポートしてること

  • 構造化ログを出力すること
    • プラットフォームリクエストログにアプリケーションログが紐づくこと

サポートしていないこと

  • サポートしてること以外の Proprietary App Engine Log API で提供していた機能
    • プラットフォームリクエストログに紐づいたアプリケーションログの中で一番高い severity をプラットフォームリクエストログの severity として伝播させる機能etc...

使い方

README.md にも記載してる通りピュアな net/http を使っているケースのみで How to use で記載してますが、他にもメジャーで使われてる(と思う)WAFでの使い方を記載してみました。
基本的には http.Handler を引数に指定するだけで実装が可能ですので、以下に示している WAF 以外でも使うことは可能だと思います。

Gin

mux := gin.New()
mux.GET("/hello", func(gc *gin.Context) {
    gc.Status(http.StatusOK)
    gc.Writer.Write([]byte("hello"))
    return
})

h := middleware.MwAEPlainLogger("ginRouter")(mux)
server := http.Server{
    Addr:    fmt.Sprintf(":%d", addr),
    Handler: h,
}

go func() {
    if err := server.ListenAndServe(); err != nil {
        log.Fatalf("server closed with error: %v", err)
    }
}()

go-chi

mux := chi.NewMux()
mux.HandleFunc("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("hello"))
}))

h := middleware.MwAEPlainLogger("chiServeMux")(mux)
server := http.Server{
    Addr:    fmt.Sprintf(":%d", addr),
    Handler: h,
}

go func() {
    if err := server.ListenAndServe(); err != nil {
        log.Fatalf("server closed with error: %v", err)
    }
}()

gemux

最近気になってる薄い router の gemux も追加しておきます。

mux := &gemux.ServeMux{}
mux.Handle("/hello", http.MethodGet, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("hello"))
}))

h := middleware.MwAEPlainLogger("chiServeMux")(mux)
server := http.Server{
    Addr:    fmt.Sprintf(":%d", addr),
    Handler: h,
}

go func() {
    if err := server.ListenAndServe(); err != nil {
        log.Fatalf("server closed with error: %v", err)
    }
}()

参考

先駆者の方々のお知恵を色々拝借したり、Googleのドキュメントを真面目に読み込んだりしてました。

追記

追記1.

ドキュメントを読んでて気づいたいんですけど https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry の Payload type の箇所の protoPayload の項目に "type.googleapis.com/google.appengine.logging.v1.RequestLog" が明記されていました。
これはしばらくはこのフォーマットがサポートされる、的な認識を持って良いでしょうかね?

google.appengine.logging.v1.RequestLog をうまいこと自前で組み立てれば、1st Gen に近しいログ出力になりそう、かつ 1st Gen の ログフォーマットに依存した監視設定をしているケースにおいてはそこを移行せずともよくなるので protoPayload を使えると嬉しみがあります。

追記2

第一弾としては構造化ログの出力のみをサポートしましたが、long-running operation 用の filed も用意されているので、こちらにも対応したいなと思ってます。
ref: https://cloud.google.com/logging/docs/agent/configuration#special-fields

勤続2年の経過報告

7月も終わって現所属に異動(転籍)してきてから2年と1ヵ月が経ったので勤続エントリを書いてみます。 (これもいつまで続くかな...笑)

去年のエントリは以下

ema-hiro.hatenablog.com

目次

  • 2年終わって
  • 技術スタック的なあれこれ
  • チャレンジしたこと
  • 次の一年

2年終わってみて

また1年よく生き延びたなと思います。
この1年は前半はフロントエンド周りに手を出しつつ、後半は登壇の機会をもらうなど、幅が広がった1年でした。

技術スタック的なあれこれ

サーバーサイドはGAE/Go、フロントエンドは Vue/Typescript からは基本的には大きくずれてません。

途中少しだけRailsの案件やったりしてましたが、ベースはGoを書いてますし、いつの間にやら2年も Go を書き続けて、なんだかんだキャリアで一番長く接した言語になりました。
(今まではPHPがキャリアの経験では最長でした。)

2年も Go にどっぷり浸かってましたが、一向に上達する気配がありません。

インフラもほぼほぼPaaSしか触れておらず、それもApp Engineのみを触ってきていて、他のインフラやクラウドは全く触ってません。
App Engine だけでも十分奥深く、面白く、そしてやっぱりよくわからんとなってます。

最近は第二世代 App Engine へのマイグレーションをやっています。結構大変ですが、面白いです。
あとこれだけはいっておきたいですが、App Engine は最高のツールです。

チャレンジしたこと

以下の二つが大きかったかなと思います。

  1. モダンなフロントエンドの挑戦
  2. 登壇

モダンなフロントエンドへの挑戦

去年の年末のエントリにも記載しましたが、モダンなフロントエンド(Vue + TypeScript)にチャレンジできたことはよかったです。
プロダクトとしてリリースするところまで持っていけたので一つスキルセットとして進歩した感じあります。

登壇

  • 社内勉強会 × 2
  • Go Conference 2019 Spring

エンジニアのキャリアの中で初めてこういった人前で発表する機会をもらえました。
とても緊張しましたし、まだまだ至らないところがあるなという反省もありつつ、チャンレンジできてよかったです。 そして、会社としてもチャレンジさせて貰えたことに感謝です。

半年で3回も人前で話すことを経験したら、流石に少しだけ度胸がつきました。
また、Feedbackをもらうことが恐くなくなった(Feedbackを歓迎するスタンス) のもいいメンタル面の変化でした。ちなみにFeedbackは怖くなくなりましたが、間違うと恥ずかしいのは変わりません。

ema-hiro.hatenablog.com

ema-hiro.hatenablog.com

次の一年

引き続き今のスキルセットをベースにして、大規模開発に必要な知見を貯めていきたいなー思ったりしてます。
飽きっぽいのでまた来年は全く違うことしてるかもしれませんが。。。

Intellij で自動 import 折り畳みをoffる

Go で import 順を揃えたい(standard -> 3rd -> local の順)のに、intellij でファイルを開くと最初から自動で import が折りたたまれてて、レビューでフィードバックされるまで気づかないことがよくあったのでその設定方法を off にする方法がないか調べました。

intellij-support.jetbrains.com

このFAQに記載されている内容通りにやればうまくいきました。

Preferences > Editor > General > Code Folding を選択して、Import のチェックを外します。

image
image

CircleCIの高速化ログ ~キャッシュ先を変更する~

ema-hiro.hatenablog.com

これの追記です。

ディレクトリ構造を変更した時に save_cache ディレクティブで新しいキャッシュ先を指定しても restore_cache で新しいキャッシュ先ではなく古いキャッシュが利用されてしまうケースがあってハマってました。

結果としてディレクトリ構造の変更を行うなどして別のpathに新しくキャッシュを作成する場合 pathsだけでなくキャッシュのkey名も変更が必要 という初歩的なことを忘れてました。

こんなんで結構時間を消費してしまってマジで「ぐぬぬ...」という感想しかありませんでした。という備忘録。

IntelliJを CLI から起動する ~ Tool Box篇 ~

ema-hiro.hatenablog.com

以前このエントリーを書いたのですが、ここ最近 JetBrains の Tool Box を使い始めたときに従来の方法で Command Line Tool が作れなくなっていたので、再度作成するための手順を記載します。

手順

IntelliJから作成する

f:id:ema_hiro:20190625015305p:plain

Tool > Create Command-Line Launcher... とすると

f:id:ema_hiro:20190625015348p:plain

上記のような通知が出る。どうやら Tool Box 配下で Jetbrains のアプリケーションを管理している場合、Tool Box 経由で Command-Line Launcher を作成しないいけないらしい。

説明は以下の Jet Brains の公式ドキュメントに記載してありました。

blog.jetbrains.com

この手順に沿って Tool Box 自体の設定画面から Generate shell scripts のボタンを ON にします。その次に、作成するコマンドラインの PATH を設定します。PATHは /usr/local/bin でいいと思います。ここはなんでも大丈夫です。
※ このとき、Tool Box で管理する前に作成したスクリプトがある場合は削除しておきます。

f:id:ema_hiro:20190625015811p:plain

PATH を指定したら Tool Box を再起動します。これで新しくコマンドが作成されていれば完了です。

f:id:ema_hiro:20190625015913p:plain