Effective Go
はじめに¶
Goは新しい言語です。既存の言語からアイデアを借用していますが、効果的なGoプログラムを、関連言語で記述されたプログラムとは異なる特性を持つものにする、独特の特性があります。C++またはJavaプログラムをGoにそのまま翻訳しても、満足のいく結果は得られない可能性が高いです。JavaプログラムはJavaで記述されており、Goではありません。一方、Goの観点から問題を検討することで、成功する可能性のある、しかし全く異なるプログラムを作成できる可能性があります。つまり、Goをうまく記述するには、その特性とイディオムを理解することが重要です。命名、フォーマット、プログラムの構成など、Goでのプログラミングに関する確立された慣習を知ることも重要です。そうすることで、あなたが書くプログラムは他のGoプログラマーにとって理解しやすくなります。
このドキュメントは、明確で慣習的なGoコードを記述するためのヒントを示しています。これは、言語仕様、Goツアー、およびGoコードの書き方を補足するものであり、これらすべてを最初に読む必要があります。
2022年1月追記:このドキュメントは2009年のGoリリースのために書かれたものであり、それ以降は大幅に更新されていません。言語自体の使用方法を理解するための優れたガイドではありますが、言語の安定性のおかげで、ライブラリについてはほとんど触れられておらず、ビルドシステム、テスト、モジュール、多相性など、記述されて以来のGoエコシステムの大きな変更については何も触れられていません。多くのことが起こっており、膨大で増加し続けるドキュメント、ブログ、書籍が最新のGoの使い方をうまく説明しているので、更新する予定はありません。「Effective Go」は引き続き役立ちますが、読者はそれが完全なガイドからは程遠いものであることを理解する必要があります。issue 28782 を参照してください。
例¶
Goパッケージソースは、コアライブラリとしてだけでなく、言語の使用方法の例としても機能することを目的としています。さらに、多くのパッケージには、go.devウェブサイトから直接実行できる、動作する独立した実行可能な例が含まれています。例えばこれなどです(必要に応じて、「例」という単語をクリックして開きます)。問題への対処方法や、何かがどのように実装される可能性があるかについて質問がある場合、ライブラリのドキュメント、コード、および例が回答、アイデア、および背景を提供できます。
フォーマット¶
フォーマットの問題は最も論争の的になりますが、最も重要ではありません。人々は異なるフォーマットスタイルに適応できますが、適応する必要がない方が良く、誰もが同じスタイルに従えば、このトピックに費やされる時間が少なくなります。問題は、長い規定のスタイルガイドなしで、このユートピアにどのようにアプローチするかです。
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ドキュメントコメント」を参照してください。
名前¶
名前は、Goでも他の言語と同様に重要です。それらはセマンティックな効果さえあります。パッケージ外の名前の可視性は、最初の文字が大文字かどうかによって決まります。したがって、Goプログラムの命名規則について少し時間を費やす価値があります。
パッケージ名¶
パッケージがインポートされると、パッケージ名はコンテンツのアクセサーになります。以下のように
import "bytes"
インポートパッケージはbytes.Buffer
について話すことができます。パッケージを使用するすべての人が、そのコンテンツを参照するために同じ名前を使用できることは役に立ちます。これは、パッケージ名は適切である必要があることを意味します。短く、簡潔で、呼び起こしやすい名前です。慣例により、パッケージには小文字の1語の名前が付けられます。アンダースコアやmixedCapsは必要ありません。簡潔さを優先してください。なぜなら、パッケージを使用するすべての人がその名前を入力することになるからです。そして、事前に衝突を心配しないでください。パッケージ名はインポートのデフォルト名に過ぎません。すべてのソースコードで一意である必要はなく、まれに衝突が発生した場合、インポートするパッケージはローカルで使用するために異なる名前を選択できます。いずれにしても、インポート内のファイル名がどのパッケージが使用されているかを正確に決定するため、混乱はまれです。
別の慣例として、パッケージ名はソースディレクトリのベース名です。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
と呼ばれるため、クライアントはパッケージをring.New
として表示するため、単に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行に複数のステートメントを区切るためにも必要です。
セミコロン挿入ルールの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
文は、何に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)
この文は、f
とerr
の2つの変数を宣言します。数行後の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 }
rangeの最初の項目(キーまたはインデックス)のみが必要な場合は、2番目の項目を省略します。
for key := range m { if key.expired() { delete(m, key) } }
rangeの2番目の項目(値)のみが必要な場合は、ブランク識別子であるアンダースコアを使用して最初の項目を破棄します。
sum := 0 for _, value := range array { sum += value }
ブランク識別子には多くの用途があり、後のセクションで説明されています。
文字列の場合、range
はさらに多くの作業を行い、UTF-8を解析することで個々のUnicodeコードポイントを抽出します。誤ったエンコーディングは1バイトを消費し、置換ルーンU+FFFDを生成します。(関連する組み込み型を持つ名前)rune
は、単一のUnicodeコードポイントを表すGoの用語です。言語仕様の詳細を参照してください。ループ
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 }
他のCに似た言語ほどGoでは一般的ではありませんが、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
を出力します。より現実的な例として、プログラム全体で関数の実行をトレースする簡単な方法があります。このような簡単なトレースルーチンを2つ記述できます。
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
に関するセクションでは、その可能性の別の例を示します。
データ¶
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}
複合リテラルのフィールドは順番に配置され、すべて存在する必要があります。ただし、要素を *field*`:`*value* のペアとして明示的にラベル付けすることにより、初期化子は任意の順序で表示でき、欠落しているものはそれぞれのゼロ値のままになります。したがって、次のように言うことができます。
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` によってキャプチャされています。ただし、その関数の設計を理解するには、もう少し情報が必要なので、後で説明します。
2次元スライス¶
Goの配列とスライスは1次元です。2次元配列またはスライスの同等物を作成するには、次のように配列の配列またはスライスのスライスを定義する必要があります。
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."), }
場合によっては、2次元スライスを割り当てる必要があることがあり、これはたとえばピクセルの走査線を処理する場合に発生する可能性があります。これを実現するには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`(「値」)を使用できます。結果は、`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
形式は整数とruneにも適用され、シングルクォートで囲まれたrune定数を生成します。) また、%x
は整数だけでなく、文字列、バイト配列、バイトスライスにも作用し、長い16進数の文字列を生成します。フォーマットにスペースを入れると(% x
)、バイト間にスペースが入ります。
もう1つの便利な形式は%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. }
初期化セクションでは、この再帰を回避する別のテクニックを紹介します。
もう1つの出力テクニックは、出力ルーチンの引数を別のそのようなルーチンに直接渡すことです。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
を引数のリストとして扱うように指示します。そうでなければ、単一のSlice引数としてv
を渡します。
ここでは説明しきれなかった出力方法が他にもあります。詳細については、パッケージfmt
のgodoc
ドキュメントを参照してください。
ちなみに、...
パラメータは特定の型にすることができます。たとえば、整数リストの最小値を選択する最小関数の場合、...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
関数とは異なります。模式的には、次のようになります。
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の定数はまさにそれです—定数です。関数内のローカルとして定義された場合でも、コンパイル時に作成され、数値、文字(rune)、文字列、またはブール値のみにすることができます。コンパイル時の制限があるため、それらを定義する式は、コンパイラによって評価できる定数式である必要があります。たとえば、1<<3
は定数式ですが、math.Sin(math.Pi/4)
は、math.Sin
への関数呼び出しが実行時に発生する必要があるため、定数式ではありません。
Goでは、列挙定数はiota
列挙子を使用して作成されます。iota
は式の1部になり、式は暗黙的に繰り返すことができるため、複雑な値のセットを簡単に構築できます。
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
として出力されます。
ここでByteSize
のString
メソッドを実装するためにSprintf
を使用するのは、変換によるものではなく、%f
を使用してSprintf
を呼び出すため安全です(無限に再帰しません)。%f
は文字列形式ではないため、Sprintf
は文字列が必要な場合にのみString
メソッドを呼び出し、%f
は浮動小数点値を必要とします。
変数¶
変数は定数と同様に初期化できますが、初期化子は実行時に計算される一般的な式にすることができます。
var ( home = os.Getenv("HOME") user = os.Getenv("USER") gopath = os.Getenv("GOPATH") )
init関数¶
最後に、各ソースファイルは、必要な状態を設定するために独自のniladic 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
で見たように、メソッドは任意の名前付き型(ポインタやインターフェースを除く)に対して定義できます。レシーバが構造体である必要はありません。
上記のSliceの議論では、Append
関数を記述しました。代わりに、これをSliceに対するメソッドとして定義できます。これを行うには、最初にメソッドをバインドできる名前付き型を宣言し、メソッドのレシーバをその型の値にします。
type ByteSlice []byte func (slice ByteSlice) Append(data []byte) []byte { // Body exactly the same as the Append function defined above. }
それでも、メソッドは更新されたSliceを返す必要があります。レシーバとしてByteSlice
へのポインタを取るようにメソッドを再定義して、メソッドが呼び出し元のSliceを上書きできるようにすることで、その面倒さを解消できます。
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
のみがio.Writer
を満たすため、ByteSlice
のアドレスを渡します。レシーバのポインタと値に関する規則は、値メソッドはポインタと値の両方で呼び出すことができますが、ポインタメソッドはポインタでのみ呼び出すことができるというものです。
この規則は、ポインタメソッドがレシーバを変更できるため発生します。値で呼び出すと、メソッドは値のコピーを受け取るため、変更は破棄されます。そのため、言語はこの間違いを許しません。ただし、便利な例外があります。値がアドレス指定可能である場合、言語は値に対してポインタメソッドを呼び出す一般的なケースを自動的にアドレス演算子を挿入することで処理します。私たちの例では、変数b
はアドレス指定可能であるため、b.Write
だけでそのWrite
メソッドを呼び出すことができます。コンパイラはそれを(&b).Write
に書き換えます。
ちなみに、バイトのスライスに対してWrite
を使用するというアイデアは、bytes.Buffer
の実装の中心となっています。
インターフェースとその他の型¶
インターフェース¶
Goのインターフェースは、オブジェクトの動作を指定する方法を提供します。何かがこれを行うことができる場合、それはここで使用できます。既にいくつかの簡単な例を見てきました。カスタムプリンターはString
メソッドで実装でき、Fprintf
はWrite
メソッドを持つものに何でも出力できます。メソッドが1つまたは2つしかないインターフェースはGoコードで一般的であり、通常はメソッドに由来する名前が付けられます(たとえば、Write
を実装するものはio.Writer
など)。
型は複数のインターフェースを実装できます。たとえば、コレクションは、Len()
、Less(i, j int) bool
、Swap(i, j int)
を含むsort.Interface
を実装している場合、パッケージ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
がSliceに対して既に実行している作業を再作成しています。(O(N²)の複雑さもあり、効率が悪いです。)Sprint
を呼び出す前にSequence
をプレーンな[]int
に変換することで、作業(および速度)を共有できます。
func (s Sequence) String() string { s = s.Copy() sort.Sort(s) return fmt.Sprint([]int(s)) }
このメソッドは、String
メソッドからSprintf
を安全に呼び出すための変換テクニックの別の例です。(Sequence
と[]int
の2つの型は、型名を無視すれば同じであるため)、それらの間で変換することが合法です。変換は新しい値を作成するのではなく、既存の値が新しい型を持っているかのように一時的に動作します。(整数から浮動小数点など、新しい値を作成する他の合法的な変換もあります。)
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
の新しい値になります。その型は、インターフェースが保持する具体的な型、または値を変換できる2番目のインターフェース型のいずれかである必要があります。値に含まれていることがわかっている文字列を抽出するには、次のように記述できます。
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
は存在し続け、型はstringになりますが、ゼロ値(空文字列)になります。
機能の例として、このセクションの先頭に示した型スイッチと同等の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 プログラムで CRC-32 アルゴリズムを Adler-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) }
使用されていないインポートと変数¶
パッケージをインポートしたり、変数を宣言したりせずに使用することはエラーです。使用されていないインポートはプログラムを膨張させ、コンパイルを遅くしますが、初期化されたが使用されていない変数は、少なくとも無駄な計算であり、おそらくより大きなバグを示唆しています。ただし、プログラムが積極的に開発中である場合、使用されていないインポートと変数はしばしば発生し、コンパイルを進めるためにそれらを削除するのは面倒であり、後で再び必要になる可能性があります。ブランク識別子は回避策を提供します。
この未完成のプログラムには、使用されていないインポート(fmt
と io
)と使用されていない変数(fd
)が2つあるため、コンパイルされませんが、これまでのコードが正しいかどうかを確認したい場合があります。
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"
このインポート形式は、パッケージが副作用のためにインポートされていることを明確にします。このファイルでは名前がないため、パッケージを使用する方法が他にないためです。(名前があり、その名前を使用しなかった場合、コンパイラはプログラムを拒否します。)
インターフェースチェック¶
上記のインターフェースに関する説明で見たように、型はインターフェースを実装することを明示的に宣言する必要はありません。代わりに、型はインターフェースのメソッドを実装するだけでインターフェースを実装します。実際には、ほとんどのインターフェース変換は静的であるため、コンパイル時にチェックされます。たとえば、io.Reader
を期待する関数に*os.File
を渡しても、*os.File
がio.Reader
インターフェースを実装していない限り、コンパイルされません。
ただし、いくつかのインターフェースチェックは実行時に発生します。1つのインスタンスは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`パッケージは、いくつかのメソッドを実装できるオブジェクトを指定する、他のいくつかのインターフェースもエクスポートしています。たとえば、`Read`と`Write`の両方を含むインターフェース`io.ReadWriter`があります。2つのメソッドを明示的にリストすることで`io.ReadWriter`を指定できますが、新しいインターフェースを形成するために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`はバッファリングされたreader/writerも実装しており、これは埋め込みを使用してreaderとwriterを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つのゴルーチンだけが値にアクセスできます。データ競合は、設計上発生しません。この考え方を実現するために、次のようなスローガンに凝縮しました。
メモリを共有して通信するのではなく、通信によってメモリを共有します。
このアプローチは行き過ぎることがあります。たとえば、参照カウントは、整数変数の周りにミューテックスを置くことで最適に行うことができます。しかし、高レベルのアプローチとして、アクセスを制御するためにチャネルを使用すると、明確で正しいプログラムを簡単に記述できます。
このモデルを検討する1つの方法は、1つのCPUで実行される典型的なシングルスレッドプログラムを検討することです。同期プリミティブは必要ありません。次に、別のそのようなインスタンスを実行します。これも同期は必要ありません。これらの2つを通信させると、通信が同期器である場合、他の同期は必要ありません。たとえば、Unixパイプはこのモデルに完全に適合します。Goの並行処理へのアプローチはHoareのCommunicating Sequential Processes(CSP)に由来しますが、Unixパイプの型安全な汎化と見なすこともできます。
ゴルーチン¶
既存の用語(スレッド、コルーチン、プロセスなど)は不正確な意味合いを伝えるため、*ゴルーチン*と呼ばれています。ゴルーチンは単純なモデルを持っています。それは、同じアドレス空間内の他のゴルーチンと同時に実行される関数です。軽量であり、スタック空間の割り当て以上の費用はほとんどかかりません。そしてスタックは小さく始まりますので、安価であり、必要に応じてヒープストレージの割り当て(と解放)によって成長します。
ゴルーチンは複数のOSスレッドに多重化されるため、I/O待ちなど、1つがブロックしても、他のゴルーチンは実行を続けます。その設計により、スレッドの作成と管理の複雑さの多くが隠されています。
関数またはメソッド呼び出しの前に`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`で割り当てられ、結果の値は基礎となるデータ構造への参照として機能します。オプションの整数パラメータが提供されている場合、チャネルのバッファサイズを設定します。デフォルトは0であり、非バッファリングまたは同期チャネルです。
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つの計算(ゴルーチン)が既知の状態にあることを保証する)を組み合わせます。
チャネルを使用する多くの優れたイディオムがあります。これは、開始するための1つの例です。前のセクションでは、バックグラウンドでソートを開始しました。チャネルを使用すると、ソートを開始したゴルーチンがソートの完了を待つことができます。
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
を実行している間は、それ以上のハンドラは、既存のハンドラのいずれかが処理を完了してバッファから受信するまで、満杯のチャネルバッファへの送信をブロックします。
しかし、この設計には問題があります。Serve
は、一度にMaxOutstanding
個しか実行できないにもかかわらず、着信要求ごとに新しいゴルーチンを作成します。その結果、要求が速すぎる場合、プログラムは無限のリソースを消費する可能性があります。この欠点は、Serve
がゴルーチンの作成を制御するように変更することで対処できます。
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func() { process(req) <-sem }() } }
(1.22より前のGoバージョンでは、このコードにバグがあります。ループ変数はすべてのゴルーチンで共有されています。Go wiki を参照してください。)
リソースを適切に管理する別の方法として、要求チャネルから読み取る固定数の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
文は、別のタイプアサーションです。失敗すると、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
の1つの用途は、他の実行中のゴルーチンを停止することなく、サーバ内で失敗したゴルーチンをシャットダウンすることです。
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
を呼び出すことで、あらゆる悪い状況からクリーンに脱出できます。そのアイデアを使用して、複雑なソフトウェアでのエラー処理を簡素化できます。ローカルエラー型を使用してパースエラーを報告する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は非常に強力なので、数行で多くのことができます。