emahiro/b.log

Drastically Repeat Yourself !!!!

AppEngineで静的ファイルをサーブする設定について

ふとした拍子に忘れてしまうappengineでの静的ファイルをサーブする設定についてのメモです。

内容はほぼ Serving Static Content  |  App Engine standard environment for Go  |  Google Cloud に載ってる内容です。

例えば以下のようなディレクトリ構成を考えてstaticディレクトリとassetsの2つのディレクトリに静的ファイル( jsとかcssとかimgといったファイル )を保持してwebサイト上で使いたいケースを考えます。

GOPATH
├── assets
│   └── index.html
├── src
│   └── app
│       ├── app.yaml
│       └── main.go
└── static
    └── index.html

まずは適当にサーバーを用意します。

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

// App Handle Object
type App struct{}

// Index App Root Handler
func (a *App) Index(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("hello world"))
}

func main() {
    app := &App{}
    mux := http.NewServeMux()
    mux.HandleFunc("/", app.Index)

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("Defaulting to port %s", port)
    }

    log.Printf("Listening on port %s", port)
    if err := http.ListenAndServe(fmt.Sprintf(":%s", port), mux); err != nil {
        panic(err)
    }
}

次にapp.yamlにサーブしたいファイルのディレクトリを指定します。せっかくなのでgo111でやってみます。
go111ではアプリケーションを作成するときのディレクトリ構成が 1.9以下のそれとは異なっているので例えば1.9の環境で以下のようなディレクトリ構成を考えると

GOPATH
├── assets
│   └── index.html
├── app.yaml
└── static
    └── index.html

このケースではにはyamlに定義する handlers の urlディレクティブのstatic_dir(実際サーブしているファイルのpathを指定するproperty)はapp.yamlと同階層にあるので以下のように記載しました。

runtime: go1.9
handlers:
  - url: /static
    static_dir: static
  - url: /assets
    static_dir: assets
  - url: /.*
    script: auto
  

ちなみに 1.9以下で指定するときは 最後の /.* より上部にサーブしたいディレクトリのルーティングの設定を書かないと正確にルーティングされません でした。なぜかは謎です。

go111の環境ではディレクトリ構成が異なります。最初に示した通り、go111ではapp.yamlはmainパッケージと同階層にあることが必要です。
そのため、app.yamlと実際にサーブするディレクトリは1.9以下のように同階層にあるわけではありません。(同階層にあるとした場合)

app.yamlから静的ファイルをサーブするディレクトリを指定するケースにおいてはそのディレクトリは相対パスで指定することになりますので以下のようになります。

runtime: go111
handlers:
  - url: /static
    static_dir: ../../static
    secure: always
  - url: /assets
    static_dir: ../../assets
    secure: always
  - url: /.*
    script: auto
    secure: always

※ go1.9の時にあった /.* より上に静的ファイルをサーブするディレクトリを記載しなくてもgo111のケースではちゃんと動作してくれました。

この状態で実際にgo111の環境でアプリを起動します。

$ dev_appserver.py src/app/app.yaml
$ curl -i -X GET http://localhost:8080/static/index.html
HTTP/1.1 200 OK
Content-length: 41
Content-type: text/html
ETag: "LTgyNzU4ODE4Ng=="
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Cache-Control: public
Server: Development/2.0
Date: Mon, 19 Nov 2018 17:42:50 GMT

<html>
  <p>hello static dir</p>
</html>

$ curl -i -X GET http://localhost:8080/assets/index.html
HTTP/1.1 200 OK
Content-length: 41
Content-type: text/html
ETag: "MTc0MjM0NzkzMg=="
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Cache-Control: public
Server: Development/2.0
Date: Mon, 19 Nov 2018 17:42:48 GMT

<html>
  <p>hello assets dir</p>
</html>

正常に静的ファイルがルーティングされてることが確認できました。
追加で app.yamlの各propertyの意味は app.yaml Configuration File  |  App Engine standard environment for Go 1.11 docs  |  Google Cloud にあるので目を通してみると良いと思います。

ちなみに今回使ったhandlerには以下のように記載してあります。

Optional. A list of URL patterns and descriptions of how they should be handled. App Engine can handle URLs by executing application code, or by serving static files uploaded with the code, such as images, CSS, or JavaScript.

静的ファイルをサーブする時に使うpropertyと記載ありますね。
handlers propertyの要素については app.yaml Configuration File  |  App Engine standard environment for Go 1.11 docs  |  Google Cloud にあるのでこちらも合わせて目を通してみると良いと思います。

app.yamlのhandlers要素の最後に script: auto と書いてますが、これはドキュメントに以下のような静的ファイルをサーブするケースでの記載に則ったものです。

Optional. Specifies that requests to the specific handler should target your app. The only accepted value for the script element is auto because all traffic is served using the entrypoint command. In order to use static handlers, at least one of your handlers must contain the line script: auto to deploy successfully.

script: auto を設定してねと記載があるのでこれはおまじないのように必須の項目のようです。localで起動する分には必要ありませんが。

[追記]

1.9以下で指定するときは 最後の /.* より上部にサーブしたいディレクトリのルーティングの設定を書かないと正確にルーティングされません でした。なぜかは謎です。

これ、そもそも /.* に食われてその先(このルーティングより下部に設定したurl) は読み込まれないとFBもらいました。(そんな仕様知らんがな)

AppEngineのGo1.11のランタイムを簡単に試してみた

GAE/Go1.11のランタイムを実際にデプロイまでしてみましたよっていう内容です。

準備

ディレクトリ構成

以下のようなディレクトリ構成でプロジェクトを作成しました。

$GOPATH
└── src
    ├── app
    │   ├── app.yaml
    │   └── main.go
    └── handler
        ├── index.go
        └── init.go

server準備

GAE/Go1.11からは標準の net/http パッケージが使えますので、http.ListenAndServe で簡単なweb server を書きます。

routerの実装

// app/main.go

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "handler"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler.Index)

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("Defaulting to port %s", port)
    }

    if err := http.ListenAndServe(fmt.Sprintf(":%s", port), mux); err != nil {
        panic(err)
    }
}

※ main.goでportを環境変数から取り出してますが、この PORT はAppEngineのデフォルトで指定されてるものなのでこのまま使えます。

handlerの実装

// handler/index.go

// Index ...
func Index(w http.ResponseWriter,r *http.Request){

    if r.URL.Path != "/" {
        http.NotFound(w,r)
        return
    }

    w.WriteHeader(http.StatusOK)
    if _, err := w.Write([]byte("hello appengine go1.11 world!!!")); err != nil {
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
}

local で起動

goapp serve app.yaml が使えなくなっていたので、gcloud component の中に入ってくる dev_appserver.py を使います。
※ 事前に dev_appserver.py を gcloud コマンドでinstall -> pathを通しておくといいです。

$ dev_appserver.py src/app/app.yaml

INFO     2018-10-28 11:25:04,061 devappserver2.py:278] Skipping SDK update check.
INFO     2018-10-28 11:25:04,213 api_server.py:275] Starting API server at: http://localhost:64860
INFO     2018-10-28 11:25:04,281 dispatcher.py:270] Starting module "default" running at: http://localhost:8080
INFO     2018-10-28 11:25:04,283 admin_server.py:152] Starting admin server at: http://localhost:8000

local packageの初期化

main package以外の各パッケージも main で初期化するものかと思ってましたが、init で初期化できました。
各packegeで初期化時に行いたい処理を各場合は init に書けば良さそうです。

// handler/init.go

func init(){
  // sample的に log とconsoleに出力させる。
    log.Printf("init")
}

Deploy

appcfg およびそのラッパーコマンドである goapp が使えなくなり gcloud app deploy のみしか使えなくなっていますのでこちらのコマンドでデプロイを行います。

事前にdeploy対象のプロジェクトにログインをしておきます。

$ gcloud auth login

Deploy準備

デプロイ前に以下のファイルを設定します。

  • setting app.yaml
  • setting .gcloudignore

setting app.yaml

app.yaml Configuration File  |  App Engine standard environment for Go 1.11 docs  |  Google Cloud を参考にするといいです。

基本はruntimeとservice名だけ入れておくとdeploy時に自動でどのサービスにデプロイするのか解釈してくれるので便利です。

runtime: go111
service: YOUR-SERVICE-NAME

setting .gcloudignore

gcloud app deploy を使う場合、従来の skip_files は使えず、deploy時にアップロードしたくないファイル群は .gcloudignore ファイルを使って管理します。

Deploy

$ gcloud app deploy --project [YOUR_PROJETCT_ID] --version [VESRION_NAME]
Services to deploy:

descriptor:      [~/emahiro/path/gae_go111_app/src/app/app.yaml]
source:          [~/emahiro/pagh/gae_go111_app/src/app]
target project:  [dena-opfsys-gcp]
target service:  [gae-go111-app]
target version:  [VESRION_NAME]
target url:      [https://gae-go111-app-[YOUR_PROJETCT_ID].appspot.com]

Do you want to continue (Y/n)?  Y

Beginning deployment of service [gae-go111-app]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 1 file to Google Cloud Storage                 ═╣
╚════════════════════════════════════════════════════════════╝

# 略

deployはmoduleで変更があった場合などアップロードする対象が増える、差分アップロードっぽいです。

まとめ

とりあえず起動してdeployするところまでざっくり試してみました。
loggingやdatastoreあたりのライブラリの動作を検証してみようと思います。特に cloud.golang.org/appengine のpackageが使えなくなるのでその代替となる golang.google.com/go あたりを使うところは確認してみたいです。
また、実際にどうライブラリをinstallしてpackageを取り込んでくれるのか、go.modあたりも使ってみようと思います。

AppEngineのGo1.11.0のランタイムについて浅く調べてみた

GAE/GoでGo1.11のランタイムがBetaになっていたので調べた内容をまとめてみます。
なお、基本的にはドキュメントに書いてあることを舐めてるだけの内容になります。

参考

先に参考を明示しておきます。ここだけ見れば、GAE/Goを現在使ってる人ならその差分がざっくりわかるはず。

変更点と影響

Go1.9 -> Go1.11について

※ 上記の参考のURLを元に書き出したのみです。なので正しい情報は本家のマイグレーションのドキュメントまで。

You specify the Go 1.11 runtime in your app.yaml file by using the line runtime: go111. For more information, see Changes to the app.yaml file.

app.yamlのファイルに runtime:go111 と書く。
これはそのうち go:1 とだけ書いてあったら自動的にgo1.11でデプロイされるようになったりしそうな気もします。 ただ、後述する1st -> 2nd への世代交代に置いて下位互換はほぼ切られてると言っても過言でない状態なのでこれはこれでされたら困るから多分正式リリースに当たって何かしらアナウンスがあると思われます。

またいくつかの app.yaml の設定がduprecatedになっている。コアに使うところなものは少ないが影響がありそうなのが skip_files が非推奨になり、代わりに .gcloudignore を使わないといけなくなります。
.gcloudignore ファイルについてはこの辺を参照するといいと思います。 -> gcloud topic gcloudignore  |  Cloud SDK  |  Google Cloud

記載方法は .gitignore と変わらない書式だからそんなに難しくないと思います。
なお、.gcloudignore ファイルがないとそもそもデプロイ時にコケます。

Each of your services must include a main package. For more information, see Creating a main package and Structuring your files.

initではなく、 第二世代AppEngineでは 各パッケージの初期化にはmainが必要
[追記] main.go以外のpackageの初期化は init で可能でした。

main.goにはmain関数が必須になる。
またマイグレーション方法にも記載されてるディレクトリ構成にする必要がある(らしい)。

app/
  hoge.go
  app.yaml

では動かずに

app/
  web/
    main.go
    app.yaml
  hoge.go

のように従来の第1世代とは異なりmain packageと同階層にyamlがあることが求められます。
これ自体はディレクトリ構造を変えればいいものなんですが、依存関係も含めてdeploy時にアップロードされるのかというところは既存のプロジェクトから移行するときに注視しないといけないかもしれません。 (deploy時にmain pachageとyamlファイルが別階層に置いてたらvendorとかが正常にアップロードされなかったりという話が聞こえてきたり...)

また気になったのは、マイグレーションドキュメントに

How you import dependencies into your project has changed. For the Go 1.11 runtime, specify dependencies either by: - Putting your application and related code in your GOPATH. - Or, creating a go.mod file to define your module.

とあって、そもそも依存関係を取り込む方法が異なってる可能性があります。
go1.11に移行した場合にlocalパッケージと同様に外部パッケージもGOPATH配下に直置きしないといけないような気がします。

実際にドキュメントの中でも

Move all relevant files and imports to your GOPATH. If using relative imports, such as import ./guestbook, update your imports to use the full path: import github.com/example/myapp/guestbook.

という記載あるように GOPATH 配下に関連するファイルやimport対象を配置することとimportするときも相対pathでなくGOPATHから見たときの絶対path指定になっているのでちゃんと配置しないとそもそもimportが参照してくれないのでは?と想定してます。

そのためおそらくですが、

GOPATH
 src
   hoge.go
   vendor
    github
      fuga.go

みたいなディレクトリ構成だとうまくいかず、

GOPATH
 src
   hoge.go
   github
    fuga.go

こんな感じだとうまくいくんだろうか...?と思ってます。
この点はまだ試してませんが、これだとGOPATH配下のプロジェクトのsrc配下に大量に依存パッケージのディレクトリできることになるけど、そういうもんなんだろうか?
deploy時にコツコツ go get するか、さもなくば glide とかでinstallしてきたものをアプリケーションの起動前に全部 GOPATH/src 配下に移すとかそういう力技に頼る他なさそう?超めんどそう....こういう時のための 1.11 以降入った go.mod が効いてくるのだろうか?
ただ、go.modについては僕も使い方よくわかってないので割愛。

The appengine build tag is deprecated and will not be used when building your app for deployment. Ensure your code still functions correctly without it being set

ビルドタグなくなります。後述しますが、urlfetch使ってるユースケースもなくなるからまぁgo1.11にしても困らないはず...。

App Engine no longer modifies the Go toolchain to include the appengine package. We strongly recommend using the Google Cloud client library or third party libraries instead of the App Engine-specific APIs. For more information, see Migrating from the App Engine Go SDK.

後述。google.golang.org/appengine が非推奨になります。 cloud.google.com/go 配下のライブラリを使え、とのこと。

Services using the Go 1.11 runtime must be deployed with the gcloud app deploy command. The appcfg.py commands can still be used with the Go 1.9 runtime, but the gcloud command-line tool is preferred.

デプロイは gcloud app deploy のみ。 appcfg は使えなくなる。

Go1.11に対応したことで何が変わるのか?

gVisor対応

以前から今年中に来ると言われていたGoogle AppEngineの2nd GenerationがGoのランタイムでも使えるようになります。
gVisorはなんぞや、という人は以下の資料とかを目を通してみるといいかもしれません。
refs: gVisor と GCP

1st GenerationのApp Engine、GoのランタイムにはGAEならではの様々な制約が存在し、それゆえに融通が効かないことがいくつか存在しました。
しかし、2nd Generationになることでいくつかの制約がなくなります。一方で今まで使えていたものが使えなくなります(※1)

※1 いくつかは仮説なので実際に検証しきれていないところがありますので「ふーん」程度に思っていただければと思います。

AppEngine Context が作られなくなる。

GAE/GoでHTTPリクエストを実装したりするときには、AppEngine Context を使うことが必須でした。

ex. 従来のGoのランタイムで一番簡易的なHTTPの実装

func handler(w http.ResponseWriter, r *http.Request) {
        ctx := appengine.NewContext(r)
        client := urlfetch.Client(ctx)
        resp, err := client.Get("https://www.sample.com")
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        fmt.Fprintf(w, "HTTP GET returned status %v", resp.Status)
}

refs. Issuing HTTP(S) Requests  |  App Engine standard environment for Go  |  Google Cloud

httpのリクエストからhandlerで appengine.NewContext によりApp EngineのContextを作って、それを使ってHTTPのリクエストを組み立てていくことが一般的なGAE/Go におけるHTTPの実装のパターンではありましたが、Go111になると appengine.NewContext でAppEngineのコンテキストが作られなくなります。

App Engine packageが非推奨になる

[20181105更新]
cloud.google.com/go が推奨されてるだけで、appengineパッケージ自体が使えなくなる訳ではないです。そのため「〇〇は使えない」と記載してる箇所は使い続けられます。

google.golang.org/appengine から、Goの標準ライブラリ、cloud.google.com/go を使うことが推奨されます。そのため推奨ライブラリを使った場合、幾つのかの今まで使えていたライブラリが使えなくなります。
(AppEngineのcontextが使えなくなることに起因していることがほとんどですが。。。)

urlfetchが使えなくなる

appengine.NewContext でAppEngineのContextが作られなくなるのとほぼ同義ですが、urlfetchも合わせて使えなくなります。
サービス開発に置いてHTTPリクエストをしないサービスというのは皆無だと思うので、1st GenerationのAppEngineのGoのRuntimeで動いていたサービスはほぼこのurlfetchを使ってAppEngine外部の外界との通信を実装していると思われますが、それが使えなくなります。
代わりにGAE/Goでもgoの標準のHTTPパッケージである net/http パッケージを使えるようになります。

urlfetchが使えなくなるデメリットはもちろんなるんですが、メリットとしては1st GenerationのGAE/Goではurlfetchをするためにだけにすべてのリクエストに対して毎回20ms程度の通信コストが発生していましたので、これがなくなることによりその通信コストも多分丸っとなくなります。

appengine/logが使えなくなる。

同じですが、appengineのcontextが作られなくなるので、func Debugf(ctx context.Context, format string, args ...interface{}) というシグネチャにおいて ctx = appengineのcontextを受け取ってるこのライブラリが使えなくなります。

これはstackdriver loggingでログを見たときにうまい具合にリクエストごとのログをネストしてくれていたので結構痛い...。
ちなみに、標準のログパッケージを使うといい感じにネストしてくれません。なのでログが非常に見づらくなります。

cloud.google.com/go/logging を使えば解消したりしないのかなぁ。。。

appengin/datastore、memcacheが使えなくなる

これもappengineのcontextが作られなくことに起因してますが、

の二つが使えなくなります。この二つが使えないことにより、いい感じにdatastoreの結果をmemcacheに乗っけてくれる便利なライブラリ goon が使えなくなります。

その他

appengine/aetest とかも使えなくなりますね。

とりあえず普段仕事でメインで使ってる google.golang.org/appengine 配下のライブラリで使えなくなるものをまとめて見ました。代替手段は全く思いついてません。
仕事でも同僚とgo1.11がβになってから話してましたが、 goon 依存が大きいのでこれほんとどうしよう...と思ってます。
通常のmanagedなmemcacheを使うか、それともGAE SEからCloud MemoryStore(フルマネージドredis)が叩けるようになるのを待つか、、、など課題が山積みだったりします。

まとめ

App Engineの世代交代に合わせて結構大幅に下位互換が切られてました。

go1.11にするメリットはもちろんあれど、すでに第1世代で動いているシステムのマイグレーションについてはいい打ち手が現段階では思いついてません。

またプラットフォーム依存の課題もあると思っています。
というのも、去年の10月ごろに正式リリースになったAppEngineのgo1.8のランタイムですら。すでに今月末でサポートは打ち切られて1.9以上にしないといけなくなっていますし、いつ1.11に強制移行させられるかわからないのではないか?っと割と戦々恐々としてます。 もちろん 1.8 -> 1.9への以降については下位互換が担保されたままだった上に、プロダクション環境でほとんど影響があるような変更はなかったので特にサポートする必要もないだろうってことで打ち切られたんだと思います。
しかし今回の1.11には大きめな変更が加えられているので、そうそう第1世代のサポートが打ち切りになるということはないような気もしてますが、プラットフォームの上で仕事してる以上、プラットフォームの決定が絶対なので本当にそうなったらどうしよう..っと思ってる次第です。

本エントリーでは公式のドキュメントを読み漁って現行環境との違いを抽出、および影響がありそうなところをざっと書き出してみた程度なので、特にデプロイ周りで依存関係を取り込んだり、App Engine Specific Libraryから標準package、cloud.google.com/go への置き換えでうまくいくかどうか、あたりはちゃんと調べてみようと思います。

[追記]

浅くしか調べてなかったので appengine のpackageを最新にした状態で appengine.Main() を使えば 1.9以前と同様に1.11の環境も使えることを知りました。google.golang.org/appengine のパッケージを使えないというわけではなく、あくまでこれから使うなら他のやつ使ってね、くらいの温度感だと理解して、既存の環境どうしようか戦々恐々する必要ないということはこのエントリー書いたあとにも色々調べてわかりました。

[追記] updated_at 20181126

GCPUGのnouhauにマイグレーションに関する資料が上がっていたので追記します。すごくわかりやすい。

https://github.com/gcpug/nouhau/tree/feature/id/76/app-engine/note/gaego19-migration-gaego111github.com

net/http だけでRESTなサーバーを作りたい

やりたいこと

当初の想定は以下のような感じでRESTFullなwebサーバーを標準の net/http パッケージだけで書こうと思います。

package main

import (
    "fmt"
    "handler"
    "net/http"
)

var port = 8000

type methodHandler map[string]http.Handler
func (m methodHandler) ServeHTTP(w http.ResponseWriter,r *http.Request){
    if h, ok := m[r.Method]; ok {
        h.ServeHTTP(w, r)
        return
    }
    http.Error(w, "method not allowed.", http.StatusMethodNotAllowed)
}

func Router() *http.ServeMux {
    mux := http.NewServeMux()
    // ここにHandlerを足していく...
    mux.Handle("/", methodHandler{"GET": http.HandlerFunc(handler.Index)})

    // user
    mux.Handle("/users", methodHandler{"GET": http.HandlerFunc(handler.GetUsers)})
    mux.Handle("/users/:id", methodHandler{"GET": http.HandlerFunc(handler.GetUser)})
    return mux
}

func main() {
    fmt.Println("Server Start....")
    mux := Router()
  if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
        panic(err)
    }
}

しかし userの単一リソースを取り出したいケースに置いて net/http だけでは不十分です。というのも net/http パッケージはルーティングの機能がとても貧弱で、 /users/:id 指定したhandlerに適切にroutingしてくれません。 そのため、なんとかして /users/:id でPKが id のリソースを取得する GetUser メソッドにルーティングさせる実装を独自にする必要になります。

net/http パッケージで :id を取り出して適切にhandlerにルーティングする方法については How to not use an http-router in go が参考になります。

net/httpだけで書く

ポイントは以下の2点です。

  • ShiftPath
  • リソースごとのHandlerを定義する。

ShiftPath

ポイントはこれです。実装の詳細は参考のURLにありますが以下のような ShifPath メソッドを定義します。

// src/handler/path.go

func ShiftPath(p string) (head, tail string) {
    p = path.Clean("/" + p)
    i := strings.Index(p[1:], "/") + 1
    if i <= 0 {
        return p[1:], "/"
    }
    return p[1:i], p[i:]
}

これはどういうことをしているかというと、メソッドのシグネチャの通り、pathからheadとtailを取り出し、tailを新しいリクエストpathとしては後続の処理に引き渡す処理です。

具体的な動作としては /users/1 のようなpathのリクエストに対して

  • haed => users
  • tail => /1 > これが後続の処理ではリクエストpathとして扱われる。

という風に分ける処理を行います。

そもそもの課題として標準の net/http パッケージでは /users/:id のようなpathのパターンを解釈できずに適切にルーティングしてくれないのがあったのでこのメソッドでリクエストのpathを解釈して適切なhandlerに登録したメソッドに振り分けます。

例えば UserHandler の場合は以下のような実装になるかと思います。

// src/handler/users.go

type UserHandler struct {}
func (h *UserHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path) 
  
  // この時に /users/:id のようなpathは head => users、 後続の処理で使うリクエストpath = /1 となっている。
  
  if head == "users" && r.Method == "GET"{
        GetUsers(w)
        return
    } 
  
  // もう一度ShftPathを行う。ここでは head => 1というint64の数字のはずである。
  head, r.URL.Path = ShiftPath(r.URL.Path)
    id, err := strconv.ParseInt(head, 10, 64)
    if err != nil {
        http.Error(w, fmt.Sprintf("invalid params. head: %s", head), http.StatusBadRequest)
        return
    }

    switch r.Method {
    case "GET":
        GetUser(w, id)
        return
    }

}

このような感じで再帰的に ShiftPath を繰り返していくことで適切なhandlerのメソッドにルーティングします。これ相当めんどい...。。。

リソースごとのHandlerを定義する

Handlerとして扱いたい独自構造体を定義してServeHTTPメソッドを持たせることでHandlerとしての振る舞いを持たせます。

http.Handler(pattern, handler) のpatternの部分でidをうまい具合に取り出して Handler に引き渡せればいいのだけど、普通に書いてるだけdはうまくいかないので専用の Handler を作りそのHandlerを ListenAndServeに登録するということをします。 ※ 愚直にpatternを書くようなことはしません。

// main.go

type AppHandler struct {
    RootHandler *handler.RootHandler // `/` のpath
  UserHandler *handler.UserHandler // `/users` のpath
}

func(h *AppHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
  // リクエストごとにどのリソースのHandlerにルーティングするするのかを決める。
}

AppHandler を登録したのは最終的に全てのHandlerをまとめて ListenAndServe に渡すためです。

これでサーバー部分のコードは以下のようなコードになります。

// src/main.go

func(h *AppHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
    var head string
    head, r.URL.Path = handler.ShiftPath(r.URL.Path)

    switch head {
    case "":
        h.RootHandler.ServeHTTP(w, r)
        return
    case "users":
        h.UserHandler.ServeHTTP(w, r)
        return
    default:
        http.Error(w, fmt.Sprintf("method not allowed request. req: %v", r.URL), http.StatusMethodNotAllowed)
        return
    }

    http.Error(w, "Not Found", http.StatusNotFound)
}

またuserリソースを取得するコードは以下のようにちょこっと変えます。main.goで UsersHandlerへの振り分けを行なっているため。

// /src/handler/users.go

func (h *UserHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    // main.goのAppHandlerで一度ShiftPathしているので headが空 = /users と同義
    if head == "" && r.Method == "GET"{
        GetUsers(w)
        return
    }

    id, err := strconv.ParseInt(head, 10, 64)
    if err != nil {
        http.Error(w, fmt.Sprintf("invalid params. head: %s", head), http.StatusBadRequest)
        return
    }

    switch r.Method {
    case "GET":
        GetUser(w, id)
        return
    }

}

まとめ

こんな感じで取得したいリソースのHandlerを逐一 AppHandler に登録していきますし、適切なメソッドに振り分けるまで ShiftPath を繰り返す必要があるのでとてもめんどくさいなと思います。

やはりある程度FWを使うのがいいのかなと思いますし、個人的には net/http 互換の GitHub - go-chi/chi: lightweight, idiomatic and composable router for building Go HTTP services がいいのではないかなと思っています。

余談

goのinterfaceを理解するのによく net/http のパッケージが例として使われることが多く、自分もこれで理解をしていましたが、今回車輪の再発明的な実装をしてみたことでメソッドを登録することでinterfaceとしての振る舞いをもつ、ということを再認識できました。特に ServeHTTP(w http.ResponseWriter, r *http.Request) をカスタムstructに持たせることでHandlerとして振舞うことができるというテンプレ的な表現は ServeMux の実装を見ることで理解できました。

mux := http.NewServeMux()
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
  panic(err)
}

というコードに置いて ServeMuxListenAndServe に登録できるのは net/http パッケージ内で ServeHTTP が定義されていてHandlerとして振舞うことができるから、なんですね。本家のコードの中でもこのパターンは多用されておりなるほどこういうことか、と再認識できました。

// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        if r.ProtoAtLeast(1, 1) {
            w.Header().Set("Connection", "close")
        }
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

ref. go/server.go at master · golang/go · GitHub

今回書いたコードはこちら

github.com

Goので外部パッケージをimportする

以下のようなパッケージ構成のGoのプロジェクトを考えます。

$GOPATH
 ├── handler
    │   └── index.go
    ├── main.go
    └── middleware
        └── log.go

これだと main.go で localに作成した handlermiddleware を import しようとするときに、 Unresolved reference エラーが発生して外部のpackageをimportできません。

正しくはこちらです。

$GOPATH
└── src
    ├── handler
    │   └── index.go
    ├── main.go
    └── middleware
        └── log.go

外部パッケージをimportするときは $GOPATH/src 配下をROOTとしてプロジェクトを切ります。

iTerm2で goapp test の実行結果を見やすくする

goapp test単体テストを走らせた時に RUN/PASS/FAIL が見づらいので iTerm2goapp test を見やすくする設定を追加します。

設定方法

Preference > Profile > Advanced > Triggers で特定の文字列だった時にハイライトさせるTriggerを設定します。

例えば RUN でハイライトさせたい場合は、Triggerを以下のように設定します。設定には正規表現を使います。

Setting => === RUN.+

僕は以下のように iTerm の設定を行なっています。

f:id:ema_hiro:20181011010232p:plain

結果としてはこんな感じでハイライトされます。

f:id:ema_hiro:20181011010255p:plain

色々見やすくするためのツールは各種あると思いますが、基本シンプルに保ちたく、余計なもの入れたくないタイプなので設定だけでどうとでもなるし、結果として欲しかったものは手に入るのでこういうのもありかなと思います。

引数で指定した型で出力する型を変更するTips part2

昨日以下のエントリを書きましたが json.Unmarshal versionで書いていたので stream でレスポンスを扱う方も追加で記載します。

ema-hiro.hatenablog.com

streamを使うパターン

func HogeSetter(url string, out interface{}) error {
    resp, err := http.Get(url)
    if err != nil {
       return err
    }

    if err := json.NewDecoder(resp).Decode(out); err != nil {
        return err
    }

    return nil 
}

なぜstreamを使うかというのは以下の記事が参考になると思います。

christina04.hatenablog.com

bytes.NewReader(b []byte)

エントリの趣旨とは関係ないですが、Go Playgroundは net/http が使えないのでこういった streamを扱うときに io.Reader を作る必要があるのですが、いつも忘れるので備忘録で記載します。

bytes.NewReader(b []byte) -> https://golang.org/pkg/bytes/#NewReader を使います。

使って書いてみたケース -> https://play.golang.org/p/g2Bl_Zoxq7N

引数で指定した型で出力する型を変更するTips

課題感

goは静的型付き言語でfuncの返り値に指定した型は厳格に守る必要があります。
一方で返り値のみ異なるケースで内部の実装の詳細が型以外ほぼ同じような関数を定義したいときは結構あります。
goだと特に同じようなコードを書くことになりがちでこれを共通化したいというユースケースを考えます。

ユースケース

HTTPリクエス

指定したpathのレスポンスをstructに詰め直して返り値を受け取りたいケースを考えるとベタに書いたらこんな風になるんじゃないかと思います。

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

func getUser(url string) (*User, error) {
    u := &User{} 
    resp, err := http.Get(url)
    if err != nil {
        return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, u); err != nil {
        return nil, err
    }
    return u, nil
} 

type Game struct {
    ID    int64  `json:"id"`
    Title string `json:"title"`
}

func getGame(url string) (*Game, error) {
    g := &Game{} 
    resp, err := http.Get(url)
    if err != nil {
        return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, g); err != nil {
        return nil, err
    }
    return g, nil
} 

goらしく同じような書き方になります。最初の説明でも記載した通りここで違うのは関数内で返り値を返すためのstructの初期化のみです。

シグネチャ

以下のようなfuncを考えます。

func HogeSetter(url string, out interface{}) error {
    resp, err := http.Get(url)
    if err != nil {
       return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, out); err != nil {
        return err
    }

    return nil 
}

説明

上記のシグネチャのようなメソッドを使う上での挙動の説明をします。

  • url: リソースのレスポンスを受け取るurlです。ここには https://www.example.com/usershttps://www.example.com/games といった文字列が入ります。
  • out: ここが今回のポイントです。interface型で受け取ることで引数で指定した型の参照型を注入することでメソッドないでjson.Unmarshalを行い、参照型にしたstructにjsonをmappingすることができます。

このシグネチャでは上記のユースケースに記載したような 共通処理 を抽象化できます。urlや詰め込みたい型は外部入力の形で受け取ってメソッド内部で柔軟に型を入れ替えても詰め替えることができるようにします。

実際に使うときは以下のように使うことになると思います。

ユースケース

func get(url string, out interface{}) error {
    resp, err := http.Get(url)
    if err != nil {
       return err
    }

    b, _ := ioutil.ReadAll(resp)
    if err := json.Unmarshal(b, out); err != nil {
        return err
    }

    return nil 
}

func main(){
    u := &User{}
    if err := get("https://www.example.com/users", u); err != nil {
        panic(err)
    }
     
    g := &Game{}
    if err := get("https://www.example.com/games", g); err != nil {
        panic(err)
    }
}

このように共通化可能です。

sample

実際にこうした Setterっぽいけど実装の意図としてはGetterのようなコードがちゃんと動くことを確認します。

package main

import (
    "encoding/json"
    "fmt"
)

type X struct {
    ID int64 `json:"id"`
}

type Y struct {
    Name string `json:"name"`
}

func main() {
    j := []byte(`{"id": 1}`)
    x := &X{}
    if err := typeSetter(j, x); err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", x)

    jj := []byte(`{"name": "taro"}`)
    y := &Y{}
    if err := typeSetter(jj, y); err != nil {
        panic(err)
    }
    fmt.Printf("%#v", y)
}

func typeSetter(input []byte, out interface{}) error {
    if err := json.Unmarshal(input, out); err != nil {
        return err
    }
    return nil
}

// output
// &main.X{ID:1}
// &main.Y{Name:"taro"}

ref: https://play.golang.org/p/PFb5Do_WJVA ここでRunを押下することで確認できます。

まとめ

goは厳密に型チェックするがゆえに冗長なコードを生み出しがちですが、このエントリで紹介した共通化の書き方はとても便利だなと思いました。

AppEngineについて考えてること

AppEngine大好きっ子で1年近くAppEngineをPaaSに採用した環境で開発を行ってきた人間としてちょっと考えたことをつらつら書いてみます。
※1 このエントリは今日(2018/09/21)同僚の人と色々話してた内容をベースにしてます。
※2 あくまで個人の感想、というかAppEngine信者の戯言です。

サマリ

特に長いつもりもないですけど言いたいことは以下のようなことです。

  • 最近のGCP界隈はKubernetes推し
  • 爆速サービス開発はFirebaseで十分になってきた
  • AppEngineがどんどんGKEとFirebaseの勢いに押されて目立たない
  • AppEngine最高だからもっと使われてほしい

最近のGCPの流れ

GKE推し

昨日一昨日開かれてた Google Cloud Next Tokyo にも参加してきましたが、とにかくKubernetesが強いです。
7月にサンフランシスコで行われたGoogle Cloud Next からKubernetes推しの流れはいまだに止まらないなぁと。

国内でもGCP/Goで勢いのあるメルカリ社もGKEを採用してるし、その派生サービスでブラックホールのごとく人を集めてるメルペイもGKEを採用しているとのこと。(公式ブログ をウォッチしてる限りの情報)

あとはCAのやってるAbemaTVでもインフラにGKEを採用しているし、とにかくGKEの流れがすごいと思います。
Google Cloud Next Tokyo セッションもGKEのセッション数は20数個ある一方で、真っ当なAppEngineのセッションはその半分くらい。ここでまず勢いの差を感じます。

Firebaseが高機能になってきた

ユーザー認証、FireStore、CloudFunctions、FirebaseHostingなど機能が充実してきててもうこれさえあれば簡単にサービス開発できるようになってきているなと思います。
いつだったか、「これからの時代はFirebase使えるPaaSエンジニアの給料が〇〇百万円〜」みたいなツイートがバズってましたが、まぁそれもわかるなと思うくらい多機能で基本的な機能を備えたサービスであれば爆速で開発が可能になっています。

わざわざGKEやAppEngine選ばなくてもFirebaseさえあればサービスを作るには事足りそうな時代はもう来てるなという印象。

AppEngineについて

僕個人としは、やはりAppEngineはいいソリューションだと思っています。というのも

  • フルマネージド
  • (ランタイムにもよりますが)数十ms〜数百msでの高速なインスタンスの起動
  • Auto Scaling
  • 標準でデプロイパイプラインを装備(gcloud app deply
  • 高機能なコンソール画面

というのは一般的にメリットとして挙げられますが、個人の経験談としては

  • AppEngineのチームにjoinして一年以上productionに向けてsshコマンドを叩いていない
  • とにかく細かいところを気にしなくていいので、アプリの開発に集中できる

というところがとても開発者体験として大きかったです。
ssh を叩かないだけで本当に心理的にはいい影響しかありません。

デメリットとして挙げられることもある outboundできない問題(urlfetchの制約) ももうすぐ出ると言われてる gVisor 対応した AppEngine 2nd Generation によりなくなる(かもしれない) と言われてるので、AppEngineならではの制約からは少しずつですが解放されていくのではないかと思います。
(※ javaやnodeのランタイムではすでに gVisor対応してます。)

そういえば今日こんなコードを一緒に話してた同僚の方が見つけていて 「おお 👀 !!」 と思いました。

github.com

特に https://github.com/GoogleCloudPlatform/golang-samples/blob/master/appengine/go11x/helloworld/helloworld.go#L11-L16 の部分。

import (
    "fmt"
    "log"
    "net/http" //これ!!!
    "os"
)

AppEngineでは urlfetch が必要になっていたのに app.yamlにある runtime: go111 のversionからは net/http という標準パッケージを使っているので、サンプル実装ではありますが、これが本当になればAppEngineならではの制約で書いてたコードもその制約から外れますし、標準的なgoの実装に合わせることができるので、デメリットはなくなると思います。

伝統的なGoogle謹製のコンテナで歴史あるプロダクトですが、ここに来て少しずつ色んな制約が取り払われてる(gVisorとか)ので、あとはAppEngineを使った事例がもっとでてくればいいなと思います。
AppEngineは分類的にはサーバレス、ではないのかもしれませんが、サーバーを意識せずに開発できるという観点ではサーバレスだと思いますし、とても使いやすいサービスだと思っています。

メリットにも書いてますが、高速なspinupとデプロイパイプラインまで標準で装備されているので、書いたコードをすぐにデプロイして *.appspot.com で確認することが可能です。
Standard Environment でサポートされてるランタイムを使うのであれば、デプロイも高速に完了します。サービスを開発したいのに、CI/CD周り整備したりとか開発以外の運用や開発プロセス整備みたいな今まで手間だったものまるっと意識しなくてよくなります。

サポートされてるランタイムの上に乗っかる前提ですが、ここ数年来勢いのあった、Railsを使った高速開発〜!といった流れを変えうる(もしかしたらすでに変えている?!)プロダクトだと思います。
(Railsはいいんですけど、やはり大規模化した時本当にずっと使い続けるの?っていう疑問は思ってますし、ruby嫌いじゃないけど、型のある安心感からgoやjavaがやはり大規模開発には向いてる気がするんですよねぇ...コンパイラで怒ってくれる安心感?みたいなもの。)

そういった言語をサポートしていて、かつメリットに記載したような内容を享受できて、GKEほどエコシステムの学習コストは高くなく、実運用ベースでFirebaseに比べればまだ実績があって(多分?)、かつ高速にサービス開発できる可能性のあるAppEngine、もっと広がってもいいんじゃないかなーと思ってます。

まとめ

とりあえず僕はAppEngineを初めて使った時から感動しっぱなしで、これは本当に最高のツールだと思っています。GKEやFirebaseの流れに負けずに存在感を出してって欲しいと思います。

Intellijで古いversionのpluginをinstallする

Intellij をはじめとした jetbrains 系のIDEで現在installしているpluginよりも古いversionのpluginを入れようと思ったのでそのメモです。

背景

そもそもなぜわざわざ古いversionのpluginを入れようかと思ったかというと、jetbrains系のIDEでUIをカスタマイズするときに Material UI というのを使うのが一般的なんですが、先日IDEをアップデートして合わせてpluginもアップデートした直後から、Intellijを起動するたびにMaterial UIの設定wizardが毎回出るようになってしまい、ものすごくうざかったので、その原因と対処を調べてたというのが理由です。

原因

ありきたりなんですが、IDEのアップデートに合わせて Material UI の pluginもアップデートしたら、pluginのpreview版がinstallされてしまったことが原因でした。
どうやら jetbrains系のIDEに使われる pluginはpreview版であってもstatable状態であればIDEEAP(Early Access Preview)版でなくてもpluginのpreview版がインストールされてしまう、ということがわかりました。

Mateial UIのページを見ると、僕がこの問題に直面した 2018年9月18日時点では以下のようになっていました。

plugins.jetbrains.com

これの Download Plugin を見ると最新版が Material Theme UI 2.9.0-pre1 でstatableになっていました。どうやらこれが原因らしいです。

Changelog 2.9-pre PS: This is a very early release which has not be tested yet, but since I'm taking a break I wanted to release it before I go. If something goes wrong you can still revert back to 2.8.3.

ちなみに注意書きにこれが書いてあって、「じゃあそもそもなんでstatableやねんw」っていう気もしましたが、仕方ないので、このエントリを書いてる時点の1つ前のversionに戻します。

古いversionのPluginをinstallする

jetbrains系のIDEでPluginをDLする場合は3つの方法があります。

f:id:ema_hiro:20180920025923p:plain

  • Install jetbrains plugin ... デフォルトで指定されてる公式が指定しているplugin。各種言語のpluginはこれに当たる
  • browse repositories ... 公式指定に限らず、一般の人も公開している全てのpluginをinstallできる。
  • install plugin from disk ... 端末からバイナリを指定してinstallする。jetbrainsIDEに内包されてるpluginのinstallerを使わない。

今回は3つ目の install plugin from disk を選択してpluginをinstallします。この手順は以下に掲載されています。

www.jetbrains.com

手順

  1. https://plugins.jetbrains.com/plugin/8006-material-theme-ui からこのエントリ執筆時点の最新安定verは2.8.3をDLする
  2. Preferences > Plugin > install plugin from disk を選択する
  3. 1でDLしたzipファイルをそのまま選択してください。取り込めるのは zip or jar だけみたいです。
  4. 正常に取り込めたらIDEを再起動します。

結果

2.8.3に戻したら毎回 Material UI の 設定ウィザードが開かなくなったので元どおりに戻りました。jetbrains系のIDEは結構罠が多いなと思います。

『ファイナンス思考』を読んだ

ファイナンス思考』を読み終えました。
会計というか事業に置けるファイナンスの考え方に興味があるのでとても面白い内容が詰まっていました。

ファイナンスを実際にすることはなくても、どういう考え方を持っておけばいいのか知りたい人にはいい書籍になるのではないかと思いました。

よかったところ

  • 説明されるとまぁ確かにと思うようなPLの考え方やそれに基づく実際やってしまいがちな思考が整理されていたこと。
  • ユースケースが多く実際に行われたことベースで理解することができること
    • 特に実際に伸びてきた外資IT企業、国内でも危機的な状況から復活した企業など自分が知らない企業の裏側も解説していたので、実際のケースに照らし合わせてみて理解しやすいこと。
  • PLになりやすい原因まで記載があったこと
    • 僕も振り返ると結構PL脳になってることが多かったと思います。
    • そして日本企業がどうしてPL脳になりがちなのか、という点も歴史的な経緯も含めて「なるほど」と思わされてしまいました。

感想

mixiの朝倉さんが書いた本をちゃんと読んだのは初めてでしたが、事前知識なくても読めるくらいに丁寧に書かれていました。とても読みやすい書籍だったので

も買って読んでみようかなと思いました。

最近仕事で意識していること

最近仕事をしている中で意識していることをまとめてみました。

Overview

  • 前提をすり合わせること
  • 細かいところにこだわってみること
  • 意図を説明すること

前提をすり合わせること

PullRequestに対してレビューしてもらう場合にdescriptionをちゃんと書くようにしたり、レビューを依頼するときに、「どこにどんな変更を加えたのか」をチャットでも一言添えるようにしています。

と言うのも、設計やレビューを依頼するときに、レビュアーにとって、コンテキストがわからない状態でレビューするのはとても負荷の高い作業になるので、極力みてもらう場合には前段のコンテキストや前提を伝えるようにしています。

同じ土俵に立ってはじめて、身のあるレビューをしてもらえるようになっているし、チームで仕事をする以上、相手のことも考えながら仕事した方が全体としては生産性はプラスに働くと考えています。
(本当はそう言う小さい気遣いを評価してもらえるようになるともっといいんだろうなと思っています)

いいfeedbackをもらうには、同じ土俵に立ってこそだと思いますし、同じ土俵にあげる努力を怠らないようにしていきたいと思います。

余談ですが、同じ土俵に上げることを 巻き込み力 とか言うのかな?とちょっと思い始めました。

細かいところにこだわってみること

今まで意識してこなかったところ、もしくは脳死的、簡単にスルーしてきたことにちょっとこだわってみています。

commitメッセージにこだわること

  • commitメッセージの内容
    • なぜ?とか書いたコードの意図をなるべくコミットメッセージに込めてみること
  • PullRequest全体のcommitの構成
    • わかりやすい粒度でコミットを分けること
    • それに伴ってgitの知識をつけること
  • squash & merge で大きなコミットをみやすくする

コミットメッセージって自分がわかればいいかなと思っていた派ではあるんですが、少しでも粒度や差分を一言で表すように意識してみると、コードを書く前にどう言う粒度にしようとかとか考えるようになってきました。
その結果コード書く前に、これから書くコードについてある程度見通しを立てることができるようになってきたと思います。

reviewdogに従ってみること

プロジェクトでコードレビューするときに reveiewdog を使っています。これがすごく良くて、Go Wayに則ってなかったり潜在的なバグが潜んでそうなところを指摘してくれるので、コードレビューの負荷がすごく減っていると思います。

一方でレビュイーからすると結構細かいところまで指摘されるので、直すのがめんどくさかったり後回しになってしまうPullRequestもしばしば見かけます。

PullRequestをどう運用していくのはレビュイーに任されていると考えているので、reviewdogで指摘されたコメントはすぐに対応するか後でまとめて対応するかに意見はあまりこだわりはないんですが、自分はおおよそのケースで指摘された次のコミットですぐに対応するようにしています。

結果、常にreviewdog意識しながらコード書くようになったためか、犬に吠えられない(望ましい)コードをかけるようになってきました。

意図を説明すること

最近

  • 「なぜその設計にしたのか?」
  • 「なぜそのコードを書いたのか?」
  • 「なぜhogehoge??」

をちゃんと説明できるようにしています。
意図があるから、設計レビューやコードレビュー時にfeedbackをもらったときにその意図を説明しますし、議論が生まれますし、結果新しい観点をinputできるようになりつつあるので、前よりもずっと開発するのが楽しくなってきました。

意図をもつなんて仕事するときに当たり前のことかもしれませんがどうしても仕事をしていると、現状の実装に合わせたり、特に考えず脳死的に要件だけを満たそうとしたりするので、難しですがなるべく意図を持ったと言い切れるように、説明できること、と言うことを意識しています。

まとめ

これは普段業務で意識していることの一例に過ぎませんが、ただ仕様を満たすのでなく、自分が どう言う意図を持って、どうその仕様を満たすのかと言うプロセスを言語化できる ようになっているのが少しわかるようになってきました。

そして、少しですが 細かいところを意識すると開発が楽しくなる と言うことを実感しています。

神は細部に宿る と言いますが、こう言うところもこだわっていくと少しいいことあるのかなと思います。

「エンジェル投資家」を読んだ

エンジェル投資家が実際に投資する上での思考回路を赤裸々に記録している書籍です。

著者のジェイソン・カラカニスという人は僕は知りませんでしたが、この人が投資した or 投資し損ねた会社は僕らが知っているサンフランシスコの超優良スタートアップばかりでした。

僕自身は特にエンジェル投資家になりたいわけでも、そういう人から支援を受けたい(起業したい)とか考えたことがあるわけではありませんが、普段どういう人が出資を受けているのか、エンジェル投資家ってなんなのか、何が仕事なのか興味があったので、通読程度ですが一気にガッと読みきりました。

読んでみて思ったのは、エンジェル投資家は投資家であると同時に、投資先の代表の最大の壁打ち相手なんだなぁと思いました。
また、流行りそうならどんなサービスにでも投資するものかと思ってたんですが、思った以上に属人的というか、「人」を重視しているのか、ということはわかりました。

普段あんまり意識することがない職業のことや、細かいことをつらつら書いてましたが、書籍の内容としては面白かったです。

Test時に値を書き換えて元に戻すサンプル

メルカリさんのこのブログを呼んで表題のテスト時に置ける値の一時的な書き換えとresetの方法がとても便利だったのでメモりました。

tech.mercari.com

テストを回すときに実際のURLでなく適当なダミーURLを叩いてHTTPのレスポンスをモックしたい場合があると思います。 その際によくやるのが、実際のURLをテスト時だけ書き換える処理です。テスト時のみ書き換えるに当たって、求められるのは

  • テスト中だけ動くこと
  • テストを抜けたら(完了したら)元のURLにresetすること

です。
これをgoのdeferを使って簡単に実現します。

deferを使った書き換え方法

サンプルコードは以下です。

// main.go
package main

import "fmt"

var rewriter = "hoge"

func main() {
    fmt.Println(rewriter)
}


// main_test.go
package main

import (
    "testing"
)

func TestSample(t *testing.T) {
    t.Run("rewrite test", func(t *testing.T) {
        defer rewriteString("fuga")()
    })
}

func rewriteString(s string) func() {
    var tmp string
    tmp, rewriter := rewriter, s
    return func() {
        rewriter = tmp
    }
}

subtest関数において

defer rewriteString("fuga")()

この1行で値の差し替えとresetを行なっています。

  • returnがfunc型であること
  • deferが呼ばれるのは関数 or サブテスト関数を抜ける時でreturnのfuncが呼ばれるのも同じタイミングであること

この2つの挙動をうまく使っているなと思いました。

最初、このコードを見たときにどうしてそのように動くのか全く理解できなかったんですが、これは

  • defer で rerwrite("fuga")を呼んだ時点で rewritehoge -> fugaに書き換わっている。
  • サブテスト関数内から抜ける(deferが呼ばれる)まで rewrite = fuga の状態になる。
  • deferで呼ばれるのは rewriteString メソッドのreturnのfunc。
  • サブテストを抜けるタイミングで return func() の中身が呼ばれるので、ここで変数がresetされる。

という挙動で意図した挙動になっていると理解しました。
改めて考えるとちゃんと動いていますが、これ実際に使われてるテクニックだとするととても綺麗な書き方だと思いました。

テスト時だけ特定の値に変数を書き換えるときに専用のメソッドを用意してましたが、テスト全体で共通で使えるメソッドを export_test.go に書いておいて、同一packageないから呼べるようにしておけばテストコードの重複もなさそうです。

使う場面は多々ありそうなので、これは今後使っていきたいtipsです。

載せるようなものでもないですが、一応、ってことでコードはこちら

github.com

Table Driven Testのテストケースの書き方について

Goでは単体テストを書く場合に Table Driven Test が可読性、保守性の観点から推奨されています。

そのTableDrivenTestにおいてテストケースの書き方について備忘録です。

テストケースの書き方

ケースごとのstructを定義する

以下のようなコードを想定します。

type sampleUseCase struct{
  field1 int
  field2 string
}

func TestHoge(t *testing.T){
  cases := []sampleUseCases {
    {
      field1:1,
      filed2:"hoge",
    },
    // 各テストケース
  }
  
  for _ , tt := range cases {
    // テストをゴリゴリ回す
  }
}

Pros/Cons

  • テストのユースケースがわかりやすい
  • 一方でテストケースをいちいち外部に出さなくてもよく、Goぽくない。
  • たまに公開structにしているケースもあるが、テストケースを公開structとして外部package向けにも公開するべきではない。
    • 影響範囲を最小にするため

Testメソッドの中に非公開structを定義する

こちらが一般的な方法。コードは以下。

func TestHoge(t *testing.T){
  
  cases := []strict{
    field1 int
    field2 string
  }{
    {
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for _, tt := ranga cases {
    // テストをゴリゴリ回す
  }
}

Pros/Cons

  • GoらしいTableDrivenTest
  • テストメソッド内で非公開structにしているので、外部への影響がない。

structのslice or map[string]struct

これは好みだと思いますが、ユニットテストを書くケースにおいてstructのsliceにするか、mapでテストケースを書くか2パターンがあると思います。
Goのテストではテストメソッドごとsubtestにしてテストするのがいいとされています。

理由は

  • subtestの第一引数に「なんのテストを実行するのか」名前をつけられること
  • deferが書きやすいこと
  • リクエストなどを含まない単純なパターンのテストの場合、並列実行により高速が直列実行するときよりもテストのパフォーマンスを向上されらるため。

この1つ目のテストケースに名前をつけられる、というところですが具体的なコードだと以下のようになります。

func TestHoge(t *testing.T){
  
  cases := []strict{
    name   string
    field1 int
    field2 string
  }{
    {
      name:   "test case 1"
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for _, tt := ranga cases {
    tt := tt
    // サブテスト
    t.Run(tt.name, func(t *testing.T){
      // テストを書く
    })
  }
}

これでも十分わかりやすいですが、テストの名前がわかるためだけに name filedを増やすのも微妙だと思ってました。 そこで考えたのが、nameをkeyにした map[string]struct を作成してテストケースにするプランです。

func TestHoge(t *testing.T){
  
  cases := map[string]strict{
    field1 int
    field2 string
  }{
    "field1 case": {
      field1: 1,
      field2: "hoge",
    },
    // 各テストケース追加
  }
  
  for k, tt := ranga cases {
    tt := tt
    // サブテスト
    t.Run(k, func(t *testing.T){
      // mapのkeyがそのままテストケースになる。
      // テストを書く
    })
  }
}

このようにテストケースのstructの定義の仕方はいくつかありますが、mapが使いやすいので今後はmapでテストケースを書いていこうと思います。