Effective Go 野良翻訳(1)
Effective Goが面白いので勉強のため翻訳してみる。意外に長いのでちょっとづつ。
はじめに
Go は新しい言語だ。Go は既存言語からアイディアを借用しているが、変わったところもあるので、実際のGoプログラムは、Goの親戚言語で書かれたプログラムと異なる特徴を持つことになる。C++やJavaで書かれたプログラムを、単にGoに書き直すだけではなかなか良いプログラムにならない。JavaプログラムはJavaで書かれているのであって、Goで書かれているわけではないからだ。逆に、Go的な観点から問題を考えると、効率的だが他の言語で書いたものとは全く異なるプログラムになる。言い換えれば、よいGoプログラムを書くにはGoの特徴と定型的な書き方をよく理解しておく必要があるということだ。さらに、Goで用いるプログラム上の慣習、たとえば名前の付け方やフォーマット、プログラムの構成を理解しておくことも重要だ。他のGoプログラマがあなたのプログラムを簡単に理解できるように
このドキュメントは、簡潔で定型的なGoコードを書くためのヒントを与える。まずは、言語仕様やチュートリアルを先に読んでほしい。このドキュメントはそれらを補うものだ。
フォーマット(スタイル)
プログラムのフォーマット(スタイル)の問題はよく論争になるが重要な話ではない。プログラマが異なるスタイルに適応することもできるが、その必要がなければそれに越したことはない。みんなが同じスタイルを守るならばこの問題に時間を割く必要がなくなる。問題は、長々としたスタイルガイドなしでどうやってそのユートピアにたどり着くかだ。
Goではちょっと変わった方法でこの問題にアプローチしている。計算機に解決させるのだ。gofmtという、Goプログラムを読んで、標準スタイルのインデントと垂直にそろえられたソースコードを出力するプログラムを使う。このプログラムは、コメントはそのままにするが、必要ならコメントも成形する。新しい書き方をどうしたらいいかわからないときにはgofmtにかけてみよう。出力結果がおかしいようだったら、gofmtを修正して欲しい(バグを登録してもいい)。適当に手でソースを直してはいけない。
例を見てみよう。構造体のフィールドに対するコメントを時間をかけてそろえる必要はない。gofmtがやってくれる。このようなソースをいれると、
type T struct { name string // name of the object value int // its value }
gofmt がそろえてくれる。
type T struct { name string // name of the object value int // its value }
ライブラリ内のソースは、すべてgofmtで成形されている。
フォーマットの詳細に関して少し書いておこう
インデント
gofmtは、デフォルトではタブをインデントに用いる。スペースは、どうしても使わなければ成らないときにしか使わない。
1行の文字数
Goには1行あたりの文字数の制限はない。パンチカードからはみ出す心配はいらない。長くなりすぎたと思ったら、改行してタブでインデントしよう
括弧
Goでは括弧を使う頻度は低い。if,for,switchなどの制御構造では、文法的に括弧を用いない。さらに、演算子の優先順位階層がコンパクトに簡潔になっている。だから、
x<<8 + y<<16
は、スペースで示した通りの意味になる。
コメント
Go はC式の /* */ で表されるブロックコメントと、C++式の // で示す行コメントの両方が使える。通常は行コメントを用いる。ブロックコメントは、パッケージコメントぐらいにしか使わない。大きなコードブロックをまとめて無効化する時にも有効だ。
プログラムでもありwebサーバでもあるgodocは、Goのソースファイルを処理してパッケージの内容に関するドキュメントを生成する。トップレベル宣言の直前にあるコメントは、余分な改行で分けられていない限り、宣言されているアイテムへの説明文として抽出される。これらのコメントをどう書くかによって、godocが生成するドキュメントの品質が決まる。
パッケージにはパッケージコメントを書いておくべきだ。package節の直前にブロックコメントとして記述する。複数ファイルから構成されるパッケージでは、パッケージコメントはどれか一つのファイルにだけ書いておけばいい。どのファイルでもかまわない。パッケージコメントは、そのパッケージ全体に関する情報を提供するべきだ。パッケージコメントは、godocページの先頭に出力され、後ろに来る詳細なドキュメントの導入となる。
/* The regexp package implements a simple library for regular expressions. The syntax of the regular expressions accepted is: regexp: concatenation { '|' concatenation } concatenation: { closure } closure: term [ '*' | '+' | '?' ] term: '^' '$' '.' character '[' [ '^' ] character-ranges ']' '(' regexp ')' */ package regexp
簡単なパッケージならパッケージコメントも簡潔でいい。
// The path package implements utility routines for // manipulating slash-separated filename paths.
コメントにはバナーやアスタリスクのような余計なフォーマットを入れなくてよい。生成されたドキュメントが、固定長フォントで表示されるかどうかもわからないのだから、スペースで列を整えるべきではない。そのあたりはgodocがgofmtと同様にうまくやってくれる。最後になったが、コメントはプレインテキストでかく。HTMLや_this_のようなアノテーションは、そのまま表示されてしまうので使うべきではない。
パッケージ内部では、トップレベルの宣言の直前のコメントがドキュメントとなる。プログラム中のエクスポートされる(つまり大文字で始まる)名前にはドキュメントコメントを書くべきだ。
ドキュメントコメントには完全な文章を書くようにしよう。そうすれば、様々な自動表示が可能になる。最初の一文は、説明の対象となる名前で始まる、1文のサマリになるようにしよう。
// Compile parses a regular expression and returns, if successful, a Regexp // object that can be used to match against text. func Compile(str string) (regexp *Regexp, error os.Error) {
Goの宣言の文法では、宣言をグルーピングすることができる。一つのドキュメントコメントで、複数の関連した変数の集合を説明することができる。このような場合、宣言はすべてドキュメントになるので、コメントはおざなりになりがちだ。
// Error codes returned by failures to parse an expression. var ( ErrInternal = os.NewError("internal error") ErrUnmatchedLpar = os.NewError("unmatched '('") ErrUnmatchedRpar = os.NewError("unmatched ')'") ... )
プライベートの名前であっても、グルーピングすることでアイテム間の関係を示すことができる。たとえば、一つのmutexで保護する変数の集合などはこう書くとよい。
var ( countLock sync.Mutex inputCount uint32 outputCount uint32 errorCount uint32 )
名前づけ
他の言語同様、Goでも名前づけは重要だ。名前に意味がある場合もある。例えば、ある名前がパッケージ外部から参照できるかどうかは、最初の文字が大文字かどうかによって定まる。だから、Goプログラムでの名前付けコンベンションに関してすこし時間を割く価値があるだろう。
パッケージ名
パッケージをインポートすると、パッケージ名は、パッケージの内容に対するアクセサになる。
import "bytes"
とするとそれ以降で bytes.Buffer を参照できるようになる。あるパッケージを使うすべてのプログラマが、パッケージの内容に対して同じ名前で参照することが出来れば、便利だ。そうするためには、パッケージ名がうまく設定されていなければならない。短く、簡潔で、 分かりやすい名前でなければならない。パッケージ名には習慣として小文字で、1単語となる名前を用いる。アンダースコアや、大文字を混ぜたりする必要はないはずだ。簡潔すぎるぐらいんほうがいい。パッケージを使うすべてのプログラマがそのパッケージ名をタイプするのだから。
名前の衝突の心配をする必要はない。パッケージ名は、インポートする際のデフォルト名にすぎないので、ソースコード全体でユニークになっている必要はない。あまりないことだが、名前が衝突したときには、インポートする側で別の名前にして使うこともできる。いずれにしろ、インポートする際のファイル名でどのパッケージを使うかが決まるので、紛らわしくなることはあまりない。
もうひとつのコンベンションは、パッケージ名が、ソースディレクトリのベース名となることだ。
src/pkg/container/vector ディレクトリに収められたパッケージは"container/vector" としてインポートされるが、名前は container_vector やcontainerVector などではなく、vectorとなる。
パッケージを利用する際には、パッケージ名を使ってパッケージの中身を参照する(インポート文のドットノーテーションは、テストなどの普通でない場合にしか使わない)。これを利用して、パッケージがエクスポートする名前がくどくなることを避けることができる。たとえば、bufioパッケージのリーダはBufReaderではなくReaderとする。ユーザは、bufio.Readerとして利用するので、こちらのほうが簡潔だからだ。さらに言えば、インポートされたエンティティは、常にパッケージ名を用いて参照されるので、bufio.Readerがio.Readerと衝突することはない。同様に、ring.Ring のインスタンスを生成する関数(Goではこのような関数をコンストラクタと呼ぶのだが)は、通常 NewRing という名前にするが、このパッケージがエクスポートする型が Ring だけで、さらにパッケージ名がring なのであれば、Newとだけすればよい。パッケージのユーザは、ring.Newとして利用する。パッケージの構造を利用して名前を分かりやすくしよう。
別の例として once.Do を挙げておこう。once.Do(setup) は十分に分かりやすく、once.DoOrWaitUntilDone(setup) という名前にしてもよりわかりやすくはならない。長い名前にすると必ず読みやすくなるわけではないのだ。ある名前が複雑で紛らわしい物を指している場合に、名前にすべての情報を押しこむよりも、ドキュメント・コメントを書いたほうが、大抵の場合はいい結果を生む。
インターフェイス名
慣例として、ひとつしかメソッドのないインターフェイスは、そのメソッド名に'er'をつけたものとする。Reader, Writer, Formatter、などなど。
このようなインターフェイスはたくさんある。これらのインターフェイス名、関数名を尊重したほうが生産的だ。Read, Write, Close, Flush, String などには標準的なシグネチャと意味がある。紛らわしくならないように、これらの名前をもちいるのは同じシグネチャと意味の場合だけにしよう。逆に、よく知られたタイプのメソッドと同じ意味を持つメソッドを定義する際には、それと同じ名前とシグネチャを使うようにしよう。例えば、文字列に変換するメソッドはToStringではなくStringにしよう。
大文字小文字混在
最後に、Goでは複数の単語からなる名前を記述する際には、アンダースコアでつなぐのではなく、MixedCapsやmixedCapsのように大文字を使う。
セミコロン
Goの形式的な文法では、Cと同様に文はセミコロンで終了する。Cと違うのはソースコードにはセミコロンが現れないことだ。Goでは文法解析器が、プログラムをスキャンするときに自動的にセミコロンを挿入するので、入力するプログラムにはセミコロンをほとんど書かなくてすむのだ。
規則は次のように決まっている。改行の直前のトークンが識別子(int や float64などの単語もふくむ)、または基本的なリテラル(数値や文字列定数など)、または、次のトークンのうちの一つ
である場合に、文法解析器はそのトークンの前にセミコロンを常に挿入する。
break continue fallthrough return ++ -- ) }
要するに、「文を終わらせることのできるトークンの直後に改行が来た場合には、セミコロンを挿入する」わけだ。
閉じ中括弧の直前のセミコロンも省略できる。次のような場合はセミコロンはいらない。
go func() { for { dst <- <-src } }()
慣例的なGoプログラムでは、セミコロンはごく限られた場所にしか登場しない。ループ節や、イニシャライザの区切り、条件式、continuation elements などだ。複数の文を一行に書くような場合にも、そういうふうに書きたいならばだが、セミコロンが必要になる。
ひとつ注意しておこう。制御構文(if, for, switch, select)の開き中括弧を次の行に書いてはいけない。そうすると開き中括弧の前に自動的にセミコロンが挿入されることになり、おかしなことになる。次のように書くのがただしい。
if i < f() { g() }
こちらは間違い。
if i < f() // wrong! { // wrong! g() }
制御構文
Goの制御構文はCのそれと似ているが、いくつかの重要な点で異なる。doループ、whileループはなく、わずかに汎用化されたforループがあるだけだ。switch文はよりフレキシブルになり、if文とswitch文では、for文のような変数初期化がオプショナルにかけるようになっている。さらに、型switchや複数の通信をマルチプレクスするためのselectといったあらたな制御構文が追加されている。文法も微妙に変更されている。括弧が不要になり、ボディ部は必ず中括弧でくくらなければならない。
If
Goにおける単純なif文はこのようになる。
if x > 0 { return y }
中括弧が必須になっているのは、単純なif文であっても複数行に分けて書く事を奨励するためだ。いずれにしろ、特にボディ部にreturnやbreakなどの制御文が含まれている際には、複数行にわけて書いたほうがいい。
if文とswitch文では初期化文が書けるようになった。次のようにローカル変数を定義するのはよくある書き方だ。
if err := file.Chmod(0664); err != nil { log.Print(err) return err }
Goのライブラリのソースを見てみると、if文が次の文に流れない場合、つまりボディ部が、break やcontinue, goto, returnで終わっている場合に、elseを省略しているのがわかる。
f, err := os.Open(name, os.O_RDONLY, 0) if err != nil { return err } codeUsing(f)
これは一連のエラーチェックを行う場合によく見られる。成功時の制御フローは、エラーケースを排除しながらページの下の方に流れている。エラーの場合には、return 文で終わることが多いので、
else文のないコードになる。
f, err := os.Open(name, os.O_RDONLY, 0) if err != nil { return err } d, err := f.Stat() if err != nil { return err } codeUsing(f, d)
For
GoのforループはCのものに似ているが同じではない。whileを合わせたものになっており、do-while文はない。for文には3つの形があり、そのうちの一つでだけセミコロンを用いる。
// Like a C for for init; condition; post { } // Like a C while for condition { } // Like a C for(;;) for { }
宣言を短くかけるのでインデックス変数をループで宣言するのが簡単になっている。
sum := 0 for i := 0; i < 10; i++ { sum += i }
配列、スライス、文字列やマップ上をループで回したりチャンネルから読み出す場合には、range節でループを制御する。
var m map[string]int sum := 0 for _, value := range m { // key is unused sum += value }
文字列では、range文はもっと仕事をしてくれる。UTF-8をパーズしてUnicode文字を切りだしてくれるのだ。エンコーディングがおかしい場合には、1バイトを消費して謎のU+FFFDを生成する。
for pos, char := range "日本語" { fmt.Printf("character %c starts at byte position %d\n", char, pos) }
このプログラムは次のような出力を行う。
character 日 starts at byte position 0 character 本 starts at byte position 3 character 語 starts at byte position 6
最後に、Goにはカンマ演算子がなく、++や--は式ではなく文になるので、複数の変数に対してループを回したい場合には、並列代入を使用する。
// Reverse a for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] }
Switch
Goのswitch文は、Cのそれよりも汎用になっている。式は定数でなくてもよいし、さらに整数である必要もない。caseは上から下に、マッチするものが見つかるまで評価される。switch 文の分岐対象となる式が書かれていない場合にはtrueに対して分岐する。このため、if-else-if-elseをswitchで書く事ができる(そうすることが慣用的でもある)。
func unhex(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'a' <= c && c <= 'f': return c - 'a' + 10 case 'A' <= c && c <= 'F': return c - 'A' + 10 } return 0 }
自動的に次のcaseに落ちるフォールスルーはない。そのかわりにcaseに複数の条件をコンマで区切って書く事ができる。
func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false }
2つのスイッチ文を用いてバイト配列を比較するルーチンを示す。
// Compare returns an integer comparing the two byte arrays // lexicographically. // The result will be 0 if a == b, -1 if a < b, and +1 if a > b func Compare(a, b []byte) int { for i := 0; i < len(a) && i < len(b); i++ { switch { case a[i] > b[i]: return 1 case a[i] < b[i]: return -1 } } switch { case len(a) < len(b): return -1 case len(a) > len(b): return 1 } return 0 }
インターフェイス変数の動的型に対してswitchすることもできる。この型スイッチでは、型アサーションの括弧の中にキーワード 'type' を指定する構文を用いる。
型スイッチで変数を宣言すると、各クローズではそれぞれに対応した型の変数として利用できる。
switch t := interfaceValue.(type) { default: fmt.Printf("unexpected type %T", t) // %T prints type case bool: fmt.Printf("boolean %t\n", t) case int: fmt.Printf("integer %d\n", t) case *bool: fmt.Printf("pointer to boolean %t\n", *t) case *int: fmt.Printf("pointer to integer %d\n", *t) }