Go Proverbsを勉強がてら和訳して少し解説した
Go (その3) Advent Calendar 2016 11日目の記事です。
Go言語の生みの親、Rob Pikeが2015年のGopherfestのセッションで 言っていた、Go Proverbsを自分の勉強がてら和訳してちょっと解説してみる。
後半に行くに連れて集中力が切れたり、いろいろ調査不足のところや、認識違いの部分もあると思うので、
そういう部分のところがあったら是非指摘してください。自分でも気づいたら修正していきます。
ちなみにタイトルの「Go Proverbs」は、動画の冒頭で囲碁の話をしているとおり「碁の格言」の直訳で、 「Goの格言」にかかっていておしゃれ。
メモリを共有して通信したり、通信してメモリを共有してはならない
原文は"Don't communicate by sharing memory, share memory by communicating."
これはGoでのメモリ共有モデルのお話。 (参考:https://blog.golang.org/share-memory-by-communicating)
CやJavaではメモリ空間(変数)を共有することでプロセス間で値をやり取りします。 しかし、正しく排他制御を行わないとデータ破壊が発生する可能性があります。 また、次のプロセスに情報を渡すときには現在行っている処理が終了したことを次のスレッドに伝える仕組みを用意する 必要もあります。(この仕組みを実装するためにJavaではConditionインタフェースが用意さています) これらの実装を間違うと、デッドロックやデータ破壊が発生してプログラムが正常に動作しなくなってしまいます。
Goではプロセス間の通信をchannelという仕組みを用意して、 メモリを共有するのではなく、情報(メッセージ)を送受信することで実現しています。 channelは以下のようにデータをやり取りします。
ch := make(chan int) // channelの作成 ch <- 1 // 書き込み i := <- ch // 読み込み
この時、channelへの書き込みと読み込みはGo本体が自動的に排他制御を行っているため、 プログラムを実装する側が排他制御を実装する必要はありません。 また、以下のように読み込みの時にそのチャンネルに何も書き込まれていないと、その部分でプログラムはブロックします。
package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { time.Sleep(3 * time.Second) ch <- 1 // 3秒待ってからchannel書き込み }() i := <-ch // 読み込み時には何も書き込まれていないのでブロックする fmt.Println(i) // 3秒後に実行 }
これにより、次のスレッドに現在行っている処理が終了したことを伝えるには、channelにデータを書き込むだけで よくなります。
このあたりのことを更に詳しく知りたい場合は、WebDBで牧さん(@lestrrat)が非常に詳しく解説している記事が あるので、そちらを読むことおすすめします。
並行と並列は違う
原文は"Concurrency is not parallelism."
「並行処理」と「並列処理」は、どちらも同時に処理を実行するという日本語ですが、意味合いが異なります。 このあたりはRob PikeがHerokuのWaza conference2012 (https://talks.golang.org/2012/waza.slide)で解説をしています。 (あと、先日Go初心者LT会で発表したりしました。リンク)
「並列処理」は同じ処理を行うプロセスを同時に実行している状態を指し、 「並行処理」は別の仕事をするプロセスを同時に実行している状態を指します。
Rob Pikeのスライドの14ページ目の図と 19ページ目の図との対比がわかりやすいと思います。 14ページの図のように、ドキュメントの山や焼却炉を複数のプロセスでアクセスしようとすると、ロックが発生したり プロセス同士を同期させる必要があり、それらがボトルネックになってしまいます。 しかし、19ページの図のように、1つのプロセスが1つのリソースにアクセスするようにプロセスを立ち上げると、 それぞれのプロセスが干渉しなくなり、無駄なく処理を続けさせることができます。
Go言語が持っている並行処理の機能はこれらを簡単に実現できるため、高速化の必要な処理を書くときは覚えておきましょう。
チャンネルは排他的で直列になるように調整する
原文は"Channels orchestrate; mutexes serialize."
Goのチャンネルは特に何もしなくてもで排他的かつ直列にデータを送れるようになっています。 そのため、プログラマがgoroutine間でデータのやり取りをする際、ロックや順番等をあまり気にせずに使用することができます。
大きなinterfaceは抽象度が低い
原文は"The bigger the interface, the weaker the abstraction."
これはタイトル通りで、interfaceの実装はなるべく小さくしましょう(言い聞かせ)。 小さいinterfaceで設計できていると構造体がinterfaceを満たすのが非常に容易です。 実際にGoのデフォルトパッケージにあるinterfaceは非常に小さく設計されています。
具体的にGoが標準で用意しているWiterを実装して、人に喋らせてみます。
package main import ( "fmt" "io" "os" ) type human struct { w io.Writer } func (h *human) Write(p []byte) (int, error) { p = append([]byte("Ω< "), p...) h.w.Write(p) return len(p), nil } func main() { h := &human{w: os.Stdout} fmt.Fprintln(h, "hello world") }
0値を有益にしなさい
原文は"Make the zero value useful."
Goでは値が宣言された段階で初期化されます。 この機能自体は最近の言語だとよくあることだと思うのですが、 重要なのは0値に意味に対してちゃんと意味を持たせておくことだと思います。
Goでは構造体生成時その内部のプライベートな値も全て0値で初期化されます。 しかし、Javaのコンストラクタの様に、使う側に必ず初期値を指定させる方法がありません (Newで始まるメソッドを作成することはありますが)。 そのため、初期化するための関数やNew関数を経由せずに呼び出されること困るようなコードは書くべきではなく、 必ず0値で初期化されたときのことを考慮してコードを書きましょう。
インターフェース型は何も言わない
原文は"interface{} says nothing."
Goではどんな値でも入れることの出来るとしてinterface{}型があります(正確にはメソッド定義のないinterfaceだから どんな値でも入れられる)。 引数の型等に仕様するとどんな型でも受け取れるようになり、汎用的なものになる気がするのですが、 Goの1つの特徴である、静的型付け言語という特性を捨てることになります。 interface{}型が使用されている部分で、正しく型アサーションや例外処理等が行われていれば問題ないのですが、 それが行われていない時にはコンパイル時にバグが発生することを察知できません。 何でも渡したい時には、もしかしたらMap型の方が適切だったり、自ら構造体を定義しておいたほうが余計なバグが 減らせる可能性が高いので、なんでもかんでもinterface{}型で引数を受け取るようにするのはやめましょう。
Gofmtのスタイルは誰かのお気に入りではないが、誰もがお気に入りだ
原文は"Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
必ず
go fmtを
しましょう
小さいコピーは小さい依存より良い
原文は"A little copying is better than a little dependency."
ある程度Goを書いていると、再利用をするためにライブラリ化をしていくと思います。 しかし、再利用が目的になることは避けないといけません。 再利用するためにライブラリ化してそれを使うということは、そのライブラリに依存するということになります。 Rob Pikeは講演で、表示のためのライブラリが巨大なUnicodeのパッケージに依存してしまう例を上げていました。 その時はそれを避けるために必要な部分をコピーして、Unicodeのパッケージに依存することを避けた、と言っています。
普段開発をしていて、流石にUnicodeレベル大きさのパッケージに依存することはあまりないとは思いますが、 依存することでテストが重くなったりするのであれば、敢えてライブラリをimportせず、ソースコードをコピーすることも検討しないと いけないのかもしれません。
システムコールは常にビルドタグで保護されるべきだ
原文は"Syscall must always be guarded with build tags."
Go言語はクロスコンパイル言語です。そのため、1つのコードが複数の環境で動くように心がけないといけません。 Goだけで書かれたパッケージを使用している場合は特に意識することはないと思います。 しかし、特定のアーキテクチャや環境に依存するシステムコールを利用する場合は必ずビルドタグで保護しましょう。 ビルドタグで保護することで、コンパイル時にその環境では動かないことを示すことができます。
Goの公式のページにも書いてある通り、 以下のようにファイルの行頭にビルドタグをつけることで、そのファイルがビルドできる環境を指定することができます。 他にもファイル名で指定する方法もあるようです。
// +build linux darwin
CGoは常にビルドタグで保護されるべきだ
原文は"Cgo must always be guarded with build tags."
これもシステムコールの理由と同じですね。 Cgoも同様にビルドタグで制御できるので、CでコンパイルされたものをGoで使用するときには必ずビルドタグを保護しましょう。
CgoはGo言語ではない
原文は"Cgo is not Go."
(自分はcgoを扱ったことがないのでここはちょっと荷が重い……) cgoはGo言語からC言語を扱うための機能です。しかしここではcgoを使うのであれば、それは既にGoではないと言っています。 理由としては以下のような物があげられるようです(参考:cgo is not Go)。 * 遅いビルド * 複雑なビルド * クロスコンパイルではなくなる * パフォーマンスの問題 * 呼び出すCが別の何かに依存していることがある * 配布時の展開の複雑さ
なにか必然的な理由(参考先ではグラフィックドライバやウィンドウシステム)がない限り、なるべく使うべきでは ないのかもしれません。
安全ではないパッケージでは保証はない
原文は"With the unsafe package there are no guarantees."
このあたりは自明なことかもしれません。 外部パッケージを使用する時にはそれが本当に安全で使いやすいものかを確認しなければいけません。 もし安全でないものを使っていると、glide upする度にコードが動かなくなることもあります(実体験)。
見やすい実装は巧みな実装より良い
原文は"Clear is better than clever."
これもある程度自明かもしれません。 Go言語の設計思想として、「読みやすく」という物があります。 それを実現するのにより見やすい実装が書けるのなら、それを採用するべきであるし、それがGo言語の哲学にもなっています。
リフレクションは決して綺麗なものではない
原文は"Reflection is never clear."
リフレクションというのは、コードを動的に操作することです。 リフレクションを使うと、プライベートの値を外から直接弄ったりメソッドの実装を置き換えたりと割と闇なことができます。 ライブラリを作る側がある程度汎用的にするために使用することはあっても、普段から使うのは避けるべきでしょう。
エラーは値だ
原文は"Errors are values."
参考:Errors are values Go言語の特徴の1つにエラーを値として扱う、というものがあります。 「エラーハンドリングをifでやるんでしょ?」と思い以下のようなコードを量産しがちですが(自分も最近理解した)、本質は違います。
if err != nil { }
Goのエラーは「値として扱えるから、ロジックで処理ができる」というのが本質になります。
参考サイトからそのままソースコードを借りてきます。 例えば、Goを書いていると以下のようなエラーハンドリングはよく発生します。
_, err = fd.Write(p0[a:b]) if err != nil { return err } _, err = fd.Write(p1[c:d]) if err != nil { return err } _, err = fd.Write(p2[e:f]) if err != nil { return err } // and so on
おそらくGoを書いたことのある人は自分で書いたこともあるし、他人が書いたのを見たこともあると思います。 これでも確かに動作はしますが、明らかに冗長なコードではあります。 そこで、エラーは値であることを利用して簡単なロジックをプログラムを作成し、きれいにハンドリングしてみます。
まず、以下のような構造体とメソッドを用意します。
type errWriter struct { w io.Writer err error } func (ew *errWriter) write(buf []byte) { if ew.err != nil { return } _, ew.err = ew.w.Write(buf) }
errWriterはWriterを実装したものと、errorを持ちます。 writeメソッドは、byteのスライスを受け取りそれをWriter interfaceが持つWriteメソッドに書き込みます。 その時、値として発生するエラーを構造体の中に保持しておき、writeメソッドが使われる度にその保持したエラーを見て 処理するかどうかを決めています。
上記の構造体を使用することで、先程の冗長な処理を以下のように書き換えることができます。
ew := &errWriter{w: fd} ew.write(p0[a:b]) ew.write(p1[c:d]) ew.write(p2[e:f]) // and so on if ew.err != nil { return ew.err }
この手法が絶対的に正しいわけではないですが、ここで重要なのはエラーをプログラムの中で処理できるということです。 エラー処理をするときには脳直でif文を書くのではなく、値として処理できることも覚えておきましょう。
エラーをチェックするだけでなく、それらを正常に処理しなさい
原文は"Don't just check errors, handle them gracefully."
参考:Don’t just check errors, handle them gracefully
同じくGo言語のエラーの話です。
こちらも参考サイトのソースコードを借りてきます。
以下のようなコードがあったとします。
func AuthenticateRequest(r *Request) error { err := authenticate(r.User) if err != nil { return err } return nil }
これ自体は単純で、authenticateメソッドが返すエラーを上のメソッドに返しているだけです。 こちらもありがちなコードだと思うのですが、もしauthenticateでエラーが発生し、 上にメソッドを返した時に、どこでエラーが発生しているのかを判別する手段がありません(参考サイトを見た時に画像で笑った)。
つまり、ただエラーチェックをして、その値をただ上に渡しているだけだと、どこで何が発生したのかわかりにくくなってしまいます。 Goではfmtパッケージで文字列を整形した上でエラーを作成するメソッドが用意されています。 それを利用して、以下のように、どこでエラーが発生したのかを明確にして上に返してあげないといけません。
func AuthenticateRequest(r *Request) error { err := authenticate(r.User) if err != nil { return fmt.Errorf("authenticate failed: %v", err) } return nil }
デザインはアーキテクチャ、名前はコンポーネント、ドキュメントは詳細
原文は"Design the architecture, name the components, document the details."
これもタイトル通りだと思います(ちょっと抽象的だったので理解できていない)。 幼稚園児レベルのヒアリングとGoogleの字幕機能を駆使してなんとか元動画の英語を 理解しようとした結果、
構成要素の詳細を示すアーキテクチャをデザインし、それを構成するコンポーネントに名前をつける。 コンポーネントの名前が適切であれば明確かつ自然にプログラムが書けるが、 説明しなければならない詳細についてドキュメントを書きましょう。
という感じで理解しました。
解釈が間違っていたり、こういう意味だよというのお待ちしています。
ドキュメントは使う人達のために
原文は"Documentation is for users."
よくプログラムにはドキュメントを書け、という話がありますが、それが本当に意味のあるドキュメントかどうかを 意識しましょう、ということです。 その関数が引数に何を取り何を返すのかをただ書くのではなく、その関数がどのような意図で作られて、 どのような意図でその処理をするのかを書くべきだということです。
これは会社でプログラムを書くようになって強く実感するようになりました。 Goに限らず、という話ですかね。
panicさせるな
原文は"Don't panic."
Effective GoのErrosの項目を読みましょう、というお話。 Goにはエラーを発生させる方法として、panicという関数が存在します。 プログラムを継続的に実行することが難しくなった時に発生するエラーです。 エラーを値として扱うものとは大きく違い、その場でプログラムが停止してしまいます。 安全にエラーを返す方法があるのであれば、間違いなくそちらを使うべきです。