emahiro/b.log

Drastically Repeat Yourself !!!!

ビルドタグで appengine.Main() の向き先が変わる話

Overview

AppEngine の Go1.11 対応において gin の version を最新の 1.3 系にあげたときに以下の問題にはまったのでその調査メモです。

  • gin の version を現時点(201904)での最新の 1.3 にあげ、appengine.Main() を使うコードに変更すると、AppEngine / Go1.9 で動いていた c.Request.Header.Get("Host") が空文字で返ってきてしまい、既存コードに影響が出てしまったこと。

原因

  • gin1.3 以降の appengine.Main() では internal.Main() が呼ばれる。appengine.Main() の中身は appengineかどうかのビルドタグで制御されていて、Go1.9 までのビルドと Go1.11 からのビルドでは appengine.Main() の実行される先が異なる。

  • gin 1.3 系(Go1.11)

  • gin 1.1系(~Go1.9)

挙動の違いを調査するために Go1.9 までで動作していた appengine.Main() の定義元の appengine_internal.Main() の中身を探します。

appengine_internal パッケージについて。

appengine_internal パッケージは Go1.11 で対応した gin の appengine.Main() では呼ばれません。

探し方

goapp env GOROOT
/PATHTO/google-cloud-sdk/platform/google_appengine/goroot-1.9 

このように appengine の goroot が出力されるので、このディレクトリ配下の src を探します。
※ goapp の go の version は 1.9

まず見たのは以下 - /PATHTO/google-cloud-sdk/platform/google_appengine/goroot-1.9/src/appengine_internal/internal.go

ここの Main 関数を確認します。

func Main() {
    close(appPackagesInitialized)
    flag.Parse()
    serveHTTP()
}

serveHTTP() メソッドを確認します。

// serveHTTP serves App Engine HTTP requests.
func serveHTTP() {
    // The development server reads the HTTP address and port that the
    // server is listening to from stdout. We listen on 127.0.0.1:0 or
    // [::1]:0 to avoid firewall restrictions.
    conn, err := net.Listen("tcp", "127.0.0.1:0")
    if err != nil {
        log.Print("appengine: couldn't listen on IPv4 TCP socket: ", err)
        conn, err = net.Listen("tcp", "[::1]:0")
        if err != nil {
            log.Fatal("appengine: couldn't listen on IPv6 TCP socket: ", err)
        }
    }

    addr := conn.Addr().(*net.TCPAddr)

    fmt.Fprintf(os.Stdout, "%s\t%d\n", addr.IP, addr.Port)
    os.Stdout.Close()

    err = http.Serve(conn, http.HandlerFunc(handleFilteredHTTP))
    if err != nil {
        log.Fatal("appengine: ", err)
    }
}

この err = http.Serve(conn, http.HandlerFunc(handleFilteredHTTP)) に着目します。

func handleFilteredHTTP(w http.ResponseWriter, r *http.Request) {
    // Patch up RemoteAddr so it looks reasonable.
    if addr := r.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
        r.RemoteAddr = addr
    } else {
        // Should not normally reach here, but pick
        // a sensible default anyway.
        r.RemoteAddr = "127.0.0.1"
    }

    // Create a private copy of the Request that includes headers that are
    // private to the runtime and strip those headers from the request that the
    // user application sees.
    creq := *r
    r.Header = make(http.Header)
    for name, values := range creq.Header {
        if !strings.HasPrefix(name, "X-Appengine-Dev-") {
            r.Header[name] = values
        }
    }
    ctx := &httpContext{req: &creq, done: make(chan struct{})}
    r = registerContext(r, ctx)

    http.DefaultServeMux.ServeHTTP(w, r)
    close(ctx.done)

    unregisterContext(r)
}

以下の部分を抜粋します。

r.Header = make(http.Header)
for name, values := range creq.Header {
    if !strings.HasPrefix(name, "X-Appengine-Dev-") {
        r.Header[name] = values
    }
}
  • 1.9以下でビルドされ、call されていた appengine_intenal.Main() では Header に関して X-Appengine-Dev-XXXX と言う文字列を持つヘッダー以外を Request Header に入れ直していました。

念の為、go1.11 以降で使用される appengine.Main() の実装について調べてみます。

エントリの冒頭でも記載してますが、 https://github.com/golang/appengine/blob/master/internal/main_vm.go#L19 を確認します。

func Main() {
    MainPath = filepath.Dir(findMainPath())
    installHealthChecker(http.DefaultServeMux)

    port := "8080"
    if s := os.Getenv("PORT"); s != "" {
        port = s
    }

    host := ""
    if IsDevAppServer() {
        host = "127.0.0.1"
    }
    if err := http.ListenAndServe(host+":"+port, http.HandlerFunc(handleHTTP)); err != nil {
        log.Fatalf("http.ListenAndServe: %v", err)
    }
}

この実装の中の handleHTTP の実装の中を見ます。 (ref:https://github.com/golang/appengine/blob/master/internal/api.go#L87-L152 )

※ 長いので header に値を set してる箇所のみ抜粋。
(ref: https://github.com/golang/appengine/blob/master/internal/api.go#L140 )

w.Header().Set(logFlushHeader, strconv.Itoa(flushes))

go1.11 以降で call されてる appengine.Main() の実装の中身を確認すると go1.9 までとは実装が異なっていました。

1.9以下でビルドされ、call されていた appengine_intenal.Main() では Header に関して X-Appengine-Dev-XXXX と言う文字列を持つヘッダー以外を Request Header に入れ直していました。

go1.11 以降では、Header から 取り出して Header に入れ直す処理を通っていないので c.Request.Header.Get("XXXX") で取り出せないものが発生していました。

まとめ

1.11 対応では call される appengine.Main() の中身が違うので、既存のコードを見直してみると良いかもしれません。

ビルドタグについて

qiita.com

追記

godoc の Request の説明を見ると

For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.

ref: https://golang.org/pkg/net/http/#Request

と記載されていた。最新版の go のドキュメントによるとそもそも Request.Header の中に Host field は含まないことになったらしい。

intellij で go module の設定を行う

intellij で go module を設定して開発を行う設定を行います。

手順

Preference > Languages & Frameworks > Go > Go Modules(vgo) を選択。

f:id:ema_hiro:20190412020707p:plain

Enable Go Modules(vgo) integration を ON にする。

f:id:ema_hiro:20190412020719p:plain

Vgo Executable は 使っている go の version(SDK) を指定する。
Proxy は direct を設定する。

これで Go Modules の設定は完了です。

参考

Go modules (vgo) - Help | GoLand

app.yamlでmainを指定して静的ファイルをサーブする

Overview

Google App Engine 2ndGenにてapp.yamlの設定でmain property を指定して、mainで指定した path をプロジェクトの root として静的ファイル( static や template 配下のファイル)が正常に読み込めるかどうか調べました。

以前も静的ファイルのサーブに関連したエントリは書いたのですが、今回はそこからアップデートのあった内容も含めて記載してます。

ema-hiro.hatenablog.com

main propertyについて

公式ドキュメントには以下のように記載されています。

Optional. The path or fully qualified package name of the main package.

You must declare the path to the main package if your package main is not in the same directory as your app.yaml. The main element supports file paths relative to app.yaml or full package names.

cf. app.yaml Configuration File  |  App Engine standard environment for Go 1.11 docs  |  Google Cloud

  1. ファイルの相対パスはmain propertyで指定されたpathの相対パスになります。
  2. main.goがapp.yamlと同階層にないとき、main packageのパスを定義します。

app.yamlの設定

以下のようなディレクトリ構成に置いて

├── app
│   └── cmd
│       └── main.go
├── app.yaml
└── go.mod

app.yaml で main property に ./app/cmd を指定することで、このアプリケーションにおける main package の path を設定します。
これにより app.yaml と main.go が同階層になくても main で指定されたディレクトリの main package が app.yaml と同階層にあるものとして読み込むことができます。

sample app.yaml

runtime: go111
service: gae-go111-app
main: ./app/cmd 

GAE 2nd Generation 以降はデプロイした時に CloudBuild でアプリケーションのビルドが行われるので、CloudBuild の実行ログから main package を ./app/cmd に変更してるログを確認することができます。

Building /tmp/stagingXXXXXXXXX/srv, with main package at ./app/cmd, saving to /tmp/stagingXXXXXXXXX/usr/local/bin/start

正常に main が再設定されてる場合は上記のようなログが出力されます。

goのコードから静的ファイルを読み込む

実際に go のファイルの中で特定のファイルを開きたい、ようなケースがあった場合も main を指定することにより、main.go が app.yaml と違う階層にあった場合も、app.yaml がある階層をルートとして go のコードから呼ぶことができるようになります。
そのため、app.yaml を配置した階層と同じ階層に特定のファイルを配置して、そのファイルをダイレクトに指定する(相対パス等を考えなくていい)ことでファイルにアクセスすることができます。
具体的には ./src/app/cmd/main.go の内部で以下のような実装をしたとしても、app.yaml そのものが出力されます。

f, err := os.Open("app.yaml")
if err != nil {
    panic(err)
}

io.Writer(os.Stdout, f)

templateを読み込む

go のコードから静的なファイルを読み込む時と同様に app.yaml があるディレクトリと同じ階層に template のディレクトリを作成し go のコードから相対パスなしで直接 template のファイルを指定することで読み込むことができます。

ディレクトリ構成は以下です。

├── app
│   └── cmd
│       └── main.go
├── app.yaml
├── go.mod
├── handler
│   └── index.go
└── templates
    └── index.tmpl

template を呼び出す側は変更ありません。

tmpl, err := template.ParseFiles("templates/index.tmpl")
if err != nil {
    panic(err)
}
if err := tmpl.Execute(w, nil); err != nil {
    panic(err)
}

static ファイルを読み込む

最後に js や css といったファイルのサーブの設定ついてですが、これは元々の設定と変わらず handler property で設定します。
/static/js/index.js というファイルを読みたい場合は、app.yaml の static_dir に static を指定します。

ディレクトリ構成は以下

├── app
│   └── cmd
│       └── main.go
├── app.yaml
├── go.mod
├── handler
│   └── index.go
├── static
│   └── js
│       └── index.js
└── templates
    └── index.tmpl

sample app.yaml

runtime: go111
service: gae-go111-app
main: ./app/cmd
handlers:
  - url: /static
    static_dir: static
    secure: always

sample index.tmpl

{{ define "index.tmpl" }}
    <html lang="ja">
    <head>
        <title>test</title>
    </head>
    <body>
    <p>hello world</p>
    </body>
    <script src="/static/index.js" ></script>
    </html>
{{ end }}

これらを実際に設定してみて、デプロイすると template を読み込んだ時に一緒に js も読み込まれます。

※ localで go run ./PathTo/main.go で起動した場合、static ディレクトリにルーティングされない(appengine を起動しないといけない) ので、デプロイするか、 dev_appserver.py app.yaml で appengine を起動させてみての確認が必須です。

まとめ

app.yaml の main を指定することで 1st Generation の時のようなプロジェクト構成でも静的なファイルをサーブして読み込むことが可能です。

ref

コードはこちらに置いておきました。

github.com

自前でUnmarshalを実装したときの panic を回避する

カスタム Struct に自前で Unmarshal を実装して json -> object に decode するときに無限ループを引き起こして panic しないようにするTipsです。

サンプルケースですが、以下のコードは json を decodeす るときに panic を引き起こします。

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

func (p *Person) UnmarshalJSON(data []byte) error {
    if p.Age == 0 {
        p.Age = 10
    }
    return json.Unmarshal(data, p)
}

refs: https://play.golang.org/p/7RtVampOalT

理由は json を decode する対象の Person struct に対して再帰的に UnmarshalJSON をかけてしまい無限ループに陥ってしまうからです。

こう言ったケースでは UnmarshalJSON メソッド内で別の type として Alias を切ることで無限ループを回避できます。

func (p *Person) UnmarshalJSON(data []byte) error {
    if p.Age == 0 {
        p.Age = 10
    }
    type Alias Person
    pp := &struct {
        *Alias
    }{
        Alias: (*Alias)(p),
    }
    return json.Unmarshal(data, pp)
}

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

元ネタはこちらの記事です↓

http://choly.ca/post/go-json-marshalling/

テストの中で使うダミーデータを作成する

前提

を使ったケースにおいてデータベースのアクセスする実装のテストを書くときにダミーデータをセットアップする方法を記載します。

今回は追加でテストツールとして testerator を使っています。 testerator: testerator - GoDoc

愚直にデータを作って、消す

ベタな方法です。単体テストの内部でデータを作成してそのテストケースが終わったら削除して次のテストケースでもう一回作ります。

type Data struct {
    ID   int64  `datastore:"-"`
    Name string `datastore: "Name"`
}

func TestMethod (t *testing.T) {
    tests := []struct{
        name  string
        src   *Data
        want  string
    }{
        { name: "case_1", src: &Data{name: "alice"}, want: "bob"},
        { name: "case_1", src: &Data{name: "hoge"}, want: "fuga"},
    }
    
    _, ctx, _ := testerator.SpinUp()
    defer testerator.SpinDown()

    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.go) {
            k := datastore.NewIncompleteKey("SampleKind")
            kk, err := datastore.Put(ctx, k, tt.src)
            if err != nil {
                panic(err)
            }

            // 何かしらのテスト

            // テスト終了後にput時に返されたkeyを指定して削除する。
            if err := datastore.Delete(ctx, kk); err != nil {
                panic(err)
            }
        })
    } 
}

特に何も考えずにやるならこの方法かなと思います。DeleteするのはPutしたときに帰ってきたCreateされたレコードのDatastoreのKeyを使うことくらいかと思います。

ここでは純粋なDatastoreのパッケージ使いましたが、普段は業務ではgoon使ってるのでデータの作成と削除はgoonのPutとDeleteを使ってます。

テストケースごとにSpinDownする

これがもう一つ考えたのが、テストケース回すごとにInstance消せばdatastore丸ごとリセットできるんじゃないかと思って以下のようなパターンです。 上記のテストごとに作って消すでもいいと思ったんですが、常にフレッシュな状態のインスタンスでテストしたいケースとかあると思うのでテストケースごとにappengineのインスタンスを消すパターンを考えました。

testerator内のdatastoreやmemcacheをimportしておくと、SpinDown() ごとにdatastore、memcacheを丸ごと消してくれるのでテストケースごとに SpinDown() させます。

import (
    // do testerator feature setup
    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/search"
    _ "github.com/favclip/testerator/memcache"
)

# 略

type Data struct {
    ID   int64  `datastore:"-"`
    Name string `datastore: "Name"`
}

func TestMethod (t *testing.T) {
    tests := []struct{
        name  string
        src   *Data
        want  string
    }{
        { name: "case_1", src: &Data{name: "alice"}, want: "bob"},
        { name: "case_1", src: &Data{name: "hoge"}, want: "fuga"},
    }
    
    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.go) {
            _, ctx, _ := testerator.SpinUp()
            defer testerator.SpinDown() // func(t *testing.T) ごとに呼ばれる。
         
            k := datastore.NewIncompleteKey("SampleKind")
            kk, err := datastore.Put(ctx, k, tt.src)
            if err != nil {
                panic(err)
            }

            // 何かしらのテスト
        })
    } 
}

この方法、綺麗なインスタンスの状態が欲しいときだったり、Context引き回したくない場合にはいいかなと思いますが、テストごとにインスタンス立ち上げるコストがかかるので多分テスト遅くなります。

追記

testerator.SpinDownは、起動しているインスタンス数の残数が0の時はインスタンスをそのまま落としますが、1つ以上残数が残っているとインスタンスを落とさずに状態をcleanupのみしてくれます。

SpinDown dev server.

This function clean up dev server environment. However, internally there are two types of processing. #1. if internal counter == 0, spin down dev server simply. #2. otherwise, call each DefaultSetup.Cleaners. usually, it means cleanup Datastore and Search APIs. see document for SpinUp function.

refs: https://godoc.org/github.com/favclip/testerator#SpinDown

そのため テストケースごとにSpinDownする 場合のときは TestMainで先に testerator.SpinUp しておく良いです。そうすると、forの中でインスタンスのSpinUp/Down を繰り返しても、常にインスタンスの残機が1つ以上残ってる状態なのでインスタンスを落とさずにDatastoreの状態をCleanupしてくれて、毎回インスタンスを起動し直さなくても、まっさら状態のインスタンスを使えて、かつテストが高速化できます。

残機のカウントの実装は以下にあります。
refs: https://github.com/favclip/testerator/blob/master/testarator.go#L164-L175

まとめ

ケースバイケースだと思いますが、多分都度作って消す方がコスト安いし良さそうです。(個人の主観です。) TestMainの実装をするのであれば、テストケースごとに SpinUp/Down をする場合もアリかなと思います。

CookPad TechConf 2019に参加してきた

CookPad TechConf 2019に参加してきたのでさらっとまとめておきます。

概要

https://techconf.cookpad.com/2019

スライドは後日上記公式サイトで公開されるとのことなのでここでは省きます。

感想

箇条書きでまとめます。

  • 前半はTechConfというよりProductConfといった感じで、プロダクト開発における実践的な内容のセッションが多かったです。エンジニアだけでなく、PdMやデザイナーの方も参加するといいなと思いました。
  • Rubyに関連するセッションがありませんでした。
  • もの作りに置いて仮説検証にとても大事にしていました。
    • 検証過程や検証ツール、その都度作り手としてどんな仮説を立てているのか?ということを話ていただけました。
      • 実践「デザインスプリント」くらい徹底していたセッションもあって勉強になりました。
    • 話してる内容それ自体はサービス土着のものだけど仮説に至る経路は汎用的な内容でした。
    • 泥臭い内容もあって好き。
  • とにかく ユーザーのため ということが徹底されていました。
    • とても好印象。上述の仮説検証に重きを置いてることとも重複しますが、技術をユーザーのために使ってました。
      • コード0行でMVP作った話やカオスエンジニアリングのセッションでは特に感じました。

まとめると、プロダクト開発における技術の使い方の思想が一貫してると思いました。

  • 新規で開発するフェーズは技術に寄らず仮説の精度を高める施策をバシバシ打つ。
  • グロースするフェーズではゴリゴリ自動化するために技術を使いまくる。
  • しかし全てはユーザーのため、そしてそれを支える開発者のため。

雑にまとめましたがこんな感じでした。
ちなみにRubyの会社だと思ってましたがGoも使われてました。Gopherくんが登場していました。

そのほか

気づいたことまとめておきます。

  • 数年参加し続けてますが、多分平日開催だったの初めて。
  • 今年は司会がAlexaではなく人間だった。

App Engineでマルチテナントなアプリを作る

Google App Engine上でマルチテナントなアプリを作る方法について調査したのでブログにまとめてみます。

マルチテナントとは?

IT用語辞典には以下のように記載されています。

マルチテナントとは、SaaSクラウドコンピューティングなどで、機材やソフトウェア、データベースなどを複数の顧客企業で共有する事業モデル。

マルチテナントとは1つのアプリケーションを複数のクライアントで共有することを意味します。

App Engineにおけるマルチテナント

App Engineは Namespace API を使ったマルチテナントの実装がサポートされています。 具体的には 公式ドキュメント に以下のように記載されています。

複数のクライアント組織に対応する個別のデータ パーティション(テナント)を提供することで、「マルチテナンシー」をサポートできます。これにより、すべてのテナントで同じデータスキーマを保持しながら、各テナントのデータ値をカスタマイズできます。マルチテナンシーではテナントを追加するときにデータ構造を変更する必要がないため、新しいテナントのプロビジョニングが効率的になります。

これにより、1つのアプリケーションをNamespaceで区切られた複数の環境で同じように動作させることが可能で、さらにこのNamespaceで区切られた環境は互いに干渉することはありません。

Namespace API

マルチテナントアプリケーションを実現するために App Engineでは Namespace API が用意されています。

cloud.google.com

ドキュメントに記載されていますが、現在サポートされてる言語は javapython、goに3言語で、Namespace API名前空間を使用するApp Engine(※1)のサービスは以下です。

  • Datastore
  • MemCache
  • Task Queue
  • Search

※1. google.golang.org/appengine のappengine package。cloud.google.comの方ではない。

このAPIを使ってマルチテナントアプリケーションを構築することで、例えば以下のようなことが可能になります。

  • Namspaceで区切られた環境は互いに干渉することがないのでそれぞれ独立したアプリケーションとして動かすこと。
  • ある名前空間を持つ環境で操作したデータが別の名前空間に影響するようなことがない状態を作ること。
  • これにより 1つのコードベースで様々な用途の環境を同時に複数用意する こと。(ex. 検証環境 etc...)

実装方法

具体的な実装方法については 公式のドキュメントを参照してください。ここでは名前空間を指定してDatastoreの新しいデータを追加する方法について記載します。

// 登録する名前空間をcontextに入れる
ctx, err := appengine.Namespace(appengine.NewContext(req), "emahiro")
if err != nil {
   panic(err)
}

key := datastore.NewKey(ctx, "SampleKind", "prop", 0, nil)
// datastoreのput処理

これだけで名前空間で区切られた環境にデータが登録されます。consoleでは以下のようにKindを選ぶ前に名前空間を選ぶUIが表示されるようになります。
ここではAppEngineのデフォルトの名前空間の他に新しく登録した emahiro という名前空間が作られていることがわかります。

※ 繰り返しになりますが、詳細な実装方法、およびDatastore以外の実装については公式ドキュメントを参考にしてください。

contextのなかに Namespaceの文字列を入れるだけという非常にシンプルな方法で名前空間を分けてデータを入れることができました。
ここでは Datastoreでの実装方法を記載しましたが、Namespace APIは DatastoreとMemCacheの両方で使われるので https://github.com/mjibson/goon を使っていてもマルチテナントアプリケーションに対応できます。(Goonも中ではdatastoreのAPIを使っているで。)

まとめ

  • App Engineにはマルチテナントアプリケーションを構築できる機能が備わっている。
  • Namespace APIを使うと簡単に名前空間を設定可能。
  • 分けられた名前空間は互いに干渉しないので、検証や配布など様々な用途で使うことができる。

参考資料

初めて技術発表をした話

社内向けではありましたが、エンジニアキャリアの中で初めて大勢の前で技術発表をするという機会を貰い、本日発表をしてきたので、そこで感じた内容をまとめます。

発表する前の自分

  • キャリアの中で規模の大小に関わらず発表経験なし。もっぱら聞く専。
  • 自分のやってきたことが発表に値するものではないと感じていたので、技術的な発表に対して後ろ向きな姿勢。
  • 発表に対して間違いを指摘されたり、Feedbackで辛辣なことがくるのではないかとビビってた。

発表した後の自分

  • 案外喋れた。(ビビってたので結構準備したのは事実)
  • 内容だけでなくプレゼンの仕方そのものも結構難しい。
  • 思ってたほど怖くなかった。
  • そこまで辛辣なFeedbackは来ない。

発表を通して勉強になったこと

  • Google スライドの使い方
    • 特にプレゼン時にトークノートと一緒にスライドを表示させる方法に手間取ったりしました。
    • PDFの出力方法が最初わからなかったりしました。
  • 時間計測の重要性
  • スライドの作り方
    • スライドで伝えたいこととスライドの説明の補足(プレゼンそのもの)のバランスを取るのが難しかったです。どうしてもスライドに全て載せたくなってしまう。1スライド1センテンス(だったかな?)とか言いますけど、知ってるのとやるのとでは大違いでした。

今回は技術についての発表それ自体よりもプレゼンについて勉強したことが多かったです。

まとめ

案外自分のやってきたことは知らない誰かのためになることがあるんだなぁと実感しました。 自分でハードルを勝手に高くしてた節があるので、少し気が楽になったかなと思います。 (とはいえ、まだちょっと苦手です。)

あと発表後にいくつか質問受けて、うまく答えられなかったり、深く検討してなかったこともあるなーと思ってあとから調べようかなと思ったので、インプットとアウトプットのサイクルってこういうことか!って腹落ちしました。

まぁなんにしても無事に終わってよかったです。

追記

許可が降りたので当日発表した資料を記載します。
FeedbackはTwitterのDM等でいただけると。

speakerdeck.com

Intellij IDEA をCLIから起動する

Intellij IDEAをコマンドラインから起動する設定について記載します。

手順

  1. メニューバーのTools > Create Command-line Launcher ... を選択

image_create_cl_launcher

  1. コマンドの作成先を指定する(必要に応じてpathを通す。)

set_idea_cmd_path

  1. コマンド確認
$ which idea
/usr/local/bin/idea

使い方

コマンド単体で叩いて Intellij IDEA を起動することもできますが、プロジェクト指定すればプロジェクトまで一気に開いてくれます。

# 単体起動
$ idea

# プロジェクト起動
$ idea ./PROJECT_PATH

IDEを使ってるとDockから起動するか、spotlightやalfredから起動することが多いですが、やはりterminalから起動したくなることが多いので、設定方法をまとめてみました。

『静的解析をはじめよう - Gopherをさがせ!』をやった

golang.tokyoが公開しているCodeLabをやってみました。

golangtokyo.github.io

普段からあまりコードの静的解析をやっているわけではありませんが、Goで静的解析をする場合にはどうするのか、というのを学ぶにはとてもいい教材でした。

特に「構文解析」の章が勉強になって、以下の部分

   // ファイルごとのトークンの位置を記録するFileSetを作成する
    fset := token.NewFileSet()

    // ファイル単位で構文解析を行う
    f, err := parser.ParseFile(fset, "_gopher.go", nil, 0)
    if err != nil {
        log.Fatal("Error:", err)
    }

これだけで構文解析が完了して、あとはトークンの位置情報を記録した fset 変数を ast.Inspect メソッドに渡してガリガリ構文チェックする。
Goだと結構簡単にファイルの構文解析ができるんですね。

普段web開発にしか触れてないですけど、たまにこういったことに触れておきたいなと思いました。

Intellij で goappコマンドを動かす

※ 作業ログです。自分の環境で動作させたので再現性があるかは不明です。

事前準備

使用しているshell(bash, zsh, fish..etc)でgoappコマンドにpathが通ってる状態であること。

Intellij上の設定

$ which goapp
# ここでpathが通っていれば問題ない。

pathが通っていない時...
Intellij上でterminalを開き echo $PATH する。

この時何も設定していないと /usr/sbin /sbin /usr/local/sbin のみpathが通っている....はず。 なので自分の使ってるshellの設定ファイルで 上記の3つにもpathを通す。

Intellijを再起動する。

$ which goapp
/HOME_PATH/google-cloud-sdk/platform/google_appengine/goapp

通っているっぽかった。

おしまい...(これであってるのか....)

TypeScript の Optional Property

TypeScriptにはObjectのもつpropertyに Optional Property という機能があります。

propertyに ? をつけることでそのpropertyがない場合もあるよということを表現することができます。

公式のドキュメントには以下のように記載されています。

  • Not all properties of an interface may be required. Some exist under certain conditions or may not be there at all.

ref. https://www.typescriptlang.org/docs/handbook/interfaces.html

あってもなくてもどちらの場合も許容する、propertyを宣言するときに使います。

それだけだと「じゃあそんなpropertyを明示的に宣言する必要があるのか?」とか「正規でpropertyを定義しておけばいいじゃん」となりますが(僕もそう思いました)、例えば以下のような必須でない補足情報を追記したり(case1)、propertyが入ってるケースにおいて型を制限したり(case2)するときに使います。

case1: 何か付加情報を追記したいが、それはoptionalな値であること

interface Person {
  name: string // 必須
  age: number // 必須
  extra?: object // optional
}

case2: propertyが入ってるケースに置いて型を制限する

interface UserAgent {
  os?: 'mac' | 'win'
}

このケースでは os propertyは 存在しないかもしれないが、存在する場合は mac or win という文字列のみ許容する という表現になります。

どちらのケースにおいても別に ? をつけなくてもいいと思いますし、つけない方がinterfaceの構造を厳密に定義することができます。

しかし、propertyがないcaseにinterfaceの構造レベルでコンパイルエラーが検出されますし、それだけのためにいらないproperty込でインスタンス化するようなことにもなると思います。

// case1
const person: Person = {name: 'bob', age: 20} //OK

// case1': extra propertyをrequiredにする
const person: Person = {name: 'bob', age: 20} //Error
const person: Person = {name: 'bob', age: 20, extra: {}} //OK

Optional Propertyを使わないケースでのPersonオブジェクトのインスタンス化は冗長です。

また、可読性の観点からも 存在しないかもしれない値だとinterfaceを見ればわかります。

Optional Propertyは Typescriptの表現力の高さを感じさせるものでした。

『ファクトフルネス』を読んだ

『ファクトフルネス』を読んでとても面白い書籍だったので、学んだことの整理もかねて備忘録として書録を記載します。

ファクトフルネスに世界を捉えること

本書の中に書かれたことを自分なりに整理すると「事実に基づいて世界を見ることを阻害する人間の持つ10の本能」を コントロールすること だと思います。
コントロールとしたのは、ファクトフルネスを阻害する本能は、文字通り本能なので、全くなくすことはできません。しかし、何か本能を刺激する事象に触れた時に、出てこようとする本能をコントロールすることはできます。
本能が前に出てきたなと感じたら、一呼吸置いて、俯瞰して目の前の事象に対して客観的に事実に基づいて見ることを意識すること、それが無意識でできるようになることなのかなと本書を読んで感じました。

以下は「事実に基づいて世界を見ることを阻害する人間の持つ10の本能」とそれにどう対処するかについて、本書に記載されてる内容をまとめました。

分断本能

世界は分断されているという思い込み。

ex.

  • 先進国/後進国
  • 西欧諸国/それ以外

ファクトフルネスに考える

大半の人間がどこにいるのかを探す。

  • 話の中の「分断」を表す言葉 に気づくこと。
    • 多くの場合は、分断はなく、誰もいないと思われていた中間層に大半の人がいる。
    • 大半の人がどこにいるのか? を探すこと。
  • 「平均の比較」 に注意する。
  • 「極端な数字の比較」 に注意する。
  • 上からの景色 であることに注意する。
    • 上から観ると同じ「低い」でも下から見ると一段階違うと全く違う。

ネガティブ本能

「世界はどんどん悪くなっている」という思い込み。

ファクトフルネスに考える

悪いニュースの方が広まりやすいと覚えておく。

  • ネガティブなニュースに気づく。
    • ネガティブなニュースは耳に入りやすい。
  • 「悪い」と「良くなっている」は両立する。
  • 良い出来事はニュースになりにくい。
  • ゆっくりとした進捗はニュースになりにくい。
  • 悪いニュースが増えても、悪い出来事が増えたとは限らない。
  • 美化された過去に気をつける。

直線本能

「ひたすら〇〇し続ける」という思い込み。

ファクトフルネスに考える

直線もいつかは曲がることを覚えておく。

  • 「グラフはまっすぐになるだろう(線形に進む)」という思い込みに気づくこと
    • 線形に進み続ける方が珍しい
  • 直線のグラフばかりを当てはめないようにする。
    • S字、滑り台、コブ、倍増、いろんなグラフの形がある。

恐怖本能

危険でないことを「恐ろしいこと」と考えてしまう。

ファクトフルネスに考える

リスクを計算する。

  • 恐ろしい物事には自然と目がいってしまうことに気づくこと。
    • 恐怖と危険は異なる。
  • 世界は恐ろしいと思う前に、現実を見ること。
    • メディアや自分自身の関心フィルターのせいで世界は実際より恐ろしく見えてしまう。
  • リスク = 危険度×頻度質×量
    • 恐ろしさはリスクとは違う
  • 行動する前に落ち着くこと。

過大視本能

目の前の数字が一番重要だという思い込み。

ファクトフルネスに考える

出てきた数字を比較する。

  • ただ1つの数字が、とても重要であるかのように勘違いしてしまうことに気づくこと。
    • 比較、割り算など他の指標を駆使して別の意味を見いだせる。
  • 比較すること。
  • 80:20ルールを使うこと。
  • 割り算をすること。
    • 量それ自体よりも大抵は「割合」の方が役に立つ。

パターン化本能

1つの例が全てに当てはまるだろうという思い込み。

ファクトフルネスに考える

分類を疑う。

  • 1つの集団のパターンを根拠に物事が説明されていたら、それ自体に気づくこと。
    • パターン化は間違いを産みやすい。
    • パターン化本能をコントロールするには、分類を疑うこと
  • 同じ集団の中にある違いを探すこと。
  • 違う集団の中の共通項を見つけること。
    • ex.) 女性一人当たりの子供の持つ人数は地域が違えど関係する指標は所得である。
  • 違う集団の間の違いも探そう。
    • ex.) 意識のない兵士(成人)と眠っている赤ちゃんへの対応。
  • 過半数」に気をつけること。
  • 強烈なイメージに注意すること。
  • 自分以外アホだと決めつけないこと。

宿命本能

全てはあらかじめ決まっているという思い込み。

ファクトフルネスに考える

ゆっくりした変化でも、変化していることを心に留めておく。

  • 色々な物事(ヒト、国、宗教、文化)が変わらなく見えるのは、日々の変化がゆっくり少しずつ起きているからだと気づくこと。
  • 小さな進捗を追いかけること。
  • 知識をアップデートすること
  • おじいさんやおばあさんにも話を聞くこと。
  • 文化が変わった例を集めること。

純化本能

世界は1つの切り口で理解できる、という思い込み。

ファクトフルネスに考える

1つの知識が全てに応用することはできない。

  • 1つの視点だけでは世界は理解できないことを知ること。
    • なんでもトンカチで叩くのではなく、様々な道具の入った工具箱を準備した方が良い。
  • 自分の考え方を検証すること。
  • 知ったかぶりはやめること。
  • やたらめったらトンカチを振り回すことをやること。
  • 数字は大切だが、数字だけに頼らないこと。
  • 単純なものの見方と、単純な答えには注意すること。
    • シンプルであることと、簡単であることは違う。

犯人探し本能

誰かを責めればはものごとは解決する、という思い込み。

ファクトフルネスに考える

誰かを責めたところで、問題は解決しない。

  • 誰かが見せしめとばかりに責められていたら、それに気づくこと。
    • 誰かを責めると他の原因に目がいかなくなり、将来起こるであろう同じ間違いを防げなくなる。
  • 犯人ではなく、原因を探すこと。
  • ヒーローでなく、社会を機能させている仕組みに目を向けること。

焦り本能

今すぐ手を打たないと大変なことになる、という思い込み。

ファクトフルネスに考える

小さな一歩を重ねる。

  • 「今すぐ決めないいけない」と感じたら、自分の焦りに気づくこと。
    • そもそも、今(その時)決めないといけないことは滅多にない。
  • 落ち着くこと
    • 深呼吸、深呼吸。
  • データにこだわること。
  • 占い師に気をつけること。
  • 過激な対策に注意すること。

まとめ

紹介されていた本能は自分も働いてることが今まで多々ありました。特に、

  • 極端な2つの集合に分けるのではなく、レベル1~4の段階でそれぞれ集合があること。そして、中間があることを念頭に置くこと。
  • ある1時点の数字や現実の結果だけに目を向けず、過去からの進捗をみること。
  • 今時点で「悪い」ことと現在進行形で「良くなっている」は両立すること。

この3つはこの本を読んで「確かに!」と気付かされたことでした。どうしても、広告やテレビを始めたとしてマスメディアの持ち出すドラマティックなメッセージやストーリー性に引きづられてしまって、思考停止して事実を正確に捉えられないことがあります。
正直これ自体はこのエントリの冒頭でも記載した通り、本能なのでなくすことはできませんが、本能の活動を意識的にコントロールするための材料を手に入れることができたと思います。

身の回りに溢れている、極端な分断的表現、ドラマティックなストーリーや単一の数字には注意深くなるきっかけを与えてくれるいい書籍でした。

Intellijでlspを使う

lsp(Language Server Protocol) とは?

The Language Server protocol is used between a tool (the client) and a language smartness provider (the server) to integrate features like auto complete, go to definition, find all references and alike into the tool

cf. Langserver.org

自動補完、定義ジャンプ、参照の検索などの機能を各ツール(editor or IDE and so on...)に統合するために、ツール(クライアント)と言語機能プロバイダ(サーバー)間でおしゃべりできるようにする共通仕様です。
共通仕様になってるから今までは自動補完始め開発支援系のライブラリが色んな言語、IDE独自で開発しなくて良くなるのだと思います。

cf. Big Sky :: gocode やめます(そして Language Server へ)

Intellijにlspを導入してみた

公式でプラグインが出てるので入れてみました。
ほとんどGitHubに記載されてる内容ですが、Goで実際に試してみます。

github.com

手順

Preference > Pluginでlsp関連のプラグインを検索してインストール、再起動をします。

f:id:ema_hiro:20190111025121p:plain

以下の項目があればOKです。

f:id:ema_hiro:20190111025141p:plain

ドキュメンテーション

これ思いの外便利です。まず定義にhoverするとhoverした定義のドキュメントが表示されます。ショートカットはデフォルトでは ^ + J です。 さらにhoverした状態で '^ + J' を押すと右側に詳細なドキュメントが表示されます。

ドキュメントの表示設定は Preference > Editor > Generalの下記の場所にチェックマークを入れます。

f:id:ema_hiro:20190111025806p:plain

実際に表示してみた結果が以下(net/httpパッケージで試してます。)

f:id:ema_hiro:20190111024809p:plain

f:id:ema_hiro:20190111024826p:plain

f:id:ema_hiro:20190111024907p:plain

コードジャンプ(定義ジャンプ)

別ウィンドウが開くようになりました。今まではコードの定義に対して Ctrl or Command + Click/ Ctrl or Command + N をすると定義の下部にずらっと一覧が出ましたが、別ウィンドウになったので検索性が向上したように思います。
ただ、その他は特にLSPを入れてIntellijがLanguage Server Clientになったからといって特段変わったことがあるようには感じませんでした。ショートカットも同じですし。

コード検査

Analyze -> Inspect CodeでLSPを使ってるかどうか確認できます。 検査すると検査結果一覧が下部に出ます。この辺もLSP使う前とあんまり挙動自体は変わっていないと思います。

f:id:ema_hiro:20190111025854p:plain

まとめ

試しに使ってみましたが、あんまり導入前と変更点はなさそうでした。まだlsp自体も発展途上っぽいので今後に期待。

intellijのgoのプラグインではそもそも今現在LSPをサポートしていないので、この辺はクライアント(IDE)自体がLSP対応するプラグインを用意していても、言語側のプラグインが対応するのを待つことになると思います。

plugins.jetbrains.com

しかし、現時点でまだまだ機能が不十分とはいえ、ドキュメントをそのまま参照できるようになる機能だけでも良さそうでした。
IntelligのLSPの設定にはLanguage Serverの参照先追加設定とかもありましたが、この辺まだよく使い方わかってないので追々わかったら追記していこうと思います。