このドキュメントは、http://golang.org/doc/effective_go.htmlの翻訳です。
はじめに
Goは新しい言語です。既存の言語からアイデアを取り入れてはいますが、他の言語にはない機能をもっているため、実際に記述されたGoのプログラムは、他の類似した言語とはだいぶ異なるものになります。C++またはJavaプログラムをGo言語へ直接変換しても、あまりうまくは行きません。JavaのプログラムはあくまでJavaで書かれており、Go言語で書かれてはいないからです。一方で、Go側の視点からこの問題を考えると、変換に成功したとしても、全く違うプログラムができてしまうことになります。言い換えると、Go言語を使いこなすには、Go言語の機能や文法を理解することが重要です。おなじく、Go言語のプログラミングにおける慣例を知っておくことも重要です。たとえば名前の付け方、書式化、プログラムの構築などです。慣例に従って記述されたプログラムは他のGo言語プログラマからも理解しやすいプログラムとなります。
この文書は、分かりやすく慣用的なGo言語のコードを書く際のヒントを与えます。また、この文章は言語仕様とチュートリアルの補足であるため、それらを先に読んでおいてください。
サンプルプログラム
Go言語付属のパッケージソースは、コアライブラリを提供するだけではなく、Go言語のサンプルプログラムとしての役割もあります。問題への糸口や実装方法を探しているときに、このサンプルプログラムがその答えやアイデア、知識を提供してくれるでしょう。
ソースコードの書式
ソースコードの書式に関する問題点は多くの論議を呼んでいるのに関わらず、あまり重要視されておりません。人々は異なる書式に順応することができますが、それはしないに越したことはありませんし、全員が同じ書式を使うのであれば、このトピックに時間を割く必要もありません。問題点は、長文のドキュメントを読ませることなくこの理想に近づいていく方法です。
Go言語では、我々はちょっと変わったアプローチを取り、コンピュータにフォーマット問題の大部分を任せました。gofmtプログラムは、Go言語のプログラムを読み込むと、インデントと垂直位置を標準スタイルに変更し、必要であればコメントの内容を保ったまま再書式化した上でソースコードを出力します。見慣れない構文をどう扱えばよいか分からなければgofmtを実行してください。それが返した答えが合っていないようであれば、そのままにせずにプログラムを修正(もしくはバグ報告)してください。
例です。構造体のフィールドのコメントを一列に並べることに時間を費やす必要はありません。代わりにgofmtがそれを行ってくれます。はじめに、宣言を行います。
type T struct {
name string // オブジェクトの名前
value int // その値
}
gofmtによってカラムが整列されます。
type T struct {
name string // オブジェクトの名前
value int // その値
}
ライブラリ内のすべてのコードは、gofmtで書式が揃えられています。
若干、フォーマットの詳細についての説明が残っていますので、簡潔に説明します。
インデント
インデントにはタブを使います。これはgofmtのデフォルトです。スペースは必要がない限り使わないでください。
行の長さ
Go言語には、行の長さ制限は有りません。パンチカードからはみ出す心配は無用です。行があまりにも長くなったときは、改行してタブでインデントしてください。
括弧
Go言語はあまり括弧を必要とはせず、制御機構(if, for, switch)の構文には括弧は不要となっています。また、演算子の優先順位は簡潔かつ明確です。たとえば、
x<<8 + y<<16
この式は、見たとおりの値を表します。
コメント
Go言語では、C形式の/* */ブロックコメント、およびC++形式の//行コメントが使用できます。基本は行コメントですが、ブロックコメントはパッケージのコメントとして使われる他、コードをブロック単位で無効化するのに役立ちます。
godocプログラム(ウェブサーバ機能もある)は、Go言語のソースファイルから、そのパッケージ内容に関したドキュメントを抽出します。トップレベルの宣言の直前に、空行を挟まずに記述されているコメントは、その宣言に対する説明として宣言とともに抽出されます。これらコメントのありようがgodocによって生成されるドキュメントの品質を左右します。
すべてのパッケージにはパッケージコメントが必要です。これはパッケージ文の前に置かれたブロックコメントです。パッケージが複数のファイルに分かれているときは、どれかひとつにパッケージコメントを記述しておけば、それが使われます。パッケージコメントはパッケージの説明、およびパッケージ全体の情報を提供するものであるべきです。パッケージコメントは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
シンプルなパッケージであれば、パッケージコメントも簡潔で構いません。
// pathパッケージにはスラッシュ切りのファイル名パスを // 扱うユーティリティルーチンが実装されています。
コメントに、アスタリスクを並べるような特殊な書式は不要です。生成された出力は、等幅フォントで表示されるかどうか分からないので、gofmtのようにスペースによる配置に頼らないよう気をつけてください。最後になりますが、コメントは何ら解釈されることはないプレーンテキストであるため、HTMLや、_this_のようなアノテーションはそのままの形で出力されるので用いないほうがよいでしょう。
パッケージ内の、トップレベルの宣言の直前にあるコメントはすべて、その宣言に対するドキュメントコメントとして扱われます。また、プログラム内でエクスポート(大文字で記述)されている識別子にはすべて、ドキュメントコメントが必要です。
ドキュメントコメントは様々な自動フォーマットが適用されることを考慮すると英語の文章が望ましいです。また先頭の一文は、宣言した識別子で始まる文章で、かつ概要を記述したものでなければなりません。
// Compileは、正規表現を解析し、成功したときに返されるRegexpオブジェクトは、
// テキストとマッチさせることができます。
func Compile(str string) (regexp *Regexp, error os.Error) {
Go言語では宣言文をグループ化することができます。このときは一連の定数または変数に対して、ひとつのドキュメントコメントで記述することができます。このようなコメントは宣言全体に対して行われるので、たいていは形式的なものとなってしまいます。
// 式の解析に失敗した時に返されるエラーコード
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にアクセスすることができるようになります。
パッケージの内容を参照する際に、利用者すべてが同一の名前を使ってアクセスできるのであれば、それは有益なことです。それは即ち、パッケージ名が短く簡潔であり、すぐに連想できるような良い名前でなければならないということです。
慣例では、パッケージ名は小文字でひとつの単語です。アンダースコアや大文字が混ざって(mixedCaps)はいけません。
パッケージ使用者がその名前をタイプすることを考慮して、簡潔すぎるぐらいにしてください。名前が重複することを気にする必要はありません。パッケージ名は、単にインポート時にデフォルトとして使われる名前であり、ソースコード全体でユニークである必要はありません。万が一、パッケージのインポート時に重複が起きたときは、別なローカルな名前をつけることができます。いずれにせよ、インポートのファイル名によってどのパッケージが使われるかが決まるので、区別がつかなくなることはまずありません。
もう一つの慣例は、パッケージ名がそのソースディレクトリのベース名であるということです。たとえばsrc/pkg/container/vectorに置かれているパッケージは、"container/vector"としてインポートし、名前はvectorとなります。container_vectorやcontainerVectorとはなりません。
パッケージを利用する側は、そのパッケージの内容へアクセスするためにパッケージ名を使用します。(import .表記を使うとアクセス時にパッケージ名が不要となりますが、これはテストや他の一般的でない状況に使うためのものなので、ここでは無視します。)パッケージ名を使うので、エクスポートされる名前は、同じ名称が繰り返されることを避けるためにパッケージ名を利用することがあります。たとえば、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です)
MixedCaps
最後になりますが、Go言語の慣例では、複数の単語から成る名前をつけるときはアンダースコアを使わずに、MixedCapsまたはmixedCapsのように単語の先頭だけ大文字を用います。
セミコロン
Go言語の文法上、ステートメントの終了にはC言語同様にセミコロンが使われますが、C言語とは異なるのは、それらセミコロンはソース上に現れないことです。その代わりに、lexer(字句解析プログラム)がソースを調べて、ある単純なルールに基づき自動的にセミコロンを付け加えるので、打ち込むコードにはセミコロンがほとんど不要です。
ルールはこうです。改行直前のトークンが識別子(intやfloat64といった単語を含む)、または文字列や数値定数といった基本的なリテラル、または下のトークンのどれかであるときに、lexerはトークンの後ろにセミコロンを付け加えます。
break continue fallthrough return ++ -- ) }
このルールは次のようにまとめられます。「改行文字の直前に、ステートメントの終わりと成りえるトークンがあるときにセミコロンが挿入される。」
また、右波括弧”}”の直前にあるセミコロンも省略可能です。次のようなステートメントではセミコロンはいりません。
go func() { for { dst <- <-src } }()
Go言語の慣用記法に沿ったプログラムにおいては、セミコロンが記述されるのはforループ節のイニシャライザ、条件、繰り返しの各要素を分割するようなときだけです。ただし一行を複数のステートメントに分割するにはセミコロンが必要なので、このようなコードを記述するときはセミコロンを挿入してください。
注意事項:制御構造(if、for、switch、select)の左波括弧”{“を、その次の行に絶対に置いてはいけません。もし置いてしまうと、括弧の前にセミコロンが挿入され、それによって予期しない影響が起きる可能性があります。よって次のように記述してください。
if i < f() {
g()
}
下のように書いてはいけません。
if i < f() // 不正
{ // 不正
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.Stderr(err)
return err
}
Go言語のライブラリ内で良く見られる書き方ですが、ifステートメントから次のステートメントへ制御が移らないとき(すなわち、break、continue、goto、returnのいずれかでifの本体から抜けるとき)は、不要な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言語のforと似てはいますが、同じではありません。Go言語のforループは、C言語のforとwhileループを兼ねていますが、do-whileループに相当するものはありません。forループには3つの形式がありますが、セミコロンを使うのはそのうちひとつだけです。
// Cのforに相当する形式
for init; condition; post { }
// Cのwhileに相当する形式
for condition { }
// 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 { // キーは使われません
sum += value
}
文字列を扱うときのrangeはより高機能で、UTF-8エンコードでUnicodeの各文字を取り出します。このとき不正なエンコーディングあると、1バイトスキップした上で置換ルーン(Unicode replacement character 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言語にはカンマ演算子がなく、また++と--は式ではなくステートメントです。forで複数の変数を回したいときは、下のように同時代入を使わなければなりません。
// 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言語より多機能です。switchの式は、定数や整数である必要はありません。一致するものが見つかるまで、caseを上から下まで評価していきます。switchが式を伴わないときは、式の値がtrueとなるcaseにマッチします。これを利用してswitchを使ってif-else-if-elseチェーンを書くことができます。これは慣用的な書き方です。
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へと処理が自動的に移ることはありませんが、caseにはカンマで区切ったリストを指定できます。
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
下は、2つのswitchステートメントを使ったバイト配列の比較ルーチンです。
// Compare は2つのバイト配列を辞書的に比較して整数を返します。
// 結果は、a == bのとき0、a < bのとき-1、a > bのとき+1
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は、インタフェース変数の動的な型を見つけるために用いることもできます。その型switchには、型アサーションの構文を使って丸括弧()の中にキーワード"type"と書きます。switchの式で変数を宣言したとき、その変数は各case節において適切な型となります。
switch t := interfaceValue.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T は型を出力する
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)
}
関数
複数の戻り値
Go言語の目新しい特徴のひとつは、関数およびメソッドが複数の値を返せることです。これは、C言語のプログラムで扱いにくかった、受信エラー(EOFを表す-1など)や引数の値の変更などを改善します。
C言語で書き込みエラーが起きたときは、マイナス値を返すことにより通知され、共用の変数にエラーコードが格納されます。Go言語ではWriteは書き込みデータ数とエラーを別々に返すことができます。つまり次のような情報を得ることができます。「何バイトか書き込めましたが、デバイスが一杯になったので一部書き込めませんでした。」
osパッケージの*File.Writeのシグネチャは次のように定義されています。
func (file *File) Write(b []byte) (n int, err Error)
シグネチャが示すとおりに、このメソッドは書き込んだバイト数を返すとともに、n != len(b)のとき非nilのErrorを返します。これは一般的な書き方です。その他の例は、エラーハンドリングに関するセクションを参照ください。
同様のアプローチにより、戻り値に参照パラメータを模してポインタを返す必要がなくなります。次の関数は、バイト配列の指定位置から数値を取り出し、その値と次の位置を返す単純な関数です。
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {
}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x*10 + int(b[i])-'0'
}
return x, i
}
この関数は次のようにして、入力配列から数値を取り出すために利用できます。
for i := 0; i < len(a); {
x, i = nextInt(a, i)
fmt.Println(x)
}
名前付き結果パラメータ
Go言語の、戻り/結果「パラメータ」には、名前をつけることができ、引数パラメータのように通常の変数として扱うことができます。名前が付けられていると、関数が呼び出されたときにその型のゼロ値で初期化されます。引数を持たないreturnステートメントを実行したときは、その時点で結果パラメータに格納されている値が、戻り値として使われます。
名前は必ずしも必要ではありませんが、名前をつけることでコードをより簡潔にすることができます。これは資料としても役立ちます。前出のnextInt関数の結果パラメータに名前を付けると、返されたint値がそれぞれ何を示しているか明確になります。
func nextInt(b []byte, pos int) (value, nextPos int) {
名前付きの結果パラメータは、初期化が行われた上で、パラメータなしのreturnと結びつけられるので、明確になるだけでなくシンプルになります。下は、この仕組みをうまく使ったio.ReadFullの例です。
func ReadFull(r Reader, buf []byte) (n int, err os.Error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:len(buf)]
}
return
}
Defer
Go言語のdeferステートメントは、deferを実行した関数がリターンする直前に、指定した関数の呼び出しが行われるようにスケジューリングします。これは、一般的な方式ではありませんが、関数内のどの経路を通ってリターンするかに関わらず、開放しなければならないリソースなどを処理するのに効果的な方式です。よくある例としては、ミューテックスのアンロックや、ファイルのクローズに使います。
// Contentsは、ファイルの内容を文字列として返します。
func Contents(filename string) (string, os.Error) {
f, err := os.Open(filename, os.O_RDONLY, 0)
if err != nil {
return "", err
}
defer f.Close() // f.Closeは、完了時に実行される
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = bytes.Add(result, buf[0:n])
if err != nil {
if err == os.EOF {
break
}
return "", err // ここでリターンしたときに、fはクローズされる
}
}
return string(result), nil // ここでリターンしたときに、fはクローズされる
}
関数の実行を遅らせることには、次の2つの利点があります。最初に、ファイルの閉じ忘れがないことを保証します。後から関数を修正し、新しいリターン経路を付け加えたときに起こしやすいミスに対処できます。第2に、closeがopenの近くに記述されるので、closeを関数の最後に記述するより、コードが見やすくなります。
遅延指定された関数の引数(関数がメソッドであれば、レシーバも含む)は、関数の実行時ではなく、deferを実行したときに評価されます。それ以外にも、関数の実行によって変数の値が変更されることを気にする必要がなくなります。これは、一か所のdefer呼び出しで、複数の関数の実行を遅延させられることを意味します。下は、少々馬鹿げた例です。
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
遅延指定された関数は、LIFO順に実行されるので、このコードでは、関数からリターンするときに、4 3 2 1 0と出力します。もう少し実用的な例は、プログラムの関数の実行を簡単にトレースする方法です。次のように、シンプルなトレースルーチンを書くことができます。
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
// これらの関数を、次のように使います。
func a() {
trace("a")
defer untrace("a")
// 何か処理する....
}
遅延指定された関数の引数が、defer実行時に評価されることを利用した、もっと良い手があります。アントレースルーチンへの引数の中で、トレースルーチンがセットアップ可能です。次は、これを利用した例です。
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
次のように出力されます。
entering: b in b entering: a in a leaving: a leaving: b
他の言語のブロックレベルのリソース管理に慣れたプログラマには、deferは、特異に見えるかも知れませんが、deferのもっとも面白く、強力な活用法は、deferがブロックベースではなく、関数ベースであることによって生み出されます。panicとrecoverのセクションに、サンプルがあります。
データ
new()による割り当て
Go言語の基本的なメモリ割り当てには、new()とmake()の2つがあります。これら2つはそれぞれ異なる働きをし、適用先の型も別となります。混乱させてしまうかもしれませんがルールは単純です。
まずnew()について説明します。これは組み込み関数であり、他の言語におけるnew()と基本的に同じです。new(T)は、型Tの新しいアイテム用にゼロ化した領域を割り当て、そのアドレスである*T型の値を返します。Go言語風に言い換えると、new(T)が返す値は、新しく割り当てられた型Tのゼロ値のポインタです。
new()から返されるメモリはゼロ化されています。ゼロ化済みオブジェクトは、さらなる初期化を行わなくても使用できるため、こういったオブジェクトの準備にnew()は便利です。すなわち、データ構造体の利用者がnew()でそれを作成すると、すぐに使える状態となります。たとえば、bytes.Bufferのドキュメントには、「Bufferのゼロ値は、利用準備が整った空のバッファである。」と記述されています。同様にsync.Mutexには明示的なコンストラクタやInitメソッドは用意されていませんが、その代わりにsync.Mutexのゼロ値は、非ロック状態のミューテックスであることが定められています。
この便利なゼロ値は連鎖的に働きます。下の型宣言をみてください。
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
このSyncedBuffer型の値もまた、割り当てや宣言を行うと同時に使用準備が整います。下のコードの変数pとvは、このままで正しく機能します。
p := new(SyncedBuffer) // *SyncedBuffer型 var v SyncedBuffer // SyncedBuffer型
コンストラクタと複合リテラル
下の例はosパッケージからの抜粋です。この例のようにゼロ値では充分でなく、コンストラクタによる初期化が必要となることがあります。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
上のコードには冗長な部分が多く見られます。複合リテラルとは、実行する度に新しいインスタンスを生成する式であり、これを使うことで下のコードのように単純化することができます。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
このようにローカル変数のアドレスを返しても問題ありません。関数から戻ったあとも、変数に割り当てたメモリは生き残ります。複合リテラルのアドレスを取得したときは、実行する度に新しいインスタンスが割り当てられる仕様なので、最後の2行を次のようにまとめることができます。
return &File{fd, name, nil, 0}
複合リテラルでは、すべてのフィールドを順番通りに記述しなければなりません。ただし明示的に「フィールド:値」の組み合わせで要素にラベルをつけたときは、イニシャライザは順序通りである必要はなく、また指定しなかった要素には、その型のゼロ値がセットされます。すなわち次のように書き換えられます。
return &File{fd: fd, name: name}
特殊なケースとして、複合リテラルがひとつもフィールドを含まないときは、その型のゼロ値が作られます。すなわち、式new(File)と&File{}は等価です。
また複合リテラルでは、フィールドのラベルをインデックスまたはマップのキーとみなして、配列、スライス、マップを作成することもできます。次の例において、Enone、Eio、Einvalがそれぞれ個別の変数でありさえすれば、その値に関わらず初期化は成功します。
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
make()による割り当て
割り当てに話を戻します。組み込み関数make(T, args)は、new(T)とは使用目的が異なります。makeで割り当てできるのはスライス、マップ、チャネルだけであり、初期化された、すなわちゼロ値でないT型(*Tでない)の値を返します。makeとnewを使い分ける理由は、これらの3つの型が隠蔽されたデータ構造への参照であり、このデータ構造が使用前に初期化されている必要があるためです。スライスを例にとると、スライスはデータ(配列内)へのポインタ、長さ、キャパシティという3つの情報から構成されており、それらの情報が初期化されるまではスライスの値はnilです。makeはスライス、マップ、チャネルの内部データ構造を初期化し、使用可能となるよう値を準備します。下は、makeの例です。
make([]int, 10, 100)
この例では、100個のintを持つ配列を割り当てたあと、その配列の先頭から10個目までの要素を示す、長さが10でキャパシティ100のスライス構造を作成します。(スライス作成時、キャパシティは省略可能です。詳細はスライスに関するセクションを参照ください。)これに対して、new([]int)は新しくメモリを割り当て、ゼロ化したスライス構造のポインタを返します。つまりこれはnilスライス値へのポインタです。
下は、new()とmake()の違いを例示したものです。
var p *[]int = new([]int) // スライス構造の割り当て(*p == nil)。あまり使わない。 var v []int = make([]int, 100) // スライスvは100個のintを持つ配列への参照 // 必要以上に複雑な書き方 var p *[]int = new([]int) *p = make([]int, 100, 100) // 一般的な書き方 v := make([]int, 100)
覚えておいていただきたいことは、make()が適用可能なのはマップ、スライス、チャネルだけであり、返されるのはポインタではないことです。ポインタが必要であればnew()で割り当ててください。
配列
配列はメモリ配置を厳密に指定したいときや、ときにはメモリ割り当てを回避したいときに役立ちますが、配列の主な役割は、次セクションの主題であるスライスから参照されることです。そのスライスについて説明する前に基礎知識として、配列について2、3説明しておきます。
Go言語とC言語では配列の動作に大きな違いがあります。Go言語では次のように振舞います。
- 配列は値です。ある配列を他へ代入するとすべての要素がコピーされます。
- すなわち関数に配列を渡すと、関数側ではポインタではなく、その配列のコピーを受け取ります。
- 配列のサイズは型の一部です。
[10]intと[20]intは異なる型です。
値として扱うと有用なこともありますが、高コストでもあるため、C言語のような動作と効率が必要であれば、配列へのポインタを関数に渡すことも可能です。
func Sum(a *[3]float) (sum float) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float{7.0, 8.5, 9.1}
x := Sum(&array) // 注:明示的にアドレス演算子を使用
しかし、これはGo言語では一般的ではなく、通常はスライスを使用します。
スライス
スライスは配列をラップして、データ列に対しより普遍的かつ効果的で使い易いインタフェースを提供します。変換行列のような明らかに多次元のデータ構造を扱う場合を除いて、配列を扱うGo言語のプログラムでは、配列そのままではなくスライスが多用されます。
スライスは参照型です。すなわちスライスを別のスライスに代入したときは、双方が同じ配列を参照します。たとえば関数がスライスを引数として取るとき、関数内でスライスの要素に変更を加えると、ポインタ渡しのように関数の呼び元からも変更内容が参照できます。ゆえにRead関数では、引数としてポインタと個数を受け取る代わりに、スライスを受け取ることが可能となります。このときスライスが持つ長さ情報は、読み込むべきデータの上限となります。下は、osパッケージのFile型のReadメソッドのシグネチャです。
func (file *File) Read(buf []byte) (n int, err os.Error)
このメソッドは読み込んだバイト数と、エラーがあればエラー値を返します。ある大きなバッファbufから先頭32バイトを読み込むためには、次のようにバッファをスライスしてください。※この「スライス」という言葉は名詞ではなく動詞です。
n, err := f.Read(buf[0:32])
こういったスライスの使い方は一般的かつ効率的です。実際のところ、あまり効率を考慮に入れなければ、下のコードでも同様にバッファの先頭32バイトを読み込めます。
var n int
var err os.Error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // 1バイト読み込み
if nbytes == 0 || e != nil {
err = e
break
}
n += nbytes
}
スライスが持つ長さ情報は、その参照先配列の範囲内であれば変更することができます。(試しにスライスを同じスライスに代入し直してみてください。) スライスのキャパシティは組み込み関数のcapで参照できます。キャパシティとはスライスが扱える長さの上限値です。
下はスライスにデータを追加する関数です。この関数ではデータの長さがキャパシティを超えるときは、スライスは再割り当てされ、結果得られたスライスが関数から返されます。nilスライスが関数に与えられたときには、仕様上nilスライスがlen、capともに0を返すことを利用しています。
func Append(slice, data[]byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // 再割り当て
// 再利用を考慮し、必要なサイズの倍、割り付ける
newSlice := make([]byte, (l+len(data))*2)
// copy関数は、事前宣言済みであり、どのスライス型にも使用できます
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
for i, c := range data {
slice[l+i] = c
}
return slice
}
Appendではsliceの要素を変更していますが、スライス自体(ランタイムデータ構造がポインタ、長さ、キャパシティを保持している)は値渡しされているため、処理を終えたあと関数からスライスを返す必要があります。
マップ
マップは、異なる型の値を結びつける便利で強力な組み込みデータ構造です。マップのキーには、イコール演算子が定められていればどんな型(例えば整数、浮動小数、文字列、ポインタ、動的な型がイコールをサポートするのであればインタフェースさえ)でも使えます。ですが構造体、配列、スライスにはイコールが定義されていないため、マップのキーとして使用することはできません。スライスと同じくマップもまた参照型であるため、マップを関数に渡し、その関数内でマップの内容に変更を加えると、変更内容は呼び出し元からも参照可能です。
複合リテラル構文を使い、キーと値をコロン区切りで指定することでマップを作成できるので、作成と同時に初期化が簡単に行えます。
var timeZone = map[string] int {
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
マップへの値の設定と取得は、配列の操作と構文的に似通っていますが、インデックスが整数である必要がない点で異なります。
offset := timeZone["EST"]
マップ内に存在しないキーを使って、マップから値を取得しようとするとマップのエントリの型のゼロ値が返されます。たとえばマップが整数のとき、存在しないキーを指定すると0が返ります。
キーが存在しないことと、ゼロ値とを区別したいこともあります。 "UTC"エントリが存在しないのかゼロ値なのかは、返されたゼロ値からは判断つきません。このようなときは複数代入形式で判断することができます。
var seconds int var ok bool seconds, ok = timeZone[tz]
これは見たままですが、「カンマok」慣用句と呼ばれています。この例では、tzが存在すれば、secondsに適切な値がセットされ、okの値は真となります。存在しなければ、secondsにはゼロがセットされ、okの値は偽となります。下の関数はこれをうまく利用してエラーを出力しています。
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Stderr("unknown time zone", tz)
return 0
}
マップ内に値が存在するか調べたいとき、値自体の取得が不要であればブランク識別子(ただのアンダーライン(_))を使うことができます。ブランク識別子を使うとどんな型の値でも代入または宣言することができ、値を他に影響およぼすことなく破棄することができます。マップ内の存在チェックだけをしたいときは、次のように本来は値が返される変数の代わりにブランク識別子を指定してください。
_, present := timeZone[tz]
マップから登録を削除するには、代入方向を変えて複数代入式の右側に論理値を指定してください。この論理値の値が偽のとき登録が削除されます。このときマップ内にキーが存在しなくても問題ありません。
timeZone["PDT"] = 0, false // 標準時に
出力
Go言語におけるフォーマット出力は、C言語のprintf群のスタイルと類似していますが、より高機能・多機能です。これら関数はfmtパッケージ内で定義されています。先頭一文字は大文字となっており、fmt.Printf、fmt.Fprintf、fmt.Sprintfなどが用意されています。文字列関数(Sprintfなど)は、指定したバッファに文字列を格納するのではなく、新しい文字列を返します。
フォーマット文字列の指定は必要ではありません。各Printf、Fprintf、Sprintfにはそれぞれ他にペアとなる関数、例えばPrintとPrintlnが用意されています。これらの関数はフォーマット文字列をとらず、代わりに引数ごとにデフォルトフォーマットを生成します。ln版では、引数の間に両引数とも文字列でないときだけ空白を挿入し、出力の後ろに改行を付加します。[訳注:実際試したところ、非ln版のときは引数がともに文字列でないときだけ引数間に空白が挿入されるのに対し、ln版のときは常に空白が挿入されるようです] 下の例では、各行はいずれも同じ内容を出力します。
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println(fmt.Sprint("Hello ", 23))
チュートリアルでも解説したように、fmt.Fprintとそれに関連する関数は、一番目の引数にio.Writerインタフェースを実装していればどんなオブジェクトでも指定可能です。 よく使われるのは、お馴染みの変数os.Stdouとos.Stderrです。
ここからC言語との違いが現れます。まず、%dのような数値フォーマットでは、フラグによる符号やサイズの指定は行いません。その代わりに出力ルーチンは、引数の型を参照して値の属性を決定します。
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
これの出力結果です。
18446744073709551615 ffffffffffffffff; -1 -1
例えば整数値を10進表記で出力するようなデフォルトの変換でよければ、どんなケースにも対応可能なフォーマット%vを使うことができます。このとき出力される内容は、PrintとPrintlnの出力結果と全く同じです。さらにこのフォーマットは、配列、構造体、マップなどどんな値でも出力することができます。下は、前のセクションで定義したタイムゾーンマップを出力するステートメントです。
fmt.Printf("%v\n", timeZone) // fmt.Println(timeZone)としても同じ
これの出力結果です。
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
マップの出力では、キーの出力は当然ながら順不同となります。
構造体を出力するとき、限定フォーマット%+vを使うと構造体の各フィールドに対してフィールド名も出力されます。また、代替フォーマット%#vを使うとどんな値でも完全なGo言語の構文形式で出力されます。
type T struct {
a int
b float
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
これらの出力結果です。(アンパサンドに注意)
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
引用符付き文字列の出力も、stringまたは[]byte型の値に対して%qを使えば可能です。(代替フォーマット%#qでは、可能であればバッククォートを使います。) また%xを文字列またはバイト配列に対して適用したときは、整数に適用したときと同様に、長い16進数文字列を出力します。このときフォーマットにスペースを入れると(% x)、バイト間にスペースが出力されます。
もう一つの便利なフォーマットは%Tです。これは値の型を出力します。
fmt.Printf("%T\n", timeZone)
これの出力結果です。
map[string] int
自作の型でデフォルトのフォーマット処理を制御したければ、必要なことはその型にメソッドString() stringを定義するだけです。下は先ほどの型Tに実装してみた例です。
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
これのフォーマット出力結果です。
7/-2.35/"abc\tdef"
自作のString()メソッドはSprintfからも呼び出されます。これは出力ルーチンが完全にリエントラント(再入可能)かつ再帰的に呼び出されるためです。もう少し手を加えて、出力ルーチンが受け取った引数を、同様の別ルーチンにそのまま渡すことも可能です。Printfのシグネチャの最後の引数には...interface{}型が使われているため、フォーマット指定以降には、いくつでも、どんな型のパラメータでも指定できます。
func Printf(format string, v ...interface{}) (n int, errno os.Error) {
Printf関数の中で、vは、[]interface{}型の変数のように振る舞いますが、これを他の可変変数関数に渡すときは、通常の引数リストのように振る舞います。下は、以前使用した関数log.Stderrの実装です。ここでは、実際にフォーマット処理を行うために、fmt.Sprintlnに引数をそのまま渡しています。
// Stderrは標準出力にログを簡単に出力するヘルパー関数です。Fprintln(os.Stderr)と似ています。
func Stderr(v ...interface{}) {
stderr.Output(2, fmt.Sprintln(v)) // Outputはパラメータ (int, string)を取る
}
ここで説明した範囲は出力機能のほんの一部です。より詳しい説明はパッケージfmtのgodocドキュメントを参照ください。
話題が変わりますが、...パラメータには型を指定します。たとえば、次のmin関数の...intです。この関数は、整数リスト内から最小値を抽出します。
func Min(a ...int) int {
min := int(^uint(0) >> 1) // intの最大値
for _, i := range a {
if i < min {
min = i
}
}
return min
}
初期化
初期化において、Go言語とCやC++言語では見かけ上それほど差がないように見えますが、Go言語の初期化はより強力です。複合構造体は初期化を行いながら構築することが可能です。また異なるパッケージ間においてもオブジェクトの初期化順序は正しく取り扱われます。
定数
Go言語における定数は、その名の通り「定数」です。定数はコンパイル時に作成されます。これは定数が関数内でローカルに定義されているときも同様です。ただし定数となり得るのは数値、文字列、論理値だけです。定数はコンパイル時に作成されるという制約上、コンパイラによって評価可能な定数式でなければなりません。たとえば1<<3は定数式ですが、math.Sin(math.Pi/4)は定数式ではありません。これは後者を評価するためにはmath.Sinの呼び出しが必要となるためです。
Go言語での定数の列挙にはiota列挙子を使います。iotaは式の一部となって、かつその式は暗黙的に繰り返すことができるので値の複雑なセットも簡単に作成することができます。
type ByteSize float64
const (
_ = iota // 一番目の値はブランク識別子に代入して無視
KB ByteSize = 1<<(10*iota)
MB
GB
TB
PB
EB
ZB
YB
)
Stringのようなメソッドを型と結びつけることができるので、型の一部として上のような値を出力用に自前で自動フォーマットすることが可能になります。
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
式YBの出力は1.00YBになり、ByteSize(1e13)の出力は9.09TBとなります。
変数
変数は、定数と同じように初期化することができますが、変数のイニシャライザは通常の式であり、実行時に評価されます。
var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)
init関数
最後になりますが、各ソースファイルにはそれぞれ必要に応じて、セットアップのためにinit()関数を定義することができます。ひとつだけ制約があり、初期化中にゴルーチンを起動することはできますが、初期化が完了するまで実行は開始しません。すなわち、初期化処理中に実行されるスレッドは常にひとつだけです。
init()関数は、パッケージでインポートしている他のパッケージが初期化されたあと、パッケージ内で宣言されているすべての変数のイニシャライザが評価されたあとに呼び出されます。init()関数の一般的な使い方は、宣言としては記述できないような初期化処理を行うほか、処理の実行開始直前に、プログラムのステートの妥当性チェックおよびステートの修正を行います。
func init() {
if USER == "" {
log.Exit("$USER not set")
}
if HOME == "" {
HOME = "/usr/" + USER
}
if GOROOT == "" {
GOROOT = HOME + "/go"
}
// GOROOTはコマンドラインから--gorrotフラグを指定することで上書き可能
flag.StringVar(&GOROOT, "goroot", GOROOT, "Go root directory")
}
メソッド
ポインタ vs. 値
メソッドは、名前がつけられていればポインタとインタフェースを除くすべての型に定義することができます。(レシーバは構造体である必要はありません。)
以前、スライスの説明のところで書いたAppend関数を今度はスライスのメソッドとして定義してみます。これにはまず、メソッドと結びつけるために新たに名前付きの型を宣言し、メソッドにこの型のレシーバを定義します。
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// 本体部分は前と全く同じ
}
このままでは、更新したスライスをメソッドから返す必要がまだ残っています。レシーバとしてByteSliceへのポインタを受け取るようメソッドを定義しなおすことによって、その問題を回避できます。こうすれば呼び出し元のスライスに対しメソッド内から上書き可能になります。
func (p *ByteSlice) Append(data []byte) {
slice := *p
// 本体部分は上とおなじだが、returnは除外
*p = slice
}
実のところ、もう少し改善可能です。たとえば、この関数を標準的なWriteメソッドに合わせるように変更すると下のようになります。
func (p *ByteSlice) Write(data []byte) (n int, err os.Error) {
slice := *p
// これも上と同じ
*p = slice
return len(data), nil
}
このようにすることで*ByteSlice型は、利便性の高い標準インタフェースio.Writerを満たすようになり、下の例のようにスライスに対し出力することが可能となります。
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
上でByteSliceのアドレスを渡しているのは、*ByteSliceでなければio.Writerインタフェースを満たさないためです。レシーバとしてポインタまたは値のどちらを受け取るかによって、次の違いがあります。レシーバとして値を受け取るメソッドでは、ポインタまたは値で呼び出すことができますが、ポインタを受け取るメソッドでは、ポインタでしか呼び出すことができません。これは後者ではメソッド内でレシーバを変更可能であるためです。(複製された値に加えた変更は破棄されてしまうため。)
ちなみに、Writeをバイトのスライスで使うというアイデアはbytes.Bufferで実装済です。
インタフェースとそれ以外の型
インタフェース
Go言語のインタフェースは、オブジェクトの振舞いを規定する手立てです。このセクションではインタフェースにより実現できることすべてを説明します。今まですでに2、3の簡単な例を見てきました。たとえばカスタム出力はStringメソッドを実装することで作られ、一方FprintfはWriteメソッドによって出力先をどこへでも変更できます。Go言語のコードでは、通常インタフェースは1~2個のメソッドしか持たず、またWriteメソッドを持つio.Writerのようにたいていメソッド名と関連した名前が付けられます。
型には複数のインタフェースを実装することができます。たとえばあるコレクション型がLen()、Less(i, j int) bool、Swap(i, j int)メソッドを持つsort.Interfaceを実装していればsortパッケージのルーチンを使ってソートが可能になり、その上さらに独自の出力メソッドを実装することもできます。下の例のSequence型は、少々不自然ですがこれらをすべて実装しています。
type Sequence []int
// sort.Interfaceに必要な全メソッドを実装
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// 出力用メソッド - 出力前に要素を並び替え
func (s Sequence) String() string {
sort.Sort(s)
str := "["
for i, elem := range s {
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
変換
さきほどのSequenceのStringメソッドを変更して、Sprintが本来持っているスライス出力機能を利用するようにしました。Sprintを呼び出す前にSequenceを純粋な[]intに変換することで、既存の処理を利用することが可能になります。
func (s Sequence) String() string {
sort.Sort(s)
return fmt.Sprint([]int(s))
}
このとき変換が行われ、sがただのスライスとみなされるため、スライスに規定されている書式で文字列が返されます。ここで変換を行われなければ、SprintはSequenceのStringメソッドを見つけ出し、呼び出しを無限に繰り返してしまいます。この2つの型(Sequenceと[]int)は名前を除けば同一であるため、これらの型の間における変換は問題なく行われます。この変換では新しい値が作られることはなく、既存の値が一時的に新しい型を持つかのような働きをします。(これと異なる変換もあります。たとえば整数を浮動小数点へ変換したときは新しく値が作成されます。)
Go言語のプログラムでは、異なるメソッド群を使用するために式の型を変換することがよく行われます。例として、既存の型sort.IntArrayを使ってサンプルプログラム全体を下のように軽量化することが可能です。
type Sequence []int
// 出力用メソッド - 出力する前に要素を並び替え
func (s Sequence) String() string {
sort.IntArray(s).Sort()
return fmt.Sprint([]int(s))
}
これで、Sequenceに複数のインタフェース(ソートと出力)を実装する代わりに、データを複数の型(Sequence、 sort.IntArray、[]int)に変換できることを利用して各機能を実現できるようになりました。このような使い方をすることは実際にはほとんどありませんが効果的な使い方です。
概説
ある型がインタフェースをひとつだけ実装していて、そのインタフェースのメソッド以外にエクスポートされたメソッドを持たないならば、型自体のエクスポートは不要です。インタフェースだけをエクスポートすることで、次の点を明白にすることができます。ひとつは、重要なのは実装ではなく振舞いであること。もうひとつは、振舞いは同じでも異なる機能を持った別個の実装であることです。また、共通メソッドを実装している各箇所で、その都度ドキュメントを記述する手間が省けるという利点もあります。
このような場合は、コンストラクタは実装している型ではなく、インタフェース値を返さなければなりません。一例として、ハッシュライブラリのcrc32.NewIEEE()とadler32.New()では、双方ともhash.Hash32インタフェース型の値を返します。Go言語プログラムでこのハッシュアルゴリズムをAdler-32からCRC-32に変更するために必要なのは、コンストラクタの呼び出しを入れ替えるだけです。それ以外のコードは、ハッシュアルゴリズムの変更による影響を受けません。
同様のアプローチにより、crypto/blockパッケージのストリーム暗号アルゴリズムは、一連のブロック暗号から独立しています。これらは特定の実装を返すのではなく、bufioパッケージにならってCipherインタフェースをラップしたhash.Hash、io.Reader、io.Writerいずれかのインタフェース値を返します。
下はcrypto/block内のインタフェースです。
type Cipher interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
// NewECBDecrypterは、rからデータを読み込み、
// c内の電子コードブック(ECB)モードを使って復号化を行うReaderを返します。
func NewECBDecrypter(c Cipher, r io.Reader) io.Reader
// NewCBCDecrypterは、rからデータを読み込み、
// c内の暗号ブロックチェイニング(CBC)モードと初期化ベクタivを使って
// 復号化を行うReaderを返します。
func NewCBCDecrypter(c Cipher, iv []byte, r io.Reader) io.Reader
NewECBDecrypterおよびNewCBCReaderで扱うことができるのは、特定の暗号化アルゴリズムやデータソースではなく、どのCipherインタフェースの実装であっても、またどのio.Readerでも適用することが可能です。これらはio.Readerインタフェース値を返すため、ECB暗号化をCBC暗号化と入れ替えるときは部分的な変更だけですみます。コンストラクタを呼び出している箇所は変更が必要ですが、その周囲のコードではコンストラクタから返されるのはio.Readerだけとみなしていれば違いに気づくことさえないでしょう。
インタフェースとメソッド
ほぼすべての型に対してメソッドを付け加えることができるので、これはすなわち、どのインタフェースであっても、ほとんどの型に実装可能であると言えます。この実例のひとつが、Handlerインタフェースを定めているhttpパッケージにあります。Handlerを実装しているすべてのオブジェクトで、HTTPリクエストを処理することが可能です。
type Handler interface {
ServeHTTP(*Conn, *Request)
}
ここでは簡略化のため、POSTは無視してHTTPリクエストが常にGETであると仮定します。 (この簡略化がハンドラの書き方に影響を及ぼすことはありません。) 下は、ページの訪問回数を単に数えるだけのハンドラの実装一式です。
// 単純なカウントサーバ
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) {
ctr.n++
fmt.Fprintf(c, "counter = %d\n", ctr.n)
}
(今回のテーマを気にとめつつ、FprintfがどのようにしてHTTP接続を出力するか注意してみてください。)
参考までに、こういったサーバをURLパスに割り当てる手順です。
import "http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
ところで、なぜCounterを構造体としているのでしょうか。必要なのは整数ひとつだけのはずです。(ただし値が増えたことを呼び出し側にも伝わるように、レシーバはポインタである必要があります。)
// 単純なカウントサーバ
type Counter int
func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) {
*ctr++
fmt.Fprintf(c, "counter = %d\n", *ctr)
}
自作プログラムが内部ステータスを持っていて、そこにページが訪問されたことを通知しなければならないとしたらどうすればよいでしょうか。このようなときは、次のようにチャネルとウェブページとを関連付けてください。
// 訪問がある度に通知を送信するチャネル
// (たぶんバッファリングされている必要がある)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(c *http.Conn, req *http.Request) {
ch <- req
fmt.Fprint(c, "notification sent")
}
最後に、パス/argsを訪問したときにサーバプログラムを起動した際に与えられた引数を出力するようにしてみましょう。引数の出力関数は下のように簡単に書けます。
func ArgServer() {
for i, s := range os.Args {
fmt.Println(s)
}
}
これをHTTPサーバへ変更していきます。さきほどのArgServer関数を適当な型(値は何でも良い)のメソッドとすることもできますが、それより良い方法があります。メソッドはポインタとインタフェース以外すべての型に定義することができるため、関数に対してメソッドを書くことも可能です。httpパッケージには、下のコードが含まれています。
// HandlerFunc型は、通常の関数をHTTPハンドラとして
// 使用可能にするためのアダプタです。
// fが適切なシグネチャを持つ関数であれば、HandlerFunc(f)は
// fを呼び出すハンドラオブジェクトとなります。
type HandlerFunc func(*Conn, *Request)
// ServeHTTPはf(c, req)を呼び出す
func (f HandlerFunc) ServeHTTP(c *Conn, req *Request) {
f(c, req)
}
HandlerFuncはServeHTTPメソッドを持つ型であるため、この型の値はHTTPリクエストを処理できます。メソッドの実装を見てください。このメソッドのレシーバは関数fであり、メソッド内でfを呼び出しています。これは少々風変わりではありますが、前出のチャネルをレシーバとしてそのチャネルへ送信するメソッドと大差ありません。
ArgServerをHTTPサーバにするため、まずは正しいシグネチャを持つように修正します。
// 引数サーバ
func ArgServer(c *http.Conn, req *http.Request) {
for i, s := range os.Args {
fmt.Fprintln(c, s)
}
}
これでArgServerはHandlerFuncと同じシグネチャを持つようになったので、以前SequenceをIntArray.Sortを使用するためIntArrayに変換したときと同様に、ArgServerをHandlerFunc内のメソッドを使用するためにHandlerFunc型に変換可能になりました。このセットアップを行うコードは次のように簡潔に書けます。
http.Handle("/args", http.HandlerFunc(ArgServer))
誰かが/argsページを訪問したときに呼び出されるハンドラは、値はArgServerで型はHandlerFuncとなりました。まず、HTTPサーバによってArgServerをレシーバとしてHandlerFunc型のServeHTTPメソッドが実行され、続いてHandlerFunc.ServeHTTP内のf(c, req)を通してArgServerが呼び出されます。そのあと引数の表示が行われます。
このセクションで、構造体、整数、チャネル、関数を使ってHTTPサーバを作成したのは、インタフェースがまさにメソッド群であり、(ほとんど)すべての型に対して定義できることを示すためです。
埋込み
Go言語には型によるサブクラス化という典型的な概念はありませんが、構造体またはインタフェースに型を埋込み、実装を「借りる」仕組みがあります。
インタフェースの埋込みはとても単純です。下は以前説明したio.Readerとio.Writerインタフェースの定義です。
type Reader interface {
Read(p []byte) (n int, err os.Error)
}
type Writer interface {
Write(p []byte) (n int, err os.Error)
}
ioパッケージではこれと同じように、オブジェクトに対しメソッドの実装を規定するためのインタフェースが、他にもいくつかエクスポートされています。たとえば、ReadとWrite両方を持つインタフェースio.ReadWriterがあります。これら2つのメソッドを明示的に記述することでio.ReadWriterを定義することもできますが、次のようにして2つのインタフェースを埋込んで新しいインタフェースを作成するほうがより簡単で、意図が伝わりやすくなります。
// ReadWriteは、基本的なメソッドReadとWriteをグルーピングしたインタフェース
type ReadWriter interface {
Reader
Writer
}
このようにすることで、Readerが行えること、およびWriterが行えることをReadWriterが兼ね備えていて、このインタフェースが埋込みインタフェース(メソッド群内に共通のメソッドを持っていてはならない)の結合により作られていることが見て取れます。ただしインタフェース内に埋込むことができるのはインタフェースだけです。
この基本的な考え方は構造体にもあてはまりますが、構造体の場合はより広範囲に渡る影響があります。bufioパッケージは2つの構造体型、bufio.Readerとbufio.Writerを持ち、当然それぞれパッケージioの対応するインタフェースを実装しています。bufioではさらに埋込みを利用して、ひとつの構造体にReaderとWriterを組み込んでバッファ付きの読み書きを実装しています。下の例で構造体内に型を列挙していますが、このときフィールド名は付けていません。
// ReadWriterは、ReaderとWriterのポインタを格納している
// これはio.ReadWriterの実装
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
この埋め込まれた要素は構造体のポインタであり、使用前には当然、有効な構造体のポインタで初期化されていなければなりません。このReadWriter構造体はこう書き換えることができます。
type ReadWriter struct {
reader *Reader
writer *Writer
}
ただこうした場合は、下のようにして呼び出しをフォワードするメソッドを用意し、フィールド内の各メソッドを実装しioインタフェースを満たすようにしなければなりません。
func (rw *ReadWriter) Read(p []byte) (n int, err os.Error) {
return rw.reader.Read(p)
}
しかし、直接構造体を埋込んでしまえば、この冗長な記述は要らなくなります。埋込まれた型が持っているメソッドは自動で持ち上げられるため、すなわちbufio.ReadWriterはbufio.Readerと bufio.Writerのメソッドを持つだけでなく、3つのインタフェース(io.Reader、io.Writer、io.ReadWriter)の全てを満たすようになります。
埋込みとサブクラス化では大きな違いがあります。型を埋込んでいるとき、埋込んだ型が持っているメソッドは埋込み先のメソッドともなりますが、実行しているメソッドのレシーバは埋込み先の型ではなく、あくまで元の型です。サンプルコードの bufio.ReadWriterのReadメソッドが実行されることと、先程のフォワードメソッドが実行されることは結果としてまったく同じです。(レシーバはReadWriterフィールドのreaderで、ReadWriter自身ではありません。)
また、埋込みを使うことで少し扱いやすくもなります。下の例では、通常の名前付きフィールドと並んで、埋込みフィールドを記述しています。
type Job struct {
Command string
*log.Logger
}
これでJob型は、*log.Logger型が持つLog、Logfといったメソッドを持つようになりました。もちろん、Loggerにフィールド名を与えることはできますが、それは必須ではありません。これで一度初期化すればJobのログが記録できるようになります。
job.Log("starting now...")
このLoggerは普通の構造体フィールドであるため、いままで通りコンストラクタを使用して初期化が行えます。
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
もしくは複合リテラルを使用します。
job := &Job{command, log.New(os.Stderr, nil, "Job: ", log.Ldate)}
埋込まれているフィールドを直接参照しなければならないときは、フィールドの型名(パッケージ名は不要)をフィールド名として用います。つまりJob型である変数jobの*log.Loggerにアクセスしたいときはjob.Loggerと書きます。これはLoggerのメソッドに手を加えたいときに役立ちます。
func (job *Job) Logf(format string, args ...) {
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args))
}
型を埋込むことで名前の競合が発生する恐れがありますが、その解決ルールは単純です。最初に、フィールドまたはメソッドXは、その他の、より深い入れ子内にあるXを隠蔽します。たとえばlog.LoggerにCommandと名づけられたフィールドまたはメソッドが含まれていても、JobのCommandフィールドがそれより優先されます。
2番目として、同一の入れ子階層に同じ名前が現れたとき、通常ではエラーとなります。たとえばJob構造体に、Loggerと名づけられた別のフィールドまたはメソッドが含まれているときlog.Loggerの埋め込みは不正です。ただし、このとき重複した名前が、型定義より外側のプログラムから一切アクセスされなければ大丈夫です。この決まりによって、埋め込まれた型へ外部から変更を加えてもある程度保護されます。つまり、フィールドの追加で他の型が持つフィールドとかち合ってしまっても、どちらのフィールドも一切使用されることがなければ何ら問題ありません。
並列性
通信による共有
並列プログラミングは大きなトピックではありますが、ここではGo言語の仕様の中で特徴的な部分だけを取り上げます。
一般的な環境における並列プログラミングは、共有変数にアクセスするためには適切な実装が厳密に求められるため扱いにくいものでした。Go言語では、共有変数をチャネル上で引き回し、実行中の異なるスレッドからは実際に同時アクセスさせないという今までとは異なる手法を推奨しています。あるタイミングで値にアクセスできるゴルーチンはひとつだけなので、設計上データ競合は起きえません。
共有メモリを使った通信は行わず、代わりに通信を使うことでメモリを共有するようにしてください。
この手法は、過剰となる側面もあります。参照カウンタであれば、たとえば整数型の変数とミューテックスを併用するほうが最適かもしれません。しかし、高水準な手法として、アクセス制御のためにチャネルを使うことは、明解で正確なプログラムを記述することをより容易くします。
このモデルについて考える手立てとして、1台のCPU上で動いている典型的なシングルスレッドプログラムがあると仮定します。これには同期プリミティブは不要です。同様に同期が不要であるインスタンスを新しく起動して、これらスレッド間を通信させます。この通信が同期していれば、それ以外の同期はとりあえず必要ありません。このモデルと一致する例としては、Unixのパイプラインが良く知られています。Go言語におけるこの並列性へのアプローチは、Hoare氏のCommunicating Sequential Processes (CSP)が基になっており、Unixパイプにおけるタイプセーフの汎用化にもこれが見受けられます。
ゴルーチン
ゴルーチンと呼ぶようにした理由は、スレッド、コルーチン、プロセスといった既存の用語では正確に伝わらないためです。ゴルーチンが持つモデルは単純で、その役割は同一アドレス空間内で他のゴルーチン同士を並列実行することです。またゴルーチンは軽量で、スタック空間を割り当てるより若干コストがかかる程度です。開始時のスタックサイズは節約のため小さく取り、必要に応じてヒープ領域を割り当て(自由に)拡張します。
ゴルーチンはOSの複数スレッド上へ多重化されるので、ひとつが例えばI/O待ちなどでブロックされていても、他のゴルーチンの実行は継続します。ゴルーチンの仕組みによってスレッドを作成・管理する複雑さの大部分が軽減されます。
関数またはメソッドの呼び出しを新規ゴルーチンとして実行するには、呼び出しの直前にキーワードgoと記述してください。呼び出しが完了した時点で、すでにゴルーチンは作成されています。(この効能はUnixシェルにてバックグラウンドでコマンドを実行するために使用する「&」表記とほぼ同じです。)
go list.Sort() // list.Sortを並列実行する(処理の完了は待たない)
関数リテラルを使うと手軽にゴルーチンが実行できます。
func Announce(message string, delay int64) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // 括弧に注意。関数を呼び出すために必要。
}
Go言語における関数リテラルはクロージャです。このとき関数内から参照されている変数は関数が実行している間、生存しつづけます。
この例では関数が完了したことを通知する手立てがないため、あまり実践的ではありません。完了通知を行うにはチャネルを使います。
チャネル
マップと同じくチャネルは参照型であり、makeを使って割り当てられます。作成時にオプションの整数パラメータを指定すると、チャネルのバッファサイズとして使われます。この値のデフォルトはゼロであり、ゼロのときバッファリングは行われず同期チャネルとなります。
ci := make(chan int) // 整数型のバッファなしチャネル cj := make(chan int, 0) // 整数型のバッファなしチャネル cs := make(chan *os.File, 100) // Fileへのポインタ型のバッファありチャネル
チャネルは、値の交換および同期という通信機能を兼ね備えており、2つの計算処理(ゴルーチン)が予期しない状態とならないことを保証します。
チャネルを利用した優れたイディオムはたくさんありますが、手始めにひとつ紹介します。前のセクションではソート処理をバックグラウンドで起動しましたが、これにチャネルを使って起動したゴルーチンのソート完了を待つよう変更できます。
c := make(chan int) // チャネルの割り当て
// ゴルーチンとしてsortを起動。完了時にチャネルへ通知
go func() {
list.Sort()
c <- 1 // 通知を送信。値は何でも良い
}()
doSomethingForAWhile()
<-c // sortの完了待ち。送られてきた値は破棄
受信側では常に、受信可能なデータが来るまでブロックされます。送信側はチャネルがバッファリングしていないときは、受信側が値を受信するまでブロックされます。チャネルがバッファリングしているときは、送信側がブロックされるのは値がバッファへコピーされる間だけです。このためバッファがいっぱいのときは、受信側で値を取り出すまで待機します。
スループットを制限するようなケースで、バッファありチャネルをセマフォのように使うことができます。下の例では、入って来たリクエストはhandleに渡されます。その中でまず値をチャネルへ送信し、リクエストを処理したのちチャネルから値を受信しています。チャネルのバッファが持つ容量を利用してprocessの同時呼び出し数を制限しています。
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // アクティブキューの空き待ち
process(r) // 時間のかかる処理
<-sem // 完了。次のリクエストを処理可能にする
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // handleの終了待ちはしない
}
}
次も同様のアイデアで、一定数のhandleゴルーチンを起動しておき、その中でリクエストチャネルから全て読み込んでいます。ここではゴルーチンの数でprocessの同時呼び出し数を制限しています。このServe関数自体も、終了指示を受信するためのチャネルを引数として受け取り、ゴルーチンを起動したあと、このチャネルを受信することでブロックしています。
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *clientRequests, quit chan bool) {
// ハンドラの開始
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // 終了指示待ち
}
チャネル内のチャネル
Go言語の特徴のなかで重要なもののひとつは、チャネルがメモリを割り当てることができ、かつ他の値と同じように引き回すことができる「優れた値」であることです。この機能は一般的に並列、非多重化を安全に実装するために使われます。
前セクションの例におけるhandleはリクエストを処理するための理想的なハンドラではありましたが、そこで扱っていたRequest型は未定義でした。もしその型に応答用チャネルが含まれていれば、各クライアント側でそれぞれ処理結果を受信する経路を用意することができます。下はRequest型の定義の概略です。
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
リクエストオブジェクト内の処理結果受信用チャネルと同様に、クライアント側で関数とその引数も準備します。
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// リクエスト送信
clientRequests <- request
// 応答待ち
fmt.Printf("answer: %d\n", <-request.resultChan)
サーバ側で変更するのは、ハンドラ関数だけです。
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
これを現実的なコードにするには、やるべきことが多々残されていることは明らかですが、このコードはあくまで帯域制限、平行処理、非ブロック型RPC機構を実装したフレームワークであり、相互排他は考慮していません。
並列化
これらの考え方のもう一つの応用は、複数のCPUコアすべてを使い計算を並列処理することです。計算処理を個々に分割できるのであれば、各計算の完了を伝えるチャネルを用意することで並列化可能です。
下のサンプルのように、アイテムを格納したVector上で実行される高コストの演算処理があり、またその各アイテムにて演算される値同士が、お互い関連性を持たないと仮定します。
type Vector []float64
// v[i], v[i+1] ... v[n-1]までを演算処理に適用
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // この部分が終わったことを伝達
}
データをCPUコア数分割し、ループでコア毎に処理をひとつ起動します。演算処理が完了する順番は不定ですが、それは問題ではありません。単に全ゴルーチンを起動後、チャネルから受信した完了通知数をカウントするだけです。
const NCPU = 4 // CPUコア数
func (v Vector) DoAll(u Vector) {
c := make(chan int, NCPU) // 任意だが、バッファリングしたほうが賢明
for i := 0; i < NCPU; i++ {
go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
}
// チャネルを空に
for i := 0; i < NCPU; i++ {
<-c // 処理の完了をひとつ待つ
}
// すべて完了
}
gc(6g等)の現実装のデフォルトでは、このコードは各CPUコアへ並列化はされません。ユーザ処理に対してはコアをひとつしか使用しないようになっているためです。システムコールで任意の数のゴルーチンをブロックすることは可能ですが、デフォルトで実行されるユーザレベルコードは常にひとつだけです。これはもっと賢くあるべきで、また今後賢くしていく予定はありますが、そうなるまでCPUを並列処理させたいときはコードを同時実行したいゴルーチン数をランタイムに指定してください。この指定方法は2つあり、環境変数GOMAXPROCSに使用するコア数(デフォルト1)を設定してからジョブを実行するか、もしくはruntimeパッケージをインポートし、runtime.GOMAXPROCS(NCPU)を呼び出してください。また、スケジューリングとランタイムが改善されたときは、この設定は不要となる予定です。
溢れるバッファ
並列プログラミング向け機能を利用して、より実装が簡単な非並列処理を記述することもできます。下はRPCパッケージから取り出した例です。クライアント側のゴルーチンではループ内でどこかのソース(おそらくネットワーク)からデータを受信します。バッファの割り当て・開放を減らすために使わなくなったバッファはリストに格納しておきます。これを実現するためにバッファありチャネルを使い、このチャネルが空のときは新たにバッファを割り当てます。受信したメッセージをバッファに格納すると、そのバッファはserverChan経由でサーバへ送信されます。
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)
func client() {
for {
b, ok := <-freeList // バッファがあれば取得
if !ok { // なければ新たに割り当てる
b = new(Buffer)
}
load(b) // 次のメッセージをネットから読み込む
serverChan <- b // サーバへ送信
}
}
サーバ側のループではクライアントからメッセージを受信し処理を行ったあと、バッファリストにバッファを返却します。
func server() {
for {
b := <-serverChan // 作業待ち
process(b)
_ = freeList <- b // 空きがあればバッファを再利用
}
}
クライアントの非ブロック受信では、利用可能であればバッファリストからバッファを取得します。利用可能なバッファがないときは新しくバッファを割り当てます。サーバのfreeListへの非ブロック送信は、バッファリストがいっぱいでなければbをfreeListに戻します。バッファが溢れたときはガーベジコレクタによって回収されます。(送信演算子をブランク識別子へ代入することで非ブロック送信となりますが、操作が成功したかどうかは無視されます。) この実装では、ある境界線で溢れてしまうバケツのような空きバッファリストを作成し、冗長なコードを記述する代わりにチャネルのバッファとガーベジコレクタ任せにしています。
エラー情報
ライブラリルーチンでは、呼び出し元にエラー情報などを返す必要が度々発生します。以前説明したように、Go言語の複数戻り値を使うと通常の戻り値と一緒にエラーの詳細情報を返すことが簡単にできます。またエラーは慣例的にos.Errorというシンプルなインタフェースを実装しています。
type Error interface {
String() string
}
ライブラリの作成者はこのインタフェースと併せて、より高機能なモデルを自由に実装して構いません。こうすることでエラーを知らせるだけでなく、何らかの状況も提供することができます。この例として、os.Openは下のos.PathErrorを返しています。
// PathErrorは、エラーとそれを引き起こした操作及び
// ファイルパスが記録されます。
type PathError struct {
Op string // "open", "unlink", など
Path string // 関連ファイル
Error Error // システムコールからの戻り値
}
func (e *PathError) String() string {
return e.Op + " " + e.Path + ": " + e.Error.String()
}
PathErrorのStringメソッドは次のような文字列を生成します。
open /etc/passwx: no such file or directory
このようにエラー情報内に問題の起きたファイル名、操作、引き金となったオペレーティングシステムのエラーを含んでいると、エラーとなった呼び出し箇所から離れた箇所でエラー情報を出力したときにも役立ちます。「そのようなファイルやディレクトリはありません」と単に返されるより、多くの情報を与えてくれます。
呼び出し元で、エラーの正確な詳細情報が必要なときは、型スイッチまたは型アサーションを使い特定のエラーを探して詳細情報を得ることができます。たとえばPathErrorsの場合、内部のErrorフィールドを調べることでリカバリ可能か判断できることがあります。
for try := 0; try < 2; try++ {
file, err = os.Open(filename, os.O_RDONLY, 0)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Error == os.ENOSPC {
deleteTempFiles() // 空き容量を回復
continue
}
return
}
Panic
呼び出し元にエラーを通知する一般的な方法は、戻り値にos.Errorを付け加えることです。標準的なReadメソッドが良い例で、バイト数とos.Errorを返します。しかし、エラーがリカバリできないときはどうでしょう?ときおり、プログラムは全く継続できない事態に陥ることがあります。
こういったときのために、組み込み関数panicがあります。これは、実質的にプログラムを停止させるランタイムエラーを作成します。(ただし、次のセクションを参照のこと) この関数は、プログラム停止時に出力するため、任意の型(たいていは文字列)の引数をひとつ受け取ります。これは、例えば無限ループから抜けるなどの、どうしようもできない事態が起きたことを示す手段でもあります。実のところコンパイラは、関数の最後に記述されたpanicがあると、returnステートメントで本来行うべきチェックを行いません。
// ニュートン法を使った、擬似的な立方根の実装
func CubeRoot(x float64) float64 {
z := x/3 // 任意の初期値
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// 100万回の繰り返しで収束しなかった。何かが間違っている。
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x)
}
これは、単なる例に過ぎませんが、本来ライブラリではpanicを起こさないようにしなければなりません。問題点を隠せるか回避できるのであれば、プログラム全体をダウンさせるより、実行を継続させる方が常に優れています。ただし、初期化においてはそうとは限らず、ライブラリが自身のセットアップがどうしても行えないときは、panicを起こすことは合理的とも言えます。
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
Recover
panicが呼び出されたとき(これには、配列のインデックスが範囲外であるときや、型アサーションに失敗したような暗黙的なものも含む)は、すぐさま、カレントの関数を停止し、ゴルーチンのスタックの巻き戻しを開始します。その途中、遅延指定されている関数をすべて実行します。この巻き戻しがゴルーチンのスタックの先頭にたどり着くと、プログラムは終了します。ただし、組み込み関数recoverを使うことで、ゴルーチンの制御を取り戻し、通常の実行を再開させることが可能です。
recoverを呼び出すと、巻き戻しを停止し、panicに渡された引数が返ります。巻き戻り中に実行できるコードは、defferで遅延指定された関数内のみなので、recoverは遅延指定された関数内でのみ役立ちます。
次のサンプルは、サーバ内の他の実行中のゴルーチンを停止することなく、エラーの起きているゴルーチンをシャットダウンするためのrecoverの応用です。
func server(workChan <-chan *Work) {
for work := range workChan {
safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Stderr("work failed:", err)
}
}()
do(work)
}
この例では、do(work)でパニックが起こると、ログに記録し、他を妨げることなく、ゴルーチンを終了します。(訳注:このサンプルは、本来safelyDoがゴルーチンとして起動されるものと思われる) 遅延指定されたクロージャ内では、他に何もする必要はなく、recoverの呼び出しで状態を完全にハンドリングします。
なお、このリカバリパターンを適所に用いれば、panicを呼び出すことで、do関数(と、それが呼び出すもの)が、どんな状況からも、きれいに抜け出せます。複雑なソフトウェアにおけるエラーハンドリングの単純化のために、この概念を利用できます。その次に、regexpパッケージからの抜粋を見ていきましょう。これは、発生した解析エラーを、ローカルなError型とともにpanicを呼び出すことで通知を行います。下のコードは、Error型、errorメソッド、Compile関数の定義です。
// Errorは、解析エラーを表す型で、os.Errorを満たします。
type Error string
func (e Error) String() string {
return string(e)
}
// errorは、*Regexpのメソッドです。解析エラーを
// Errorとともにpanicを呼び出すことで通知します。
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compileは、解析した正規表現を返します。
func Compile(str string) (regexp *Regexp, err os.Error) {
regexp = new(Regexp)
// doParseは、解析エラーのときpanicを起こす
defer func() {
if e := recover(); e != nil {
regexp = nil // 戻り値をクリア
err = e.(Error) // 解析エラーでなければ、再びpanicを起こす
}
}()
return regexp.doParse(str), nil
}
doParseがパニックを起こしたときは、リカバリブロックでは、戻り値にnilをセットします。(遅延指定された関数内では、名前付きの戻り値を変更できます) その次に、errへ代入する際、問題の発生原因が解析エラーであるかを調べるため、Error型に型アサーションを行います。Error型でなければ、型アサーションに失敗するので、それによりランタイムエラーが発生して、何も起きなかったかのようにスタックの巻き戻しが再開します。このチェックは、配列のインデックスが範囲外であったなどの予期しないことが起こる可能性があることを意味しており、panicとrecoverでユーザトリガーのエラーをハンドリングしていても、コードがエラーを起こすことは避けられません。
適所で、このエラーハンドリングを使っていると、errorメソッドを使った解析エラーの通知が、解析スタックを巻き戻すことに気を使うことなく簡単に行えます。
このパターンは役立ちますが、パッケージ内でだけ使われなければなりません。Parseは、内部におけるpanicの呼び出しを、呼び出し側に見せないためにos.Error変更します。これは、従いやすいルールです。
ウェブサーバ
Go言語のウェブサーバプログラムを作成して締めくくります。これは実際にはウェブ転送サーバの類です。Googleではhttp://chart.apis.google.comで、データをチャートやグラフに自動変換するサービスを提供していますが、このサービスはデータをクエリパラメータとしてURLに含めなくてはならないため対話的な使い方ができません。ここで紹介するプログラムは、文字を入力するとQRコード(テキストをエンコードした矩形マトリクス)を生成させるために先程のチャートサーバを呼び出す、ちょっとした画面を提供します。生成された画像を携帯電話のカメラで撮影するとURLなどに変換されるので、携帯電話の小さいキーを駆使してURLを入力する手間が省けます。
下はこのプログラム一式です。説明はそのあとに続きます。
package main
import (
"flag"
"http"
"io"
"log"
"template"
)
var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18
var fmap = template.FormatterMap{
"html": template.HTMLFormatter,
"url+html": UrlHtmlFormatter,
}
var templ = template.MustParse(templateStr, fmap)
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Exit("ListenAndServe:", err)
}
}
func QR(c *http.Conn, req *http.Request) {
templ.Execute(req.FormValue("s"), c)
}
func UrlHtmlFormatter(w io.Writer, v interface{}, fmt string) {
template.HTMLEscape(w, []byte(http.URLEscape(v.(string))))
}
const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{.section @}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={@|url+html}"
/>
<br>
{@|html}
<br>
<br>
{.end}
<form action="/" name=f method="GET"><input maxLength=1024 size=70
name=s value="" title="Text to QR Encode"><input type=submit
value="Show QR" name=qr>
</form>
</body>
</html>
`
main関数までは簡単に説明します。まずサーバのHTTPポート番号のデフォルト値をflagに設定しています。変数templは目新しい部分で、ページを表示するためにサーバ上で実行されるHTMLテンプレートを構築しています。これについては後でもう少し説明します。
main関数で起動パラメータの解析(flag.Parse)後、以前説明した方法でサーバのルートパスとQR関数とを紐付けています。続いてサーバを起動するためにhttp.ListenAndServeが呼び出されます。サーバが動いている間は、ここでブロックされ続けます。
QR関数では、入力フォームデータが含まれているリクエストを受け取り、そのフォーム内のsと名付けられたデータを使ってテンプレート処理を実行します。
このテンプレートパッケージは、json-templateを参考に作られており、とても高機能です。このサーバプログラムでは、ほんの触り程度しか紹介しませんが、テンプレートパッケージの主用途はテンプレートテキストの一部分をtempl.Executeに渡されたデータから得られた要素(この場合はフォームの値)で書き換えることです。テンプレートテキスト(templateStr)内の波括弧{}で囲まれている部分でテンプレートの動作を指定します。{.section @}から{.end}までの部分はデータ項目@の値を伴って実行されます。この@は「カレントのアイテム」を表しており、今回の値はフォーム値です。(文字列が空のときは、テンプレートの該当部分は出力されません。)
{@|url+html}の箇所では、フォーマッタマップ(fmap)に"url+html"という名前で登録されているフォーマット用関数を使ってデータを処理するよう指定しています。実際に登録されているUrlHtmlFormatter関数では、ウェブページ上で文字列を安全に表示させるためのサニタイズ処理を行います。
テンプレート内のその他の文字列はただのHTMLで、ページが読み込まれたときに表示されます。ここでは駆け足で説明したので、詳しくはテンプレートパッケージのドキュメントを参照ください。
数行のコードと、データと掛け合わせて出力するHTMLテキストだけで便利なウェブサーバが手に入りました。このようにGo言語はたった数行でも色々なことが行える高機能な言語です。
Trackback URL
Comments