効果的なGo
はじめに
Goは新しい言語です。既存の言語からアイデアを借りているものの、Goの効果的なプログラムは、関連する言語で書かれたプログラムとは異なる特性を持っています。C++やJavaのプログラムをそのままGoに翻訳しても、満足のいく結果が得られる可能性は低いでしょう。JavaプログラムはJavaで書かれており、Goで書かれているわけではありません。一方で、問題をGoの視点から考えると、成功するものの全く異なるプログラムが生まれる可能性があります。言い換えれば、Goをうまく書くためには、その特性とイディオムを理解することが重要です。また、命名、書式設定、プログラム構成など、Goプログラミングの確立された慣習を知ることも重要です。そうすることで、あなたが書いたプログラムが他のGoプログラマーにとって理解しやすくなります。
このドキュメントは、明確で慣用的なGoコードを書くためのヒントを提供します。これは、言語仕様、Tour of Go、およびGoコードの書き方を補完するものであり、これらすべてを最初に読むべきです。
2022年1月追記:このドキュメントはGoが2009年にリリースされた際に書かれたものであり、それ以降大幅な更新は行われていません。言語の安定性のおかげで、言語自体の使い方を理解するための良いガイドではありますが、ライブラリについてはほとんど触れられておらず、ビルドシステム、テスト、モジュール、ポリモーフィズムなど、執筆以降のGoエコシステムにおける重要な変更については全く触れられていません。非常に多くのことが起こり、現代のGoの利用方法を記述する大規模で増え続けるドキュメント、ブログ、書籍が優れた役割を果たしているため、これを更新する計画はありません。Effective Goは引き続き有用ですが、読者はこれが完全なガイドとは程遠いことを理解すべきです。詳細についてはissue 28782を参照してください。
例
Goパッケージのソースは、コアライブラリとしてだけでなく、言語の使い方の例としても機能することを意図しています。さらに、多くのパッケージには、go.devウェブサイトから直接実行できる、動作する自己完結型の実行可能例が含まれています。この例がそうです(必要であれば、「Example」という単語をクリックして開いてください)。問題へのアプローチ方法や実装方法について疑問がある場合、ライブラリのドキュメント、コード、および例が答え、アイデア、背景を提供することができます。
書式設定
書式設定の問題は最も議論されがちですが、最も重要性の低いものです。人々は異なる書式設定スタイルに適応できますが、その必要がない方が良く、誰もが同じスタイルに従えば、その話題に費やす時間は少なくなります。問題は、長い規定的なスタイルガイドなしにこのユートピアにどうアプローチするかです。
Goでは珍しいアプローチを取り、ほとんどの書式設定の問題を機械に任せています。gofmtプログラム(ソースファイルレベルではなくパッケージレベルで動作するgo fmtとしても利用可能)は、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
}
標準パッケージのすべてのGoコードはgofmtでフォーマットされています。
いくつかの書式設定の詳細は残っています。ごく簡単に言うと、
- インデント
- インデントにはタブを使用し、
gofmtはデフォルトでタブを出力します。スペースは、どうしても必要な場合にのみ使用してください。 - 行の長さ
- Goには行の長さの制限がありません。パンチカードからあふれることを心配しないでください。行が長すぎるように感じたら、改行して余分なタブでインデントしてください。
- 括弧
- GoはCやJavaよりも括弧を必要としません。制御構造(
if、for、switch)には構文に括弧がありません。また、演算子の優先順位階層が短く明確なので、x<<8 + y<<16
他の言語とは異なり、スペースが示唆する意味になります。
コメント
GoはCスタイルの/* */ブロックコメントとC++スタイルの//行コメントを提供します。行コメントが一般的であり、ブロックコメントは主にパッケージコメントとして現れますが、式内や大量のコードを無効にする場合に役立ちます。
トップレベルの宣言の前に、間に改行を挟まずに現れるコメントは、宣言自体を文書化するものと見なされます。これらの「ドキュメントコメント」は、特定のGoパッケージまたはコマンドの主要なドキュメントです。ドキュメントコメントの詳細については、「Go Doc Comments」を参照してください。
名前
Goにおける名前は、他のどの言語と同様に重要です。それらは意味的な効果さえ持っています。パッケージ外部からの名前の可視性は、その最初の文字が大文字であるかどうかによって決まります。そのため、Goプログラムにおける命名規則について少し時間を割いて説明する価値があります。
パッケージ名
パッケージがインポートされると、パッケージ名はその内容へのアクセス手段となります。次の例の後、
import "bytes"
インポートするパッケージはbytes.Bufferについて言及できます。パッケージを使用する誰もが同じ名前でその内容を参照できると便利です。これは、パッケージ名が良いものであるべきであること、つまり短く、簡潔で、示唆に富んでいることを意味します。慣習として、パッケージには小文字の単語名が付けられます。アンダースコアやMixedCapsは不要です。あなたのパッケージを使用する誰もがその名前をタイプすることになるため、簡潔さを優先してください。そして、a prioriに衝突を心配しないでください。パッケージ名はインポートのデフォルト名に過ぎず、すべてのソースコードで一意である必要はありません。稀に衝突が発生した場合は、インポートするパッケージがローカルで使用する別の名前を選択できます。いずれにせよ、インポートのファイル名がどのパッケージが使用されているかを決定するため、混乱は稀です。
もう一つの慣習は、パッケージ名がそのソースディレクトリのベース名であることです。src/encoding/base64内のパッケージは"encoding/base64"としてインポートされますが、名前はbase64であり、encoding_base64でもencodingBase64でもありません。
パッケージのインポーターは、その内容を参照するために名前を使用するため、パッケージ内のエクスポートされた名前はその事実を利用して繰り返しを避けることができます。(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)と書くことで改善されることはありません。長い名前が自動的に読みやすくなるわけではありません。役立つドキュメントコメントは、余分に長い名前よりも価値があることが多いです。
ゲッター
Goはゲッターとセッターの自動サポートを提供しません。自分でゲッターとセッターを提供することに何の問題もなく、そうすることが適切な場合も多いですが、ゲッターの名前にGetを入れるのは慣用的でも必須でもありません。owner(小文字、非エクスポート)というフィールドがある場合、ゲッターメソッドはGetOwnerではなくOwner(大文字、エクスポート)と呼ばれるべきです。エクスポートのために大文字の名前を使用することで、フィールドとメソッドを区別するためのフックが提供されます。必要な場合、セッター関数はSetOwnerと呼ばれる可能性が高いでしょう。どちらの名前も実際には読みやすいです。
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
インターフェース名
慣習として、1つのメソッドを持つインターフェースは、メソッド名に-er接尾辞または同様の変更を加えてエージェント名詞を構築します。例:Reader、Writer、Formatter、CloseNotifierなど。
そのような名前は数多くあり、それらと、それらが捉える関数名を尊重することは生産的です。Read、Write、Close、Flush、Stringなどは規範的なシグネチャと意味を持っています。混乱を避けるために、同じシグネチャと意味を持たない限り、メソッドにそれらの名前のいずれかを付けないでください。逆に、あなたの型がよく知られた型のメソッドと同じ意味のメソッドを実装している場合、同じ名前とシグネチャを付けてください。あなたの文字列変換メソッドはToStringではなくStringと呼んでください。
MixedCaps
最後に、Goの慣習として、複数の単語からなる名前を書く際にはアンダースコアではなくMixedCapsまたはmixedCapsを使用します。
セミコロン
Cと同様に、Goの正式な文法ではセミコロンでステートメントを終端しますが、Cとは異なり、ソースコードにはこれらのセミコロンは現れません。代わりに、レクサーはスキャンする際に単純なルールを使用してセミコロンを自動的に挿入するため、入力テキストにはほとんどセミコロンが含まれません。
ルールは次のとおりです。改行の前の最後のトークンが識別子(intやfloat64のような単語を含む)、数値や文字列定数などの基本的なリテラル、または以下のいずれかのトークンである場合、
break continue fallthrough return ++ -- ) }
レクサーは常にそのトークンの後にセミコロンを挿入します。これは、「改行がステートメントを終了できるトークンの後に来る場合、セミコロンを挿入する」と要約できます。
閉じ括弧の直前でもセミコロンは省略できるため、次のようなステートメントは、
go func() { for { dst <- <-src } }()
セミコロンは必要ありません。慣用的なGoプログラムでは、forループの句(初期化、条件、継続の要素を区切るためなど)のような場所にのみセミコロンがあります。また、同じ行に複数のステートメントを記述する場合にも、セミコロンが必要です。
セミコロン挿入規則の1つの結果として、制御構造(if、for、switch、またはselect)の開始ブレースを次の行に置くことはできません。もしそうすると、ブレースの前にセミコロンが挿入され、意図しない効果を引き起こす可能性があります。次のように記述してください。
if i < f() {
g()
}
このような書き方はしないでください
if i < f() // wrong!
{ // wrong!
g()
}
制御構造
Goの制御構造はCのそれと関連していますが、重要な点で異なります。doループやwhileループはなく、わずかに一般化されたforのみです。switchはより柔軟で、ifとswitchはforと同様にオプションの初期化ステートメントを受け入れます。breakとcontinueステートメントは、中断または継続するものを識別するためのオプションのラベルを受け入れます。そして、型スイッチや多方向通信マルチプレクサである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)
if err != nil {
return err
}
codeUsing(f)
これは、一連のエラー条件からコードを保護する必要がある一般的な状況の例です。成功した制御フローがページを下に実行され、発生したエラーケースを排除するようにすると、コードは読みやすくなります。エラーケースはreturnステートメントで終わる傾向があるため、結果のコードにはelseステートメントは必要ありません。
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
再宣言と再代入
余談ですが、前のセクションの最後の例は、:=による短い宣言がどのように機能するかという詳細を示しています。os.Openを呼び出す宣言は次のようになります。
f, err := os.Open(name)
このステートメントは、2つの変数fとerrを宣言します。数行後、f.Statの呼び出しは次のようになります。
d, err := f.Stat()
これはdとerrを宣言しているように見えます。しかし、errが両方のステートメントに現れていることに注意してください。この重複は合法です。errは最初のステートメントによって宣言されますが、2番目のステートメントでは再代入されるだけです。これは、f.Statの呼び出しが、上で宣言された既存のerr変数を使用し、それに新しい値を与えるだけであることを意味します。
:=宣言では、変数vがすでに宣言されている場合でも、以下の条件が満たされていれば、現れることができます。
- この宣言が、
vの既存の宣言と同じスコープ内にあること(もしvがより外側のスコープで既に宣言されている場合、この宣言は新しい変数を作成します§)。 - 初期化内の対応する値が
vに代入可能であること、そして - 宣言によって作成される変数が少なくとも1つ存在すること。
この珍しい特性は純粋な実用主義であり、例えば長いif-elseチェーンで単一のerr値を簡単に使用できるようにします。これは頻繁に利用されているのを目にするでしょう。
§ここで注目すべきは、Goでは関数のパラメータと戻り値のスコープは、それらが関数本体を囲むブレースの字句的に外側に現れても、関数本体と同じであるということです。
For
GoのforループはCのそれと似ていますが、同じではありません。forとwhileを統合し、do-whileはありません。3つの形式があり、そのうちセミコロンを持つのは1つだけです。
// 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句でループを管理できます。
for key, value := range oldMap {
newMap[key] = value
}
範囲内の最初の項目(キーまたはインデックス)だけが必要な場合は、2番目の項目を省略します。
for key := range m {
if key.expired() {
delete(m, key)
}
}
範囲内の2番目の項目(値)だけが必要な場合は、ブランク識別子(アンダースコア)を使用して最初の項目を破棄します。
sum := 0
for _, value := range array {
sum += value
}
ブランク識別子には多くの用途があり、後のセクションで説明されています。
文字列の場合、rangeはUTF-8を解析して個々のUnicodeコードポイントを分離するなど、より多くの作業を行います。誤ったエンコーディングは1バイトを消費し、U+FFFDの置換ルーンを生成します。(runeという名前(および関連する組み込み型)は、Goの用語で単一のUnicodeコードポイントを指します。詳細については、言語仕様を参照してください。)次のループは、
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
出力します
character U+65E5 '日' starts at byte position 0 character U+672C '本' starts at byte position 3 character U+FFFD '�' starts at byte position 6 character U+8A9E '語' starts at byte position 7
最後に、Goにはカンマ演算子はなく、++と--は式ではなく文です。したがって、forで複数の変数を実行したい場合は、並列代入を使用する必要があります(ただし、これは++と--を排除します)。
// 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のそれよりも一般的です。式は定数である必要はなく、整数である必要もありません。ケースは上から下に評価され、一致が見つかるまで続けられます。もし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
}
自動的なフォールスルーはありませんが、ケースはカンマ区切りのリストで提示できます。
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
Goでは他のCライクな言語ほど一般的ではありませんが、breakステートメントはswitchを早期に終了させるために使用できます。しかし、場合によっては、switchではなく囲むループから抜け出す必要があり、Goではループにラベルを付けてそのラベルに「break」することで実現できます。この例では両方の使用法を示しています。
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
もちろん、continueステートメントもオプションのラベルを受け入れますが、ループにのみ適用されます。
このセクションを締めくくるために、2つのswitchステートメントを使用するバイトスライスの比較ルーチンを以下に示します。
// Compare returns an integer comparing the two byte slices,
// 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
}
型スイッチ
スイッチは、インターフェース変数の動的型を検出するためにも使用できます。このような型スイッチは、括弧内にtypeキーワードを持つ型アサーションの構文を使用します。スイッチが式内で変数を宣言した場合、その変数は各句で対応する型を持ちます。このような場合、同じ名前を再利用することが慣用的であり、実質的に各ケースで同じ名前ですが異なる型の新しい変数を宣言します。
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
関数
複数の戻り値
Goの珍しい機能の1つは、関数とメソッドが複数の値を返すことができることです。この形式は、Cプログラムにおけるいくつかの不器用なイディオム、例えばEOFに対する-1のような帯域内エラーリターンや、アドレスで渡された引数を変更する、といった問題を改善するために使用できます。
Cでは、書き込みエラーは負のカウントで通知され、エラーコードは揮発性の場所に隠されます。Goでは、Writeはカウントとエラーを返すことができます。「はい、いくつかのバイトは書き込みましたが、デバイスがいっぱいになったためすべてではありませんでした」。osパッケージのファイルの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
}
入力スライスb内の数値を次のようにスキャンするために使用できます。
for i := 0; i < len(b); {
x, i = nextInt(b, 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 error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
Defer
Goのdeferステートメントは、deferを実行している関数が戻る直前に実行される関数呼び出し(遅延関数)をスケジュールします。これは、関数がどのパスを通って戻るかに関わらず、解放されなければならないリソースなどの状況に対処するための珍しいが効果的な方法です。典型的な例は、ミューテックスのロック解除やファイルのクローズです。
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close will run when we're finished.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later.
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here.
}
}
return string(result), nil // f will be closed if we return here.
}
Closeのような関数への呼び出しを遅延させることには2つの利点があります。まず、ファイルのクローズを忘れることが決してないことを保証します。これは、後で新しいリターンパスを追加するために関数を編集した場合に発生しやすい間違いです。次に、クローズがオープンに近い位置に配置されることを意味します。これは、関数の最後に配置するよりもはるかに明確です。
遅延関数の引数(関数がメソッドの場合はレシーバーを含む)は、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) }
// Use them like this:
func a() {
trace("a")
defer untrace("a")
// do something....
}
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は奇妙に思えるかもしれませんが、その最も興味深く強力なアプリケーションは、ブロックベースではなく関数ベースであるという事実から正確に生まれます。panicとrecoverのセクションで、その可能性のもう1つの例を見ることになります。
データ
newによる割り当て
Goには、組み込み関数newとmakeという2つの割り当てプリミティブがあります。これらは異なることを行い、異なる型に適用されるため混乱を招く可能性がありますが、ルールはシンプルです。まずnewについて説明しましょう。これはメモリを割り当てる組み込み関数ですが、他の言語の同名のものとは異なり、メモリを初期化するのではなく、ゼロクリアするだけです。つまり、new(T)は型Tの新しいアイテムに対してゼロクリアされたストレージを割り当て、そのアドレス(型*Tの値)を返します。Goの用語では、新しく割り当てられた型Tのゼロ値へのポインタを返します。
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) // type *SyncedBuffer var v SyncedBuffer // type 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
}
Cとは異なり、ローカル変数のアドレスを返すことは完全に問題ないことに注意してください。変数に関連付けられたストレージは、関数が戻った後も存続します。実際、複合リテラルのアドレスを取得すると、評価されるたびに新しいインスタンスが割り当てられるため、この最後の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)とは異なる目的を果たします。スライス、マップ、チャネルのみを作成し、初期化された(ゼロクリアされていない)型Tの値(*Tではない)を返します。この区別がある理由は、これら3つの型が、内部的に使用前に初期化されなければならないデータ構造への参照を表しているためです。例えば、スライスはデータ(配列内部)、長さ、容量へのポインタを含む3つの項目記述子であり、これらの項目が初期化されるまでスライスはnilです。スライス、マップ、チャネルの場合、makeは内部データ構造を初期化し、使用のために値を準備します。例えば、
make([]int, 10, 100)
は100個のintの配列を割り当て、次に配列の最初の10要素を指す長さ10、容量100のスライス構造を作成します(スライスを作成する際、容量は省略できます。詳細についてはスライスのセクションを参照してください)。対照的に、new([]int)は、新しく割り当てられ、ゼロ値が設定されたスライス構造へのポインタ、つまりnilスライス値へのポインタを返します。
これらの例は、newとmakeの違いを示しています。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints // Unnecessarily complex: var p *[]int = new([]int) *p = make([]int, 100, 100) // Idiomatic: v := make([]int, 100)
makeはマップ、スライス、チャネルにのみ適用され、ポインタを返さないことに注意してください。明示的なポインタを取得するには、newで割り当てるか、変数のアドレスを明示的に取得してください。
配列
配列はメモリの詳細なレイアウトを計画する際に有用であり、時には割り当てを回避するのに役立ちますが、主にそれらはスライス(次のセクションの主題)の構成要素です。そのトピックの基礎を築くために、配列についていくつか説明します。
GoとCの配列の動作には大きな違いがあります。Goでは、
- 配列は値です。ある配列を別の配列に代入すると、すべての要素がコピーされます。
- 特に、配列を関数に渡すと、配列のコピーを受け取り、配列へのポインタではありません。
- 配列のサイズはその型の一部です。型
[10]intと[20]intは異なります。
値プロパティは便利ですが、コストもかかります。Cのような動作と効率が必要な場合は、配列へのポインタを渡すことができます。
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
しかし、このスタイルでさえ慣用的なGoではありません。代わりにスライスを使用してください。
スライス
スライスは配列をラップして、データのシーケンスに対してより汎用的で強力で便利なインターフェースを提供します。変換行列のような明示的な次元を持つ項目を除けば、Goでのほとんどの配列プログラミングは単純な配列ではなくスライスで行われます。
スライスは基になる配列への参照を保持し、あるスライスを別のスライスに代入すると、両方とも同じ配列を参照します。関数がスライス引数を受け取り、スライスの要素を変更すると、呼び出し元にその変更が可視になります。これは基になる配列へのポインタを渡すのと似ています。したがって、Read関数はポインタとカウントではなくスライス引数を受け入れることができます。スライス内の長さは読み取るデータ量の上限を設定します。以下はosパッケージのFile型のReadメソッドのシグネチャです。
func (f *File) Read(buf []byte) (n int, err error)
このメソッドは、読み取ったバイト数と、エラーが発生した場合はエラー値を返します。より大きなバッファbufの最初の32バイトに読み込むには、バッファをスライス(ここでは動詞として使用)します。
n, err := f.Read(buf[0:32])
このようなスライスは一般的で効率的です。実際、効率は一時的に置いておいて、次のスニペットでもバッファの最初の32バイトが読み込まれます。
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
スライスの長さは、基になる配列の制限内に収まる限り変更できます。それ自体をスライスに代入するだけです。組み込み関数capでアクセスできるスライスの容量は、スライスが取り得る最大長を報告します。以下は、スライスにデータを追加する関数です。データが容量を超えると、スライスは再割り当てされます。結果のスライスが返されます。この関数は、lenとcapがnilスライスに適用されたときに合法であり、0を返すという事実を利用しています。
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
Appendはsliceの要素を変更できますが、スライス自体(ポインタ、長さ、容量を保持する実行時データ構造)は値で渡されるため、その後スライスを返す必要があります。
スライスに追加するというアイデアは非常に便利であるため、組み込み関数appendによって捉えられています。ただし、その関数の設計を理解するにはもう少し情報が必要なので、後でそれに立ち戻ります。
二次元スライス
Goの配列とスライスは一次元です。2D配列またはスライスに相当するものを作成するには、次のように配列の配列またはスライスのスライスを定義する必要があります。
type Transform [3][3]float64 // A 3x3 array, really an array of arrays. type LinesOfText [][]byte // A slice of byte slices.
スライスは可変長なので、各内部スライスの長さが異なっていても問題ありません。LinesOfTextの例のように、これは一般的な状況になり得ます。各行は独立した長さを持っています。
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
ピクセルのスキャンラインを処理する場合など、2Dスライスを割り当てる必要がある場合があります。これには2つの方法があります。1つは各スライスを個別に割り当てる方法、もう1つは単一の配列を割り当てて個々のスライスをその中に指す方法です。どちらを使用するかはアプリケーションによります。スライスが拡大または縮小する可能性がある場合、次の行を上書きしないように個別に割り当てる必要があります。そうでない場合は、単一の割り当てでオブジェクトを構築する方が効率的です。参考までに、2つの方法のスケッチを以下に示します。まず、1行ずつです。
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize)
}
そして、今度は1つの割り当てとして、行にスライスします。
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
マップ
マップは、ある型(キー)の値を別の型(要素または値)の値と関連付ける、便利で強力な組み込みデータ構造です。キーは、等価演算子が定義されている任意の型(整数、浮動小数点数、複素数、文字列、ポインタ、インターフェース(動的型が等価性をサポートする限り)、構造体、配列など)にすることができます。スライスは等価性が定義されていないため、マップキーとして使用することはできません。スライスと同様に、マップは基になるデータ構造への参照を保持します。マップの内容を変更するマップを関数に渡すと、変更は呼び出し元に表示されます。
マップは、コロン区切りのキーと値のペアを持つ通常の複合リテラル構文を使用して構築できるため、初期化中に簡単に作成できます。
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が返されます。セットは値の型をboolとするマップとして実装できます。マップエントリをtrueに設定して値をセットに入れ、その後は単純なインデックス付けでテストします。
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}
if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}
時には、存在しないエントリとゼロ値を区別する必要があります。「UTC」のエントリがあるのか、それともマップに全く存在しないために0なのか?複数の代入形式で区別できます。
var seconds int var ok bool seconds, ok = timeZone[tz]
明らかにこれは「カンマok」イディオムと呼ばれます。この例では、tzが存在する場合、secondsは適切に設定され、okはtrueになります。存在しない場合、secondsはゼロに設定され、okはfalseになります。以下は、これを組み合わせて適切なエラー報告を行う関数です。
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
実際の値を気にせずにマップ内の存在を確認するには、値の通常の変数の代わりにブランク識別子 (_) を使用できます。
_, present := timeZone[tz]
マップエントリを削除するには、組み込み関数deleteを使用します。引数はマップと削除するキーです。キーがすでにマップに存在しない場合でも、これを行うのは安全です。
delete(timeZone, "PDT") // Now on Standard Time
出力
Goにおける書式付き出力は、Cのprintfファミリーに似たスタイルを使用しますが、より豊富で一般的です。関数はfmtパッケージにあり、fmt.Printf、fmt.Fprintf、fmt.Sprintfなどの大文字の名前を持っています。文字列関数(Sprintfなど)は、提供されたバッファに書き込むのではなく、文字列を返します。
フォーマット文字列を提供する必要はありません。Printf、Fprintf、Sprintfのそれぞれについて、例えばPrintとPrintlnという別の関数ペアがあります。これらの関数はフォーマット文字列を受け取らず、代わりに各引数にデフォルトのフォーマットを生成します。Printlnバージョンは引数の間に空白を挿入し、出力の最後に改行を追加しますが、Printバージョンは、どちらかのオペランドが文字列でない場合にのみ空白を追加します。この例では、各行が同じ出力を生成します。
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
書式付き出力関数fmt.Fprintなどは、最初の引数としてio.Writerインターフェースを実装する任意のオブジェクトを受け取ります。変数os.Stdoutと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(「value」の略)を使用できます。結果はPrintとPrintlnが生成するものとまったく同じです。さらに、この形式は配列、スライス、構造体、マップなど、あらゆる値を出力できます。前のセクションで定義したタイムゾーンマップの出力ステートメントを以下に示します。
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
出力は次のようになります
map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
マップの場合、Printfとその仲間はキーによって出力が辞書順にソートされます。
構造体を出力する際、修正された書式%+vは構造体のフィールドにその名前を注釈付けし、任意の値に対して代替書式%#vは値を完全なGo構文で出力します。
type T struct {
a int
b float64
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, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
(アンパサンドに注意してください。)引用符付き文字列形式は、型stringまたは[]byteの値に適用される場合、%qでも利用できます。代替形式%#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"
(型Tの値とTへのポインタの両方を出力する必要がある場合、Stringのレシーバーは値型でなければなりません。この例では、構造体型の場合、ポインタの方が効率的で慣用的であるため、ポインタを使用しました。詳細については、ポインタ対値レシーバーに関する以下のセクションを参照してください。)
私たちのStringメソッドはSprintfを呼び出すことができます。これは、出力ルーチンが完全に再入可能であり、このようにラップできるためです。ただし、このアプローチについて理解しておくべき重要な詳細が1つあります。Stringメソッド内でSprintfを呼び出す際に、無限にStringメソッドに再帰するような方法で呼び出さないでください。これは、Sprintfの呼び出しがレシーバーを直接文字列として出力しようとした場合に発生する可能性があり、その結果メソッドが再度呼び出されます。この例が示すように、これはよくある簡単な間違いです。
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}
修正も簡単です。引数を、そのメソッドを持たない基本的な文字列型に変換します。
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}
初期化セクションでは、この再帰を回避する別のテクニックを紹介します。
別の出力手法は、出力ルーチンの引数を直接別のルーチンに渡すことです。Printfのシグネチャは、最後の引数に...interface{}型を使用して、フォーマットの後に任意の数のパラメータ(任意の型)が現れることを指定します。
func Printf(format string, v ...interface{}) (n int, err error) {
関数Printf内では、vは[]interface{}型の変数として振る舞いますが、別の可変長引数関数に渡されると、通常の引数リストのように振る舞います。以下は、上記で使用したlog.Println関数の実装です。これは、実際の書式設定のために引数を直接fmt.Sprintlnに渡しています。
// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string)
}
入れ子になったSprintlnの呼び出しでvの後に...を記述するのは、コンパイラにvを引数リストとして扱うように指示するためです。そうしないと、vは単一のスライス引数として渡されてしまいます。
ここまでに説明した以外にも、出力にはさらに多くのことがあります。詳細については、fmtパッケージのgodocドキュメントを参照してください。
ちなみに、...パラメータは特定の型を持つことができます。例えば、整数のリストから最小値を選択するmin関数の場合は...intとなります。
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
Append
さて、これで組み込み関数appendの設計を説明するために必要だった欠落部分が揃いました。appendのシグネチャは、上記で作成したカスタムAppend関数とは異なります。概略的には、次のようになります。
func append(slice []T, elements ...T) []T
ここでTは任意の型のプレースホルダーです。Goでは、型Tが呼び出し元によって決定される関数を実際に記述することはできません。だからこそappendは組み込み関数なのです。コンパイラのサポートが必要です。
appendは、要素をスライスの末尾に追加し、その結果を返します。手書きのAppendと同様に、基になる配列が変更される可能性があるため、結果を返す必要があります。この単純な例は、
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
[1 2 3 4 5 6]と出力されます。したがって、appendはPrintfのように、任意の数の引数を収集して動作します。
しかし、私たちのAppendがするように、スライスをスライスに追加したい場合はどうでしょうか?簡単です。上記のOutputの呼び出しで行ったように、呼び出しサイトで...を使用します。このスニペットは、上記のコードとまったく同じ出力を生成します。
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
その...がない場合、型が異なるためコンパイルされません。yはint型ではありません。
初期化
CやC++の初期化とは表面上あまり変わりませんが、Goの初期化はより強力です。複雑な構造体を初期化中に構築でき、初期化されたオブジェクト間の順序の問題、さらには異なるパッケージ間での問題も正しく処理されます。
定数
Goの定数は、文字通り定数です。関数内でローカルとして定義されている場合でも、コンパイル時に作成され、数値、文字(ルーン)、文字列、またはブール値のみが可能です。コンパイル時の制限のため、それらを定義する式はコンパイラが評価可能な定数式でなければなりません。例えば、1<<3は定数式ですが、math.Sin(math.Pi/4)は、math.Sinの関数呼び出しが実行時に行われる必要があるため、定数式ではありません。
Goでは、列挙定数はiota列挙子を使用して作成されます。iotaは式の一部になり、式は暗黙的に繰り返されるため、複雑な値のセットを簡単に構築できます。
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
ユーザー定義の型にStringなどのメソッドを添付する機能により、任意の値を自動的に整形して出力することが可能になります。構造体に適用されるのを最もよく見かけますが、このテクニックはByteSizeのような浮動小数点型などのスカラ型にも有用です。
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と表示されます。
ここでSprintfをByteSizeのStringメソッドの実装に使用することは安全です(無限再帰を避けます)。これは変換のためではなく、Sprintfを%fで呼び出しているためです。%fは文字列形式ではないため、Sprintfは文字列が必要な場合にのみStringメソッドを呼び出し、%fは浮動小数点値を求めます。
変数
変数は定数と同様に初期化できますが、初期化子は実行時に計算される一般的な式にすることができます。
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
init関数
最後に、各ソースファイルは、必要な状態を設定するために独自の引数なしのinit関数を定義できます(実際には、各ファイルに複数のinit関数を持つことができます)。そして、「最後に」とは文字通り最後を意味します。initは、パッケージ内のすべての変数宣言が初期化子を評価した後、そしてそれらがインポートされたすべてのパッケージが初期化された後にのみ呼び出されます。
宣言として表現できない初期化の他に、init関数の一般的な用途は、実際の実行が始まる前にプログラムの状態の正確性を検証または修復することです。
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
メソッド
ポインタと値
ByteSizeで見たように、メソッドは任意の名前付き型(ポインタまたはインターフェースを除く)に対して定義できます。レシーバーは構造体である必要はありません。
上記の「スライス」の議論で、私たちはAppend関数を書きました。これをスライスのメソッドとして定義することもできます。これを行うには、まずメソッドをバインドできる名前付き型を宣言し、次にそのメソッドのレシーバーをその型の値にします。
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// Body exactly the same as the Append function defined above.
}
これでもメソッドは更新されたスライスを返す必要があります。この煩雑さを解消するために、メソッドをByteSliceへのポインタをレシーバーとして受け取るように再定義することで、メソッドが呼び出し元のスライスを上書きできるようにします。
func (p *ByteSlice) Append(data []byte) {
slice := *p
// Body as above, without the return.
*p = slice
}
実際、さらに良いことができます。関数を標準的なWriteメソッドのように変更すると、次のようになります。
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// Again as above.
*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を満たすためです。レシーバーのポインタと値に関するルールは、値メソッドはポインタと値の両方で呼び出すことができますが、ポインタメソッドはポインタでのみ呼び出すことができるというものです。
このルールは、ポインタメソッドがレシーバーを変更できるために生じます。値で呼び出すと、メソッドは値のコピーを受け取るため、変更は破棄されます。したがって、言語はこの間違いを許可しません。ただし、便利な例外があります。値がアドレス指定可能である場合、言語は、値に対してポインタメソッドを呼び出す一般的なケースを、アドレス演算子を自動的に挿入することで処理します。この例では、変数bはアドレス指定可能なので、b.WriteだけでそのWriteメソッドを呼び出すことができます。コンパイラはそれを(&b).Writeに書き換えてくれます。
ちなみに、バイトのスライスに対してWriteを使用するというアイデアは、bytes.Bufferの実装の中心にあります。
インターフェースとその他の型
インターフェース
Goのインターフェースは、オブジェクトの振る舞いを指定する方法を提供します。つまり、何かがこれを実行できるなら、ここで使用できるということです。すでにいくつかの簡単な例を見てきました。カスタムプリンタはStringメソッドによって実装でき、FprintfはWriteメソッドを持つものなら何にでも出力できます。Goコードでは、1つまたは2つのメソッドしか持たないインターフェースが一般的であり、通常はメソッドから派生した名前が付けられます。例えば、Writeを実装するものにはio.Writerなどです。
ある型は複数のインターフェースを実装できます。たとえば、コレクションがsort.Interface(Len()、Less(i, j int) bool、Swap(i, j int)を含む)を実装していれば、sortパッケージのルーチンによってソートでき、カスタムフォーマッタを持つこともできます。この作られた例では、Sequenceは両方を満たしています。
type Sequence []int // Methods required by 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] } // Copy returns a copy of the Sequence. func (s Sequence) Copy() Sequence { copy := make(Sequence, 0, len(s)) return append(copy, s...) } // Method for printing - sorts the elements before printing. func (s Sequence) String() string { s = s.Copy() // Make a copy; don't overwrite argument. sort.Sort(s) str := "[" for i, elem := range s { // Loop is O(N²); will fix that in next example. if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" }
変換
SequenceのStringメソッドは、Sprintがスライスに対してすでに実行している作業を再作成しています(また、複雑性はO(N²)であり、劣悪です)。Sprintを呼び出す前にSequenceをプレーンな[]intに変換することで、その作業を共有(そして高速化)できます。
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}
このメソッドは、StringメソッドからSprintfを安全に呼び出すための変換テクニックのもう一つの例です。2つの型(Sequenceと[]int)は、型名を無視すれば同じであるため、それらの間で変換することは合法です。この変換は新しい値を作成するのではなく、単に既存の値が新しい型を持つかのように一時的に振る舞うだけです。(整数から浮動小数点への変換など、新しい値を作成する他の合法的な変換もあります。)
Goプログラムでは、異なる一連のメソッドにアクセスするために式の型を変換するのが慣用的です。例えば、既存の型sort.IntSliceを使用すると、全体の例を次のように短縮できます。
type Sequence []int
// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
これで、Sequenceが複数のインターフェース(ソートと出力)を実装する代わりに、データ項目が複数の型(Sequence、sort.IntSlice、[]int)に変換される機能を使用しています。それぞれがジョブの一部を実行します。これは実際には珍しいですが、効果的です。
インターフェース変換と型アサーション
型スイッチは変換の一種です。インターフェースを受け取り、スイッチ内の各ケースについて、ある意味でそのケースの型に変換します。ここでは、fmt.Printfの内部で型スイッチを使用して値を文字列に変換する方法の簡略版を示します。それがすでに文字列である場合、インターフェースが保持する実際の文字列値が必要であり、Stringメソッドを持っている場合は、そのメソッドを呼び出した結果が必要になります。
type Stringer interface {
String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
最初のケースは具体的な値を見つけ、2番目のケースはインターフェースを別のインターフェースに変換します。このように型を混ぜることは完全に問題ありません。
私たちが関心のある型が1つしかない場合はどうでしょうか?値がstringを保持していることがわかっていて、それを単に抽出したい場合はどうでしょうか?1つのケースの型スイッチでも可能ですが、型アサーションでも可能です。型アサーションはインターフェース値を受け取り、そこから指定された明示的な型の値を抽出します。この構文は、型スイッチを開く句から借用していますが、typeキーワードではなく明示的な型を使用します。
value.(typeName)
結果は、静的型typeNameを持つ新しい値です。その型は、インターフェースが保持する具体的な型であるか、その値に変換できる別のインターフェース型である必要があります。値に含まれているとわかっている文字列を抽出するには、次のように書くことができます。
str := value.(string)
しかし、その値に文字列が含まれていないことが判明した場合、プログラムはランタイムエラーでクラッシュします。それを防ぐために、「カンマOK」イディオムを使用して、値が文字列であるかどうかを安全にテストします。
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
型アサーションが失敗した場合でも、strは存在し、文字列型になりますが、ゼロ値(空の文字列)を持つことになります。
この機能の例として、このセクションの冒頭で説明した型スイッチと同等のif-else文を示します。
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
汎用性
ある型がインターフェースを実装するためだけに存在し、そのインターフェースを超えてエクスポートされるメソッドを持たない場合、型自体をエクスポートする必要はありません。インターフェースのみをエクスポートすることで、その値がインターフェースに記述されている以外の興味深い振る舞いを持たないことが明確になります。また、一般的なメソッドのすべてのインスタンスでドキュメントを繰り返す必要もなくなります。
このような場合、コンストラクタは実装する型ではなく、インターフェース値を返す必要があります。例えば、ハッシュライブラリでは、crc32.NewIEEEとadler32.Newの両方がインターフェース型hash.Hash32を返します。GoプログラムでAdler-32の代わりにCRC-32アルゴリズムを使用するには、コンストラクタの呼び出しを変更するだけでよく、残りのコードはアルゴリズムの変更による影響を受けません。
同様のアプローチにより、様々なcryptoパッケージのストリーミング暗号アルゴリズムを、それらが連結するブロック暗号から分離することができます。crypto/cipherパッケージのBlockインターフェースは、単一のデータブロックの暗号化を提供するブロック暗号の動作を指定します。その後、bufioパッケージと同様に、このインターフェースを実装する暗号パッケージは、ブロック暗号化の詳細を知ることなく、Streamインターフェースで表されるストリーミング暗号を構築するために使用できます。
crypto/cipherインターフェースは次のようになります。
type Block interface {
BlockSize() int
Encrypt(dst, src []byte)
Decrypt(dst, src []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
ここに、ブロック暗号をストリーミング暗号に変換するカウンターモード(CTR)ストリームの定義があります。ブロック暗号の詳細が抽象化されていることに注目してください。
// NewCTR returns a Stream that encrypts/decrypts using the given Block in // counter mode. The length of iv must be the same as the Block's block size. func NewCTR(block Block, iv []byte) Stream
NewCTRは、特定の暗号化アルゴリズムやデータソースだけでなく、Blockインターフェースと任意のStreamのあらゆる実装に適用されます。インターフェース値を返すため、CTR暗号化を他の暗号化モードに置き換えるのは局所的な変更で済みます。コンストラクタ呼び出しは編集する必要がありますが、周囲のコードは結果をStreamとしてのみ扱う必要があるため、違いに気づきません。
インターフェースとメソッド
ほとんどどんなものにもメソッドを付けることができるので、ほとんどどんなものでもインターフェースを満たすことができます。一つの例としてhttpパッケージがあり、ここではHandlerインターフェースが定義されています。Handlerを実装するどんなオブジェクトでもHTTPリクエストを処理できます。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter自体もインターフェースであり、クライアントに応答を返すために必要なメソッドへのアクセスを提供します。これらのメソッドには標準のWriteメソッドが含まれているため、io.Writerが使用できる場所ならどこでもhttp.ResponseWriterを使用できます。Requestは、クライアントからのリクエストの解析された表現を含む構造体です。
簡潔さのため、POSTは無視し、HTTPリクエストは常にGETであると仮定しましょう。この単純化はハンドラのセットアップ方法には影響しません。以下は、ページの訪問回数を数えるハンドラの簡単な実装です。
// Simple counter server.
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
(私たちのテーマに沿って、Fprintfがhttp.ResponseWriterに出力できることに注目してください。)実際のサーバーでは、ctr.nへのアクセスは同時アクセスから保護する必要があるでしょう。提案については、syncおよびatomicパッケージを参照してください。
参考までに、このようなサーバーをURLツリーのノードにアタッチする方法を次に示します。
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
しかし、なぜCounterを構造体にするのでしょうか?必要なのは整数だけです。(インクリメントが呼び出し元に可視であるためには、レシーバーはポインタである必要があります。)
// Simpler counter server.
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
プログラムに、ページが訪問されたことを通知する必要がある内部状態がある場合はどうでしょうか?チャネルをウェブページに結び付けます。
// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
最後に、サーバーバイナリを呼び出すときに使用された引数を/argsに表示したいとしましょう。引数を出力する関数を書くのは簡単です。
func ArgServer() {
fmt.Println(os.Args)
}
それをHTTPサーバーに変換するにはどうすればよいでしょうか?ArgServerを、その値を無視する何らかの型のメソッドにすることもできますが、もっとクリーンな方法があります。ポインタとインターフェースを除くすべての型に対してメソッドを定義できるので、関数に対してメソッドを記述できます。httpパッケージにはこのコードが含まれています。
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
HandlerFuncはServeHTTPメソッドを持つ型であるため、その型の値はHTTPリクエストを処理できます。メソッドの実装を見てください。レシーバーは関数fであり、メソッドはfを呼び出します。これは奇妙に思えるかもしれませんが、例えばレシーバーがチャネルで、メソッドがチャネルで送信するのとそれほど変わりません。
ArgServerをHTTPサーバーにするには、まず適切なシグネチャを持つように変更します。
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
ArgServerはHandlerFuncと同じシグネチャを持つため、SequenceをIntSliceに変換してIntSlice.Sortにアクセスしたのと同様に、そのメソッドにアクセスするためにその型に変換できます。設定コードは簡潔です。
http.Handle("/args", http.HandlerFunc(ArgServer))
誰かが/argsページを訪れると、そのページにインストールされたハンドラは値ArgServer、型HandlerFuncを持ちます。HTTPサーバーはその型のメソッドServeHTTPを呼び出し、ArgServerをレシーバーとして、それが今度はArgServerを呼び出します(HandlerFunc.ServeHTTP内のf(w, req)の呼び出しを介して)。その後、引数が表示されます。
このセクションでは、構造体、整数、チャネル、および関数からHTTPサーバーを作成しました。これは、インターフェースが単なるメソッドの集合であり、(ほぼ)あらゆる型に対して定義できるためです。
ブランク識別子
これまで、for rangeループやマップの文脈で、ブランク識別子に何度か言及してきました。ブランク識別子は、任意の型の任意の値で代入または宣言でき、その値は無害に破棄されます。Unixの/dev/nullファイルに書き込むのに少し似ています。これは、変数が必要だが実際の値は関係ない場合にプレースホルダーとして使用される書き込み専用の値を表します。すでに見たもの以外にも用途があります。
多重代入におけるブランク識別子
for rangeループにおけるブランク識別子の使用は、一般的な状況である多重代入の特殊なケースです。
代入の左側に複数の値が必要だが、そのうちの1つがプログラムで使用されない場合、代入の左側にブランク識別子を使用すると、ダミー変数を作成する必要がなくなり、その値が破棄されることを明確に示します。例えば、値とエラーを返す関数を呼び出すが、エラーのみが重要な場合、ブランク識別子を使用して不要な値を破棄します。
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
時折、エラーを無視するためにエラー値を破棄するコードが見られますが、これはひどい習慣です。エラーリターンは理由があって提供されているので、常にチェックしてください。
// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}
未使用のインポートと変数
パッケージをインポートしても使用しないことや、変数を宣言しても使用しないことはエラーです。未使用のインポートはプログラムを肥大化させ、コンパイルを遅くし、初期化されたが使用されない変数は少なくとも計算の無駄であり、より大きなバグの兆候である可能性があります。しかし、プログラムが活発に開発されている場合、未使用のインポートや変数が頻繁に発生し、コンパイルを進めるためだけにそれらを削除し、後で再び必要になるのは煩わしい場合があります。ブランク識別子は回避策を提供します。
この途中で書かれたプログラムには、2つの未使用のインポート(fmtとio)と1つの未使用変数(fd)があるため、コンパイルされませんが、ここまでのコードが正しいかどうかを確認したい場合があります。
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
}
未使用のインポートに関する苦情を黙らせるには、ブランク識別子を使用してインポートされたパッケージのシンボルを参照します。同様に、未使用の変数fdをブランク識別子に代入すると、未使用変数エラーが黙ります。このバージョンのプログラムはコンパイルされます。
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader // For debugging; delete when done.
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}
慣習として、インポートエラーを黙らせるためのグローバル宣言は、インポートの直後に配置し、見つけやすくするためと、後でクリーンアップすることを思い出させるためにコメントを付けるべきです。
副作用のためのインポート
前の例のfmtやioのような未使用のインポートは、最終的に使用されるか削除されるべきです。空白の代入は、コードが開発中であることを示します。しかし、明示的な使用なしに、副作用のためだけにパッケージをインポートすることが有用な場合があります。たとえば、init関数の中で、net/http/pprofパッケージはデバッグ情報を提供するHTTPハンドラを登録します。エクスポートされたAPIがありますが、ほとんどのクライアントはハンドラ登録のみを必要とし、ウェブページを介してデータにアクセスします。副作用のためだけにパッケージをインポートするには、パッケージ名をブランク識別子に変更します。
import _ "net/http/pprof"
このインポート形式は、パッケージが副作用のためにインポートされていることを明確に示しています。というのも、このファイルではそのパッケージに名前がないため、他にパッケージを使用する方法がないからです(もし名前があり、その名前を使用しなかった場合、コンパイラはプログラムを拒否します)。
インターフェースのチェック
上記のインターフェースの議論で見たように、型はインターフェースを実装することを明示的に宣言する必要はありません。代わりに、型はインターフェースのメソッドを実装するだけでインターフェースを実装します。実際には、ほとんどのインターフェース変換は静的であり、コンパイル時にチェックされます。例えば、*os.Fileをio.Readerを期待する関数に渡すと、*os.Fileがio.Readerインターフェースを実装していない限り、コンパイルされません。
ただし、いくつかのインターフェースチェックは実行時に行われます。その一例がencoding/jsonパッケージです。このパッケージはMarshalerインターフェースを定義しています。JSONエンコーダは、このインターフェースを実装する値を受け取ると、標準の変換を行う代わりに、その値のマーシャリングメソッドを呼び出してJSONに変換します。エンコーダは、型アサーションのような方法で、このプロパティを実行時にチェックします。
m, ok := val.(json.Marshaler)
型がインターフェースを実装しているかどうかだけを尋ねる必要がある場合、おそらくエラーチェックの一部としてインターフェース自体を使用することなく、ブランク識別子を使用して型アサートされた値を無視します。
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
この状況が発生する1つの場所は、型を実装するパッケージ内で、その型が実際にインターフェースを満たしていることを保証する必要がある場合です。例えば、json.RawMessageのような型がカスタムのJSON表現を必要とする場合、json.Marshalerを実装すべきですが、コンパイラがこれを自動的に検証するような静的な変換はありません。もし型が誤ってインターフェースを満たさない場合でも、JSONエンコーダは機能しますが、カスタム実装は使用されません。実装が正しいことを保証するために、パッケージ内でブランク識別子を使用したグローバル宣言を使用できます。
var _ json.Marshaler = (*RawMessage)(nil)
この宣言では、*RawMessageをMarshalerに変換する代入は、*RawMessageがMarshalerを実装していることを要求し、そのプロパティはコンパイル時にチェックされます。もしjson.Marshalerインターフェースが変更された場合、このパッケージはコンパイルできなくなり、更新が必要であることが通知されます。
この構成におけるブランク識別子の出現は、宣言が型チェックのためだけに存在し、変数を生成するためではないことを示しています。ただし、インターフェースを満たすすべての型に対してこれを行わないでください。慣習として、このような宣言は、コードに既存の静的変換がない場合にのみ使用され、これはまれなケースです。
埋め込み
Goには典型的な、型駆動のサブクラス化という概念はありませんが、構造体やインターフェース内に型を埋め込むことで、実装の一部を「借りる」能力があります。
インターフェースの埋め込みは非常にシンプルです。以前にio.Readerとio.Writerインターフェースについて言及しました。以下にそれらの定義を示します。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
io パッケージは、いくつかのメソッドを実装できるオブジェクトを指定する他のいくつかのインターフェースもエクスポートします。たとえば、io.ReadWriter は Read と Write の両方を含むインターフェースです。io.ReadWriter は、2つのメソッドを明示的に列挙することで指定できますが、次のように2つのインターフェースを埋め込んで新しいインターフェースを形成する方が簡単で、より分かりやすいです。
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
これは見た目どおりの意味です。ReadWriter は Reader が行うことと Writer が行うことの両方を行うことができます。つまり、埋め込まれたインターフェースの結合です。インターフェース内に埋め込めるのはインターフェースだけです。
同じ基本的な考え方は構造体にも適用されますが、より広範囲にわたる影響があります。bufio パッケージには bufio.Reader と bufio.Writer の2つの構造体型があり、それぞれもちろん io パッケージのアナログなインターフェースを実装しています。そして bufio はバッファリングされたリーダー/ライターも実装しており、それはリーダーとライターを埋め込みによって1つの構造体に結合することで実現しています。つまり、構造体内に型を列挙するだけで、フィールド名を与えません。
// ReadWriter stores pointers to a Reader and a Writer.
// It implements 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 error) {
return rw.reader.Read(p)
}
構造体を直接埋め込むことで、この手間を省きます。埋め込み型のメソッドは無料で利用できるため、bufio.ReadWriter は bufio.Reader と bufio.Writer のメソッドを持つだけでなく、io.Reader、io.Writer、および io.ReadWriter の3つのインターフェースすべてを満たします。
埋め込みがサブクラス化と異なる重要な点があります。型を埋め込むとき、その型のメソッドは外側の型のメソッドになりますが、それらが呼び出されるときのメソッドのレシーバーは外側の型ではなく内側の型です。この例では、bufio.ReadWriter の Read メソッドが呼び出された場合、上記で書かれた転送メソッドと全く同じ効果を持ちます。レシーバーは ReadWriter 自体ではなく、ReadWriter の reader フィールドです。
埋め込みは単なる便宜である場合もあります。この例は、通常の名前付きフィールドと並んで埋め込みフィールドを示しています。
type Job struct {
Command string
*log.Logger
}
Job 型は、*log.Logger の Print、Printf、Println、およびその他のメソッドを持つようになりました。もちろん、Logger にフィールド名を与えることもできましたが、そうする必要はありません。そして、初期化されると、Job にログを記録できるようになります。
job.Println("starting now...")
Logger は Job 構造体の通常のフィールドなので、次のように Job のコンストラクタ内で通常の方法で初期化できます。
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
または複合リテラルを使って。
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
埋め込みフィールドを直接参照する必要がある場合、パッケージ修飾子を無視したフィールドの型名がフィールド名として機能します。これは、ReadWriter 構造体の Read メソッドで行ったようにです。ここでは、Job 変数 job の *log.Logger にアクセスする必要がある場合、job.Logger と記述します。これは、Logger のメソッドを洗練させたい場合に役立ちます。
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
型の埋め込みは名前の衝突の問題を引き起こしますが、その解決ルールは単純です。まず、フィールドまたはメソッド X は、型のより深くネストされた部分にある他のすべてのアイテム X を隠します。もし log.Logger が Command と呼ばれるフィールドまたはメソッドを含んでいた場合、Job の Command フィールドがそれを支配します。
次に、同じ名前が同じネストレベルで出現する場合、それは通常エラーです。もし Job 構造体が Logger と呼ばれる別のフィールドまたはメソッドを含んでいた場合、log.Logger を埋め込むことは誤りとなります。ただし、重複する名前が型定義の外部でプログラム内で一度も言及されない場合は問題ありません。この条件は、外部から埋め込まれた型に加えられた変更に対する保護を提供します。あるフィールドが別のサブタイプの別のフィールドと衝突しても、どちらのフィールドも使用されない限り問題ありません。
並行処理
コミュニケーションによる共有
並行プログラミングは大きなトピックであり、ここでは Go 特有のハイライトのみを扱います。
多くの環境での並行プログラミングは、共有変数への正しいアクセスを実装するために必要な微妙な点によって困難になります。Go は、共有される値がチャネル上でやり取りされ、実際には実行中の別々のスレッドによってアクティブに共有されることはないという異なるアプローチを推奨しています。任意の時点で値にアクセスできるのは1つのゴルーチンだけです。データ競合は、設計上発生しません。この考え方を奨励するために、私たちはそれをスローガンにまとめました。
メモリを共有して通信するのではなく、通信によってメモリを共有する。
このアプローチは行き過ぎる可能性があります。たとえば、参照カウントは整数変数にミューテックスをかけるのが最善かもしれません。しかし、高レベルのアプローチとして、アクセス制御にチャネルを使用すると、明確で正しいプログラムをより簡単に書くことができます。
このモデルを考える一つの方法は、一つのCPUで実行される典型的なシングルスレッドプログラムを想像することです。それには同期プリミティブは必要ありません。次に、そのようなインスタンスをもう一つ実行します。それも同期は必要ありません。次に、それら二つが通信するようにします。もし通信が同期器であるならば、他の同期はやはり必要ありません。例えば、Unix パイプはこのモデルに完璧に適合します。Go の並行性へのアプローチは Hoare の Communicating Sequential Processes (CSP) に由来しますが、Unix パイプの型安全な一般化と見ることもできます。
ゴルーチン
既存の用語(スレッド、コルーチン、プロセスなど)が不正確な意味合いを持つため、これらは「ゴルーチン」と呼ばれます。ゴルーチンにはシンプルなモデルがあります。それは、同じアドレス空間内で他のゴルーチンと並行して実行される関数です。軽量で、スタック領域の割り当て以上のコストはほとんどかかりません。スタックは小さく始まるため安価であり、必要に応じてヒープストレージを割り当て(解放し)て成長します。
ゴルーチンは複数の OS スレッドに多重化されるため、I/O 待ちなどでブロックしても、他のゴルーチンは実行を継続します。その設計は、スレッドの作成と管理の多くの複雑さを隠蔽します。
新しいゴルーチンで呼び出しを実行するには、関数またはメソッド呼び出しの前に go キーワードを付けます。呼び出しが完了すると、ゴルーチンは黙って終了します。(これは、Unix シェルのコマンドをバックグラウンドで実行するための & 表記に似た効果があります。)
go list.Sort() // run list.Sort concurrently; don't wait for it.
関数リテラルはゴルーチン呼び出しで便利です。
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // Note the parentheses - must call the function.
}
Go では、関数リテラルはクロージャです。実装は、関数が参照する変数がアクティブである限り存続することを保証します。
これらの例は、関数が完了を通知する手段がないため、あまり実用的ではありません。そのためにはチャネルが必要です。
チャネル
マップと同様に、チャネルは make で割り当てられ、結果の値は基になるデータ構造への参照として機能します。オプションの整数パラメータが指定された場合、それはチャネルのバッファサイズを設定します。デフォルトはゼロで、バッファなしまたは同期チャネル用です。
ci := make(chan int) // unbuffered channel of integers cj := make(chan int, 0) // unbuffered channel of integers cs := make(chan *os.File, 100) // buffered channel of pointers to Files
バッファなしチャネルは、通信(値の交換)と同期(2つの計算(ゴルーチン)が既知の状態にあることを保証)を組み合わせます。
チャネルを使った素敵なイディオムはたくさんあります。これは最初の一歩となるものです。前のセクションでは、バックグラウンドでソートを起動しました。チャネルを使用すると、起動元のゴルーチンがソートの完了を待つことができます。
c := make(chan int) // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
list.Sort()
c <- 1 // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c // Wait for sort to finish; discard sent value.
受信側は、受信するデータがあるまで常にブロックします。チャネルがバッファなしの場合、送信側は受信側が値を受け取るまでブロックします。チャネルにバッファがある場合、送信側は値がバッファにコピーされるまでのみブロックします。バッファが満杯の場合、これは、何らかの受信側が値を取得するまで待機することを意味します。
バッファ付きチャネルは、たとえばスループットを制限するためにセマフォのように使用できます。この例では、受信リクエストが handle に渡され、handle はチャネルに値を送信し、リクエストを処理し、その後チャネルから値を受信して「セマフォ」を次のコンシューマーのために準備します。チャネルバッファの容量は、process への同時呼び出し数を制限します。
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // Wait for active queue to drain.
process(r) // May take a long time.
<-sem // Done; enable next request to run.
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Don't wait for handle to finish.
}
}
MaxOutstanding ハンドラが process を実行している場合、それ以上は満たされたチャネルバッファへの送信を試みてブロックします。既存のハンドラの1つが終了してバッファから受信するまで。
この設計には問題があります。Serve は、すべての受信リクエストに対して新しいゴルーチンを作成しますが、一度に実行できるのは MaxOutstanding のみです。その結果、リクエストが速すぎると、プログラムは無制限のリソースを消費する可能性があります。ゴルーチンの作成をゲートするように Serve を変更することで、この欠点を解決できます。
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
(Go のバージョン 1.22 より前では、このコードにはバグがあります。ループ変数がすべてのゴルーチンで共有されます。詳細は Go wiki を参照してください。)
リソースをうまく管理するもう1つのアプローチは、リクエストチャネルから読み取る固定数の handle ゴルーチンを開始することです。ゴルーチンの数は、process への同時呼び出しの数を制限します。この Serve 関数は、終了を指示されるチャネルも受け入れます。ゴルーチンを起動した後、そのチャネルからの受信をブロックします。
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Start handlers
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Wait to be told to exit.
}
チャネルのチャネル
Go の最も重要なプロパティの1つは、チャネルが他のあらゆるものと同様に割り当てられ、やり取りできる第一級の値であるということです。このプロパティの一般的な使用法は、安全で並行なデマルチプレックスを実装することです。
前のセクションの例では、handle はリクエストの理想化されたハンドラでしたが、それが処理する型を定義しませんでした。その型が返信用チャネルを含む場合、各クライアントは応答のための独自のパスを提供できます。型 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)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)
サーバー側では、ハンドラ関数のみが変更されます。
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
これを現実的にするためには、明らかにやるべきことがもっとたくさんありますが、このコードはレート制限された並行非ブロッキングRPCシステムのフレームワークであり、ミューテックスは全くありません。
並列化
これらのアイデアの別の応用は、複数の CPU コアにわたる計算を並列化することです。計算が独立して実行できる個別の部分に分割できる場合、各部分が完了したときに通知するためのチャネルを使用して並列化できます。
項目ベクトルの費用のかかる操作を実行する必要があり、各項目に対する操作の値が独立しているとします。この理想化された例のように。
type Vector []float64
// Apply the operation to v[i], v[i+1] ... up to 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 // signal that this piece is done
}
ループ内で、CPUごとに1つずつ、各部分を個別に起動します。それらは任意の順序で完了できますが、問題ありません。すべてのゴルーチンを起動した後、チャネルを使い切ることで完了信号を数えるだけです。
const numCPU = 4 // number of CPU cores
func (v Vector) DoAll(u Vector) {
c := make(chan int, numCPU) // Buffering optional but sensible.
for i := 0; i < numCPU; i++ {
go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
}
// Drain the channel.
for i := 0; i < numCPU; i++ {
<-c // wait for one task to complete
}
// All done.
}
numCPU の定数値を作成する代わりに、実行時に適切な値を問い合わせることができます。関数 runtime.NumCPU は、マシンのハードウェア CPU コアの数を返すため、次のように書くことができます。
var numCPU = runtime.NumCPU()
また、runtime.GOMAXPROCS という関数もあり、Go プログラムが同時に実行できるユーザー指定のコア数を報告(または設定)します。デフォルトは runtime.NumCPU の値ですが、同じ名前のシェル環境変数を設定するか、正の数で関数を呼び出すことで上書きできます。0 で呼び出すと値がクエリされるだけです。したがって、ユーザーのリソース要求を尊重したい場合は、次のように書く必要があります。
var numCPU = runtime.GOMAXPROCS(0)
並行性(プログラムを独立して実行されるコンポーネントとして構成すること)と並列性(複数の CPU で効率的に計算を実行すること)のアイデアを混同しないように注意してください。Go の並行機能は、一部の問題を並列計算として簡単に構成できるようにしますが、Go は並行言語であり、並列言語ではありません。すべての並列化問題が Go のモデルに適合するわけではありません。この区別の詳細については、このブログ投稿で引用されている講演を参照してください。
リーキーバッファ
並行プログラミングのツールは、非並行的なアイデアさえも表現しやすくします。これは、RPC パッケージから抽象化された例です。クライアントのゴルーチンは、ネットワークなどからデータを受信するループを実行します。バッファの割り当てと解放を避けるために、フリーリストを保持し、それを表現するためにバッファ付きチャネルを使用します。チャネルが空の場合、新しいバッファが割り当てられます。メッセージバッファの準備が整うと、serverChan を介してサーバーに送信されます。
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)
func client() {
for {
var b *Buffer
// Grab a buffer if available; allocate if not.
select {
case b = <-freeList:
// Got one; nothing more to do.
default:
// None free, so allocate a new one.
b = new(Buffer)
}
load(b) // Read next message from the net.
serverChan <- b // Send to server.
}
}
サーバーのループは、クライアントからの各メッセージを受信し、処理し、バッファをフリーリストに戻します。
func server() {
for {
b := <-serverChan // Wait for work.
process(b)
// Reuse buffer if there's room.
select {
case freeList <- b:
// Buffer on free list; nothing more to do.
default:
// Free list full, just carry on.
}
}
}
クライアントは freeList からバッファを取得しようとします。利用可能なバッファがない場合、新しいバッファを割り当てます。サーバーの freeList への送信は、リストが満杯でない限り b をフリーリストに戻します。リストが満杯の場合、バッファは破棄され、ガベージコレクタによって回収されます。(select ステートメントの default 句は、他のケースが準備できていないときに実行されます。つまり、select は決してブロックしません。)この実装は、バッファ付きチャネルとガベージコレクタに依存して、わずか数行でリーキーバケットフリーリストを構築します。
エラー
ライブラリルーチンは、呼び出し元に何らかのエラー指示を返す必要があることがよくあります。前述のように、Go の多値戻り値は、通常の戻り値とともに詳細なエラー記述を返すことを容易にします。この機能を使用して詳細なエラー情報を提供するのは良いスタイルです。たとえば、後で説明するように、os.Open は失敗時に単に nil ポインタを返すだけでなく、何がうまくいかなかったかを記述するエラー値も返します。
慣例により、エラーは error 型を持ち、これは単純な組み込みインターフェースです。
type error interface {
Error() string
}
ライブラリ開発者は、このインターフェースを舞台裏でより豊かなモデルで自由に実装でき、エラーを見るだけでなく、何らかのコンテキストを提供することも可能にします。前述のとおり、通常の *os.File 戻り値とともに、os.Open はエラー値も返します。ファイルが正常に開かれた場合、エラーは nil になりますが、問題がある場合は os.PathError を保持します。
// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
PathError の Error は次のような文字列を生成します。
open /etc/passwx: no such file or directory
このようなエラー(問題のあるファイル名、操作、トリガーされたオペレーティングシステムエラーを含む)は、それを引き起こした呼び出しから遠く離れて印刷されても役立ちます。それは単純な「そのようなファイルやディレクトリはありません」よりもはるかに有益です。
可能であれば、エラー文字列は、エラーを生成した操作やパッケージの名前をプレフィックスとして付けるなどして、その発生元を特定する必要があります。たとえば、image パッケージでは、不明な形式によるデコードエラーの文字列表現は「image: unknown format」です。
正確なエラーの詳細を気にする呼び出し元は、型スイッチまたは型アサーションを使用して特定のエラーを探し、詳細を抽出できます。PathErrors の場合、回復可能な失敗については内部の Err フィールドを調べることが含まれる場合があります。
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Recover some space.
continue
}
return
}
ここでの2番目の if ステートメントは、もう1つの 型アサーション です。失敗した場合、ok は false になり、e は nil になります。成功した場合、ok は true になり、エラーが *os.PathError 型であったことを意味します。そして e もその型になり、エラーに関する詳細情報を調べることができます。
パニック
呼び出し元にエラーを報告する通常のやり方は、error を追加の戻り値として返すことです。標準的な Read メソッドはよく知られた例です。それはバイト数と error を返します。しかし、エラーが回復不能な場合はどうでしょうか?プログラムが単純に続行できない場合があります。
この目的のために、組み込み関数 panic があり、実際にはプログラムを停止させる実行時エラーを発生させます(ただし、次のセクションを参照)。この関数は任意の型(しばしば文字列)の単一の引数を取り、プログラムが終了するときに表示されます。これは、無限ループから抜け出すなど、不可能なことが起こったことを示す方法でもあります。
// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
z := x/3 // Arbitrary initial value
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// A million iterations has not converged; something is wrong.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
これは単なる例ですが、実際のライブラリ関数は panic を避けるべきです。問題が隠蔽されたり、回避できるのであれば、プログラム全体を停止させるよりも、実行を継続させる方が常に良いです。ただし、初期化中は例外となる可能性があります。もしライブラリが本当に自分自身を設定できない場合、いわばパニックを起こすことは合理的かもしれません。
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
復旧
スライスが範囲外である場合や型アサーションに失敗した場合などのランタイムエラーのために暗黙的に呼び出される場合を含め、panic が呼び出されると、現在の関数の実行を直ちに停止し、ゴルーチンのスタックを巻き戻し始め、途中で遅延関数を実行します。その巻き戻しがゴルーチンのスタックの最上位に達すると、プログラムは終了します。ただし、組み込み関数 recover を使用して、ゴルーチンの制御を取り戻し、通常の実行を再開することが可能です。
recover の呼び出しは巻き戻しを停止し、panic に渡された引数を返します。巻き戻し中に実行されるコードは遅延関数の内部のみであるため、recover は遅延関数内部でのみ役立ちます。
recover の応用の一つは、サーバー内で失敗したゴルーチンを、他の実行中のゴルーチンを停止させることなくシャットダウンすることです。
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
この例では、do(work) がパニックを起こした場合、結果がログに記録され、ゴルーチンは他のゴルーチンを妨げることなくきれいに終了します。遅延クロージャで他に何かをする必要はありません。recover を呼び出すだけで条件を完全に処理します。
recover は、遅延関数から直接呼び出されない限り常に nil を返すため、遅延コードは、それ自体が panic と recover を使用するライブラリルーチンを呼び出しても、失敗することはありません。例として、safelyDo の遅延関数は recover を呼び出す前にロギング関数を呼び出すことができ、そのロギングコードはパニック状態の影響を受けずに実行されます。
回復パターンを適用することで、do 関数(およびそれが呼び出すものすべて)は、panic を呼び出すことによって、どんな悪い状況からもきれいに抜け出すことができます。このアイデアを利用して、複雑なソフトウェアのエラー処理を簡素化できます。ローカルなエラー型で panic を呼び出すことによって解析エラーを報告する、理想化された regexp パッケージを見てみましょう。以下は Error の定義、error メソッド、および Compile 関数です。
// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
return string(e)
}
// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse will panic if there is a parse error.
defer func() {
if e := recover(); e != nil {
regexp = nil // Clear return value.
err = e.(Error) // Will re-panic if not a parse error.
}
}()
return regexp.doParse(str), nil
}
もし doParse がパニックを起こした場合、回復ブロックは戻り値を nil に設定します(遅延関数は名前付き戻り値を変更できます)。次に、err への代入で、問題がローカル型 Error であることをアサートすることによって、それが解析エラーであったかどうかをチェックします。そうでない場合、型アサーションは失敗し、何も中断しなかったかのようにスタックの巻き戻しを続けるランタイムエラーを引き起こします。このチェックは、配列の範囲外アクセスなど、予期せぬことが起こった場合でも、解析エラーを処理するために panic と recover を使用しているにもかかわらず、コードが失敗することを意味します。
エラー処理が整備されている場合、error メソッド(型にバインドされたメソッドであるため、組み込みの error 型と同じ名前でも問題なく、むしろ自然です)を使用すると、解析スタックを手動で巻き戻すことを心配することなく、解析エラーを簡単に報告できます。
if pos == 0 {
re.error("'*' illegal at start of expression")
}
このパターンは有用ですが、パッケージ内でのみ使用すべきです。Parse はその内部の panic 呼び出しを error 値に変換します。クライアントに panic を公開しません。これは従うべき良いルールです。
ちなみに、この再パニックイディオムは、実際のエラーが発生した場合にパニック値を変更します。ただし、元の失敗と新しい失敗の両方がクラッシュレポートに表示されるため、問題の根本原因は引き続き確認できます。したがって、この単純な再パニックアプローチで通常は十分です(結局クラッシュですから)が、元の値のみを表示したい場合は、予期しない問題をフィルタリングして元のエラーで再パニックするコードをもう少し書くことができます。それは読者への課題とします。
ウェブサーバー
最後に、Go プログラム全体のウェブサーバーを紹介します。これは実際には一種のウェブ再サーバーです。Google は chart.apis.google.com でデータからグラフやチャートを自動的に整形するサービスを提供しています。しかし、データを URL にクエリとして含める必要があるため、対話的に使用するのは困難です。ここで紹介するプログラムは、ある形式のデータに対してより良いインターフェースを提供します。短いテキスト片が与えられると、チャートサーバーを呼び出して QR コード(テキストをエンコードするボックスの行列)を生成します。この画像を携帯電話のカメラでキャプチャして、たとえば URL として解釈することで、URL を携帯電話の小さなキーボードに入力する手間を省くことができます。
これが完全なプログラムです。説明が続きます。
package main
import (
"flag"
"html/template"
"log"
"net/http"
)
var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18
var templ = template.Must(template.New("qr").Parse(templateStr))
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func QR(w http.ResponseWriter, req *http.Request) {
templ.Execute(w, req.FormValue("s"))
}
const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<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 までの部分は簡単に理解できるはずです。1つのフラグは、サーバーのデフォルトの HTTP ポートを設定します。テンプレート変数 templ は、面白い部分です。これは、サーバーがページを表示するために実行する HTML テンプレートを構築します。これについては後ほど詳しく説明します。
main 関数はフラグを解析し、上で説明したメカニズムを使用して、関数 QR をサーバーのルートパスにバインドします。次に http.ListenAndServe が呼び出されてサーバーが起動され、サーバーの実行中はブロックされます。
QR は、フォームデータを含むリクエストを受け取り、フォーム値 s のデータに対してテンプレートを実行するだけです。
テンプレートパッケージ html/template は強力です。このプログラムは、その機能の一部に触れるだけです。本質的に、templ.Execute に渡されたデータ項目(この場合、フォーム値)から派生した要素を置換することによって、HTML テキストの一部をオンザフライで書き換えます。テンプレートテキスト(templateStr)内の二重波括弧で囲まれた部分は、テンプレートアクションを示します。{{if .}} から {{end}} までの部分は、現在のデータ項目(. (ドット) と呼ばれる)の値が空でない場合にのみ実行されます。つまり、文字列が空の場合、テンプレートのこの部分は抑制されます。
2つのスニペット {{.}} は、テンプレートに提示されたデータ(クエリ文字列)をウェブページに表示するように指示します。HTML テンプレートパッケージは、テキストが安全に表示されるように適切なエスケープを自動的に提供します。
テンプレート文字列の残りの部分は、ページが読み込まれたときに表示される HTML です。もしこの説明が急ぎすぎていると感じる場合は、より詳細な議論についてはテンプレートパッケージのドキュメントを参照してください。
これで、いくつかのコードとデータ駆動型の HTML テキストで、便利なウェブサーバーが完成しました。Go は、わずかな行数で多くのことを実現できるほど強力です。