読者です 読者をやめる 読者になる 読者になる

Goと過ごした1年間

今日で仕事納めだったので、振り返っておく。

今年はGoのコードをたくさん書けた1年間だったと思う。

  • サービス基盤の一部をGoで開発した
  • 新人研修の主な言語としてGoを教えた
  • 既存のRailsで作ったサイトのリプレースをGoでやった
  • 社内で使うツールをGoで作っている
  • goaを触り始めた

サービス基盤の開発

去年の後半から始まって、今年の3月末にリリースが完了した。モバイルアプリ用のpush配信周りの実装を担当したが、APNSに関連してHTTP/2を使ったやりとりとかも経験できて面白かった。

周囲の特に上司の理解と協力もあって、社内でGoを使ったプロジェクトの実績が作れたのは大きかった。 前例ができたからか、以降「Goでやります」と言いやすくなったし、リプレースのプロジェクトにもつながったと思う。

新人研修

自分のチームに配属になったのが6月からで、それからGoを教えて、社内で使うツールを作ってもらった。とりあえず年末を目処にそこそこ使える形のあるものはできたので良かったと思う。

研修が始まる前には、プログラミング自体を経験したことがない、という人にGoを教材としてプログラミングを教えることが正解なのかどうか、かなり悩んだ。

現状Goでの、プログラミングが未経験という人に向けた情報が少ないと思う。なので、参考になる事例も見当たらず、本当に研修を全うできるのか?という不安があった。

正直言うと(本人にも伝えたが)途中で辛くなって、Railsでなんかパッと動くやつを課題に設定した方が、研修としては面白かったのでは?とか思ったこともあって、モチベーションが低かった時もあった。

最終的には、そこそこのコードは普通に書けるねという感じになってきているし、良かったと思う。

研修をしてわかったことは、新人でもGoで書いたコードは普通に質が高いというか、誰が書いても同じようなコードになるのが不思議だなと。瑣末なところはもちろん色々あるんだけど、おかしなコードを書いているってことは感じたことがない。

リプレース

Railsで作ったサイトをリプレースした。もともと小規模なサイトだったので、Goで一から作ろうということになって、3サイトをリプレースした。

リリースしてからリソースのグラフをたまに見ているが、Railsで起きていた徐々にメモリ消費が増えていくということもなく、安定している。

サーバーのスペックを落としても充分さばけると思うし、お客さんの費用に直結するので、省リソースでサイトが運用できるというのは良いことだと思う。

ついでに社内のサービスと接続するためのAPIクライアントも作れたので、ますますGoでプロジェクトを進めやすくなった。

社内ツールもろもろ

chatops(ってもう死語?)とかその辺でslackのbot作ったりとか、他にも細々としたツールを作っていて、なんか作る時にはGoで作るようにしている。

やっぱりポータビリティというか、コンパイルしてコピーして動くっていう手軽さは大きい。

goa

Goでウェブアプリケーション作るときに、あまり大きな(リフレクションを使うような黒魔術的)フレームワークは採用したくない。でもチーム開発する以上ある程度のルールは必要で、goaを使えばその辺を解消できると思う。

例えば、基本的なバリデーションとか細々した実装がそこそこあってちょっとしんどい、みたいなところがラクできそう。

goa自体はAPIサーバーを前提としていると思うけど、管理画面とかHTMLを返したいし、その辺を独自ジェネレーターとかDSL拡張とかで対応できないか試行錯誤しているところ。

apexと組み合わせてAWS lambdaで動かしたりしたい。

おわりに

特にまとめはない。

Goをやるぞ!と言って実際に実績が積めているのは大変嬉しいことだし、もっと社内でも普及活動をしていこうと思う。

Goのhttp.Headerについて

この記事は Go (その3) Advent Calendar 2016 の15日目の記事です。

GoでHTTPのHeaderを扱う際に使用する http.Headermap[string][]string を拡張している。なので、直接キーにヘッダー名を使用して値を操作することができる。 また GetSet などアクセサを介してもデータを取得することができる。たぶんアクセサを使用することが多いと思う。

例えば、以下のようなことができる。

import (
    "fmt"
    "net/http"
)

func main() {
    h := http.Header{}
    h.Set("User-Agent", "Dummy")

    fmt.Println(h.Get("User-Agent"))
    fmt.Println(h["User-Agent"])
    
    fmt.Println(h.Get("user-agent"))
    fmt.Println(h["user-agent"])
    
    fmt.Println(h.Get("USER-AGENT"))
    fmt.Println(h["USER-AGENT"])    
}

出力結果は、

Dummy
[Dummy]
Dummy
[]
Dummy
[]

となる。 Get を使用するとヘッダー名の大文字小文字に関係なく値が取得できる。

なぜだろう?気になったので調べてみた。

アクセサを使用するとキー名が正規化される

http.Header のアクセサでは net/textproto パッケージの MIMEHeader が使用(キャスト)されていて、そのアクセサを呼ぶようになっている。

// Set sets the header entries associated with key to
// the single element value. It replaces any existing
// values associated with key.
func (h Header) Set(key, value string) {
    textproto.MIMEHeader(h).Set(key, value)
}

// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns "".
// To access multiple values of a key, access the map directly
// with CanonicalHeaderKey.
func (h Header) Get(key string) string {
    return textproto.MIMEHeader(h).Get(key)
}

textproto.MIMEHeader のアクセサでは、 textproto.CanonicalMIMEHeaderKey が使用されている。

// Set sets the header entries associated with key to
// the single element value. It replaces any existing
// values associated with key.
func (h MIMEHeader) Set(key, value string) {
    h[CanonicalMIMEHeaderKey(key)] = []string{value}
}

// Get gets the first value associated with the given key.
// If there are no values associated with the key, Get returns "".
// Get is a convenience method. For more complex queries,
// access the map directly.
func (h MIMEHeader) Get(key string) string {
    if h == nil {
        return ""
    }
    v := h[CanonicalMIMEHeaderKey(key)]
    if len(v) == 0 {
        return ""
    }
    return v[0]
}

この textproto.CanonicalMIMEHeaderKey が、MIME Headerのルールに合うようにヘッダー名を変更してくれている。

// CanonicalMIMEHeaderKey returns the canonical format of the
// MIME header key s. The canonicalization converts the first
// letter and any letter following a hyphen to upper case;
// the rest are converted to lowercase. For example, the
// canonical key for "accept-encoding" is "Accept-Encoding".
// MIME header keys are assumed to be ASCII only.
// If s contains a space or invalid header field bytes, it is
// returned without modifications.
(実装は省略)

そういうことで、 http.Header のアクセサを使うと、ヘッダー名の大文字小文字を気にせず値を取得することができるようになっている。

なお net/http パッケージにも CanonicalHeaderKey という textproto.CanonicalMIMEHeaderKey をラップした関数が用意されている。

ヘッダー情報を map[string]string で扱いたいとき

以下のようなJSONをUnmarshalしたい場合、

{
    "headers": {
      "Accept": "*/*",
      "Accept-Encoding": "gzip, deflate",
      ...
    },
    "body": "{\r\n\t\"a\": 1\r\n}"
}

次のような構造体では上手く行かない。http.Header の実体は map[string][]string だから。

type Request struct {
    Headers http.Header     `json:"headers"`
    Body    json.RawMessage `json:"body"`
}

map[string]string だと当然だが上手く行く。

type Request struct {
    Headers map[string]string `json:"headers"`
    Body    json.RawMessage   `json:"body"`
}

マップの値を取得する場合には、キーに対して CanonicalHeaderKey を使う。

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    b := []byte(`{
      "headers": {
          "Accept-Encoding": "gzip, deflate"
      },
      "body": "{\r\n\t\"a\": 1\r\n}"
  }`)

    var req struct {
        Headers map[string]string `json:"headers"`
        Body    json.RawMessage   `json:"body"`
    }

    json.Unmarshal(b, &req)
 
    key := http.CanonicalHeaderKey("accept-encoding")
    fmt.Println(req.Headers[key])
    // gzip, deflate と出力される

}

https://play.golang.org/p/QkMCHmx36M

ヘッダー名が正しく送信されてくるとは限らない

上記の例で、送信されてくるヘッダー名が正規化されていれば良いが、そうじゃないこともある。

Configure Proxy Integration for a Proxy Resource - Amazon API Gateway に載っているJSONでは…

    "headers": {
      "Accept": "*/*",
      "Accept-Encoding": "gzip, deflate",
      "cache-control": "no-cache",
      "CloudFront-Forwarded-Proto": "https",
      "CloudFront-Is-Desktop-Viewer": "true",
      "CloudFront-Is-Mobile-Viewer": "false",
      "CloudFront-Is-SmartTV-Viewer": "false",
      "CloudFront-Is-Tablet-Viewer": "false",
      "CloudFront-Viewer-Country": "US",
      "Content-Type": "application/json",
      "headerName": "headerValue",
      "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com",
      "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f",
      "User-Agent": "PostmanRuntime/2.4.5",
      "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)",
      "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==",
      "X-Forwarded-For": "54.240.196.186, 54.182.214.83",
      "X-Forwarded-Port": "443",
      "X-Forwarded-Proto": "https"
    },

cache-control とか headerName とかある。これは単なるtypoかもしれないが、正しい情報が常に送られてくるとは言い切れない。

うまくデータを扱うためには、 json.Unmarshaler に対応して、 json.Unmarshal の際にヘッダー名を正規化するしかなさそう。 textproto.MIMEHeader を参考に、次のような型を定義してみた。

type Header map[string]string

func (h *Header) UnmarshalJSON(bytes []byte) error {
    var m map[string]string
    err := json.Unmarshal(bytes, &m)
    if err != nil {
        return err
    }

    for k, v := range m {
        m[http.CanonicalHeaderKey(k)] = v
    }
 
    *h = m

    return nil
}

func (h Header) Get(key string) string {
    if h == nil {
        return ""
    }
    return h[http.CanonicalHeaderKey(key)]
}

取得する箇所はこんな感じになる。

    fmt.Println(req.Headers.Get("accept-encoding"))
    fmt.Println(req.Headers.Get("Cache-Control"))

https://play.golang.org/p/25cUIvAkCy

無事にヘッダー名の大文字小文字を気にせずに、値が取得できるようになった。

まとめ

  • HTTP のヘッダーを扱う http.Header を使用する際にはアクセサを使用する
  • 独自にヘッダー名を扱う必要がある場合は、 http.CanonicalHeaderKey を活用する

続・GoでパスワードなどをPrintfで出力させたくない

前回、GoでパスワードなどをPrintfで出力させたくない - kawaken's blog というのを書いたけど、 その後 GoStringer というインターフェースがあることを知ったので再度試してみた。

前回のおさらい

前回上手く隠蔽できずに出力されてしまったのが、以下。 %+v だと出力されないんだけど、%#vだと出力されてしまう…

type Password string

func (p Password) String() string {
    return "FILTERED"
}

type User struct {
    Name     string
    Password Password
}

func main() {
    u := &User{
        Name:     "namae",
        Password: Password("pass"),
    }

    fmt.Printf("%#v", u)
    // &main.User{Name:"namae", Password:"pass"}

}

https://play.golang.org/p/1BT9E4dM17

GoStringerを実装する

package main

import (
    "fmt"
)

type Password string

func (p Password) String() string {
    return "FILTERED by String"
}

func (p Password) GoString() string {
    return "FILTERED by GoString"
}

type User struct {
    Name     string
    Password Password
}

func main() {
    u := &User{
        Name:     "namae",
        Password: Password("pass"),
    }

    fmt.Printf("%#v", u)
    // &main.User{Name:"namae", Password:FILTERED by GoString}

}

https://play.golang.org/p/H_ccstzjCC

よっしゃー!うまく隠蔽された。

これでログとかに意図せず出力されることが回避できそう。

GoでパスワードなどをPrintfで出力させたくない

RailsのLoggerだとリクエストパラメータにパスワードなどが含まれていると、FILTEREDといった感じで生の情報が出力されない。

Goでも、例えば、以下のような構造体Userを fmt.Printf("%#v", u) で出力したときに Password が隠れて欲しい。

type User struct {
    Name     string
    Password string
}

ノーガード

特に何もしないと、そのまま出力される。

type User struct {
    Name     string
    Password string
}

func main() {
    u := &User{
        Name:     "namae",
        Password: "pass",
    }

    fmt.Printf("%+v", u)
    // &{Name:namae Password:pass}
}

https://play.golang.org/p/HZuuaXwS4O

そりゃそうだ。

Password を Stringer にしてみる

fmtで出力されるとき、StringerだとStringが呼ばれるはず。

type Password string

func (p Password) String() string {
    return "FILTERED"
}

type User struct {
    Name     string
    Password Password
}

func main() {
    u := &User{
        Name:     "namae",
        Password: Password("pass"),
    }

    fmt.Printf("%+v", u)
    // &{Name:namae Password:FILTERED}
}

https://play.golang.org/p/hmNsB0yrbk

お、できた!?

"%#v" には勝てない

%+v だと出力されないんだけど、%#vだと出力されてしまう…

type Password string

func (p Password) String() string {
    return "FILTERED"
}

type User struct {
    Name     string
    Password Password
}

func main() {
    u := &User{
        Name:     "namae",
        Password: Password("pass"),
    }

    fmt.Printf("%#v", u)
    // &main.User{Name:"namae", Password:"pass"}

}

https://play.golang.org/p/1BT9E4dM17

ポインタ型にしてみる

ポインタ型にしたらどうだろう。

type Password string

func (p Password) String() string {
    return "FILTERED"
}

type User struct {
    Name     string
    Password *Password
}

func main() {
    p := Password("pass")
    u := &User{
        Name:     "namae",
        Password: &p,
    }

    fmt.Printf("%#v", u)
    // &main.User{Name:"namae", Password:(*main.Password)(0x1040a120)}

}

https://play.golang.org/p/_pcSzrx64p

アドレスが出力されるので、とりあえずパスワードはわからない状態になった。

まとめ

  • とりあえずStringerにする
  • 構造体のフィールドとしてはポインタ型にしておく

で良いんだろうか?

もっと良い方法ないだろうか。

Macの開発環境構築メモ

最近SSDに変えてクリーンインストールしたので、また環境構築している。

前にAnsibleのPlaybookで色々インストールできるようにしていたはずだけど、対象のソフトのバージョンが古かったりとかまぁなんかアレだったので、ぼちぼち手作業でインストールすることにした。

今まで homebrew 使わない派?だったんだけど、今回は面倒なので homebrewで楽することにした。

homebrewのインストール

Homebrew — OS X 用パッケージマネージャー

ここにコマンドが乗ってるので、コピペして実行する。

GNU のコマンドインストール

とくにsedがいつも困るので、Linuxと同じように使えるようにする。
cf: Install and Use GNU Command Line Tools on Mac OS X | Hong Xu (xuhdev)

brew install binutils
brew install findutils --with-default-names
brew install gawk --with-default-names
brew install gnu-indent --with-default-names
brew install gnu-sed --with-default-names
brew install gnu-tar --with-default-names
brew install gnu-which --with-default-names
brew install grep --with-default-names
brew install gzip
brew install watch
brew install wdiff --with-gettext
brew install wget
brew install zsh
brew install fontforge

既存のコマンドを上書きしたい時には、 --with-default-names とかのオプションが必要らしい。

fontforgeRicty のインストールに必要。

MacVimのインストール

Releases · splhack/macvim-kaoriya · GitHub

ここから最新版のdmgをダウンロードしてインストールする。

Ricty(フォント)のインストール

Mac に Ricty と Ricty Diminished をインストールする - Qiita

これを参考にした。合成には数分時間がかかる。

Terminal の設定

最近見つけた、Terminalのカラースキーマが一杯あるリポジトリから好きなのを選らぶ。

GitHub - lysyi3m/osx-terminal-themes: Color schemes for default Mac OS X Terminal.app

数年間 Solarized Dark にしてたけど、やっぱり背景色は黒色が良いかもと思って、最近は会社でも DimmedMonokai にしてる。

git clone https://github.com/lysyi3m/osx-terminal-themes.git
open osx-terminal-themes/schemes/DimmedMonokai.terminal

これでスキーマが使えるようになる。デフォルト設定とかはターミナルの設定から行う。

dotfiles の設定

githubでdotfilesを管理してるので、それを入れる。

MacVimのPATH設定が入ってなかったので追加した。

PATHの順番入れ替え

Homebrewで入れたgit,zshを優先的に利用する - Glide Note

/etc/paths で管理されている。知らなかった。

Alfred2 のインストール

AppStoreのは古いやつなので、ダウンロードしてインストールする。活用するといろいろできるの知ってるんだけど、結局簡単なランチャと電卓代わりになってる。

ATOKのインストール

課金ユーザなので使わないともったいないんだけど、もうそろそろいいかなーとか思い始めている。

ていうか今ATOKじゃないとダメな理由ってなんだろ。昔は「今日」で「2016/03/06」って変換ができたのが便利だったんだけど、デフォルトのIMEでもできるようになってるしなぁ。

AppStoreから購入済みアプリをインストール

AppStoreで買うと再インストールが楽だ。最近Macから直接S3をいじる機会が増えたので、Transmitをまた使うようになった。CyberDuckでも十分だけど。

Karabiner のインストール

もうこれがないと生きていけない体になってしまった。

  • 英数 -> esc
  • かな -> enter
  • Shift+英数 -> 英数
  • Shift+かな -> かな
  • 右コマンド -> delete

というカスタマイズをしている。

各種SDK

必要に応じて入れる。いまんところ Go くらいかな。

とりあえずそんなところかな。これ便利だよってのがあったら教えてください。

2015年振り返りと2016年目標

遅くなったけど、簡単に2015年の振り返りと、2016年の目標。

2015年初に立てた計画はどうだったか

デプロイ自動化

まだまだ細かいところの調整が必要。というか、終わりがない感じする。

読書

年間50冊が目標で、最終25冊だった。前半は調子が良かったが、後半はペースが落ちた。一番の敗因は、コードコンプリートだと思う。そんなに面白くないのに、やたら長くてモチベーションの低下が著しかった。さっさと違う書籍に移れば良かった。

目標はまったく達成できなかったが、古典で著名だからといって良い書籍ばかりではないし相性がある、ということがわかったのが昨年の成果だと思う。

チームとかの共同作業

キャリアパスや評価辺りまで面倒を見ているわけでもないし、チーム内であの人が何やってるかわかんないという状況ではないので、悩んだだけ損だったのかもしれない。人数も多くないし。

10月から3ヶ月ほど、Goの勉強も兼ねてほぼ日で簡単な問題作成をしたりしていて、教育面でサポートできたのは良かったかなと思う。

ドキュメント

Sphinxをちょっと試してみたけど、普及には至らず。

やらなくて良いことはやらない

これは意識していたので、けっこうできていた気がする。

その他

年初は想像もしてなかったが、8月からiOSアプリの開発に携わることになり、35歳にして新しいチャレンジの機会が得られたのは良かった。Goもやる機会ができて良かった。

アウトプットはほとんどできてないが、1件だけOSSにプルリクをした。簡単なドキュメントの間違いだったけど。


2016年の目標

今年はアウトプットを増やしたい。

モバイルアプリ

個人的なプロジェクトだと、3つなんかアプリを作ろうと思う。ネタはあるので、時間を作ってリリースする。

モバイルアプリと言うけど、バックエンドも作り込みいるし、時間を上手く作って達成したい。

読書

読書はムリせず30冊にしとく。月2冊か3冊。

たまには小説も読んでも良いかなと思ってる。 技術書については、DBとかOSとかのベーシックなところを読みたい。

OSSへのコントリビューション

今までやってこなかったのだけど、頑張ってみよう。簡単なタイポとかだと敷居も低い。

仕事

仕事に関しては、新入社員の教育とかの大事なこともあるんだけど、意識してコード書く時間取っていかないと、緩慢な死を迎えそうで怖い。

いかにコード書く時間を増やせるかを意識して立ち回りたい。

MacVimの起動時にメニューのエラーがでる

splhack/macvim-kaoriya を使っているんだけど、気づいたら以下のようなエラーが出るようになっていた。

kawaken % /Applications/MacVim.app/Contents/Resources/vim/runtime/menu.vim の処理中にエラーが検出されました:
行 1236:
E334: メニューが見つかりません: Help.MacVim\ Help

別に動作に問題ないのでほったらかしていたけど、ちゃんと確認してみた。

どうやらカラースキーマaltercation/vim-colors-solarized が影響しているっぽい。

" Menus "{{{
" ---------------------------------------------------------------------
" Turn off Solarized menu by including the following assignment in your .vimrc:
"
"    let g:solarized_menu=0

そもそもメニューを使ってないし、コメントに従って let g:solarized_menu=0 を設定したらエラーが出なくなった。

あるいは、MacVimのバグでは?というIssueがSolarizedの方に立っていた。

Solarized menu items in MacVim not working · Issue #133 · altercation/vim-colors-solarized

実際どうなのかわからないけど、とりあえずエラーが出なくなったので解決ってことにしておく。