Introduction
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を参照してください。
Examples
Goパッケージソースは、コアライブラリとしてだけでなく、言語の使用例としても機能することを目的としています。さらに、多くのパッケージには、go.devウェブサイトから直接実行できる動作する自己完結型の実行可能な例が含まれています。たとえば、こちらの例があります(必要に応じて「Example」という単語をクリックして開いてください)。問題にどのようにアプローチするか、または何かがどのように実装されるかについて質問がある場合、ライブラリ内のドキュメント、コード、および例が回答、アイデア、背景を提供できます。
Formatting
フォーマットの問題は最も議論の余地があり、しかし最も重要ではありません。人々は異なるフォーマットスタイルに適応できますが、そうする必要がない方が良く、全員が同じスタイルに従う場合、トピックに費やす時間が少なくなります。問題は、長い指示的なスタイルガイドなしでこのユートピアにどのようにアプローチするかです。
Goでは、通常とは異なるアプローチを取り、機械にほとんどのフォーマットの問題を処理させます。gofmt
プログラム(ソースファイルレベルではなくパッケージレベルで動作するgo fmt
としても利用可能)は、Goプログラムを読み取り、標準のインデントと垂直整列のスタイルでソースを出力し、コメントを保持し、必要に応じて再フォーマットします。新しいレイアウト状況を処理する方法を知りたい場合は、gofmt
を実行してください。答えが正しくないように思える場合は、プログラムを再配置してください(またはgofmt
に関するバグを報告してください)、それを回避しないでください。
たとえば、構造体のフィールドに対するコメントを整列させるために時間を費やす必要はありません。Gofmt
がそれを行います。宣言がある場合、
type T struct {
name string // name of the object
value int // its value
}
``````bash
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
Commentary
GoはCスタイルの/* */
ブロックコメントとC++スタイルの//
行コメントを提供します。行コメントが一般的であり、ブロックコメントは主にパッケージコメントとして現れますが、式内や大規模なコードの無効化に役立ちます。
トップレベルの宣言の前に現れるコメントは、間に改行がない場合、その宣言自体を文書化するものと見なされます。これらの「ドキュメントコメント」は、特定のGoパッケージやコマンドの主な文書です。ドキュメントコメントについての詳細は、「Goドキュメントコメント」を参照してください。
Names
名前はGoにおいて他の言語と同様に重要です。名前の可視性は、パッケージの外部での最初の文字が大文字かどうかによって決まります。したがって、Goプログラムにおける命名規則について少し時間をかける価値があります。
Package names
パッケージがインポートされると、パッケージ名はその内容へのアクセサーとなります。
import "bytes"
インポートするパッケージはbytes.Buffer
について話すことができます。パッケージを使用するすべての人がその内容を参照するために同じ名前を使用できることが助けになります。これは、パッケージ名が良いものであるべきことを意味します: 短く、簡潔で、喚起的であるべきです。慣習として、パッケージには小文字の単語名が付けられ、アンダースコアや混合大文字は必要ありません。簡潔さを優先してください。なぜなら、あなたのパッケージを使用するすべての人がその名前を入力することになるからです。そして、衝突について心配する必要はありません。パッケージ名はインポートのデフォルト名に過ぎず、すべてのソースコードで一意である必要はありません。衝突が稀に発生した場合、インポートするパッケージはローカルで使用するために異なる名前を選択できます。いずれにせよ、混乱は稀であり、インポート内のファイル名がどのパッケージが使用されているかを決定します。
もう一つの慣習は、パッケージ名がそのソースディレクトリの基本名であることです。src/encoding/base64
のパッケージは"encoding/base64"
としてインポートされますが、base64
という名前を持ち、encoding_base64
やencodingBase64
ではありません。
パッケージのインポーターは、その内容を参照するためにその名前を使用します。したがって、パッケージ内のエクスポートされた名前は、その事実を利用して繰り返しを避けることができます。(テストを実行する必要があるが、テストしているパッケージの外で実行する必要があるimport .
表記法は使用しないでください。これはテストを簡素化できますが、他の場合は避けるべきです。)たとえば、bufio
パッケージのバッファードリーダー型はReader
と呼ばれ、BufReader
ではなく、bufio.Reader
として見られます。これは明確で簡潔な名前です。さらに、インポートされたエンティティは常にそのパッケージ名でアドレス指定されるため、bufio.Reader
はio.Reader
と衝突しません。同様に、ring.Ring
の新しいインスタンスを作成する関数は、Goにおけるコンストラクタの定義であり、通常はNewRing
と呼ばれますが、Ring
がパッケージによってエクスポートされる唯一の型であり、パッケージがring
と呼ばれるため、New
と呼ばれ、パッケージのクライアントにはring.New
として見られます。パッケージ構造を使用して良い名前を選ぶのに役立ててください。
もう一つの短い例はonce.Do
です。once.Do(setup)
は読みやすく、once.DoOrWaitUntilDone(setup)
と書くことで改善されることはありません。長い名前は自動的に物事をより読みやすくするわけではありません。役立つドキュメントコメントは、しばしば余分に長い名前よりも価値があります。
Getters
Goは自動的にゲッターとセッターをサポートしていません。自分でゲッターとセッターを提供することに問題はありませんし、しばしばそれが適切ですが、Get
をゲッターの名前に入れることは慣用的でも必要でもありません。owner
(小文字、エクスポートされていない)というフィールドがある場合、ゲッターメソッドはOwner
(大文字、エクスポートされている)と呼ばれるべきであり、GetOwner
とは呼ばれるべきではありません。エクスポートのための大文字の名前の使用は、フィールドとメソッドを区別するためのフックを提供します。必要に応じて、セッター関数はSetOwner
と呼ばれる可能性が高いです。両方の名前は実際に読みやすいです:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
Interface names
慣習として、1メソッドインターフェースはメソッド名に-erサフィックスや類似の修飾を加えてエージェント名を構成します: Reader
、Writer
、Formatter
、CloseNotifier
など。
このような名前は多数存在し、それらとそれらが捉える関数名を尊重することは生産的です。Read
、Write
、Close
、Flush
、String
などは、標準的なシグネチャと意味を持っています。混乱を避けるために、同じシグネチャと意味を持たない限り、それらの名前をメソッドに付けないでください。逆に、あなたの型がよく知られた型のメソッドと同じ意味を持つメソッドを実装している場合は、同じ名前とシグネチャを付けてください。文字列変換メソッドをString
と呼び、ToString
とは呼ばないでください。
MixedCaps
最後に、Goの慣習は、複数の単語の名前を書く際にMixedCaps
またはmixedCaps
を使用することであり、アンダースコアを使用することではありません。
Semicolons
Cと同様に、Goの正式な文法は文を終了するためにセミコロンを使用しますが、Cとは異なり、ソース内にセミコロンは表示されません。代わりに、字句解析器はスキャン中にセミコロンを自動的に挿入するための単純なルールを使用します。そのため、入力テキストはほとんどセミコロンがありません。
ルールは次のとおりです。改行の前の最後のトークンが識別子(int
やfloat64
のような単語を含む)、数値や文字列定数のような基本的なリテラル、または次のトークンのいずれかである場合、
break continue fallthrough return ++ -- ) }
字句解析器は常にトークンの後にセミコロンを挿入します。これは「トークンの後に改行が来る場合、セミコロンを挿入する」と要約できます。
セミコロンは閉じ括弧の直前にも省略できます。したがって、次のような文は
go func() { for { dst <- <-src } }()
セミコロンを必要としません。慣用的なGoプログラムには、for
ループの条件、初期化子、条件、継続要素を区切るためのセミコロンのみが含まれています。また、行に複数の文がある場合は、それらを区切るためにも必要です。
セミコロン挿入ルールの一つの結果は、制御構造(if
、for
、switch
、またはselect
)の開き括弧を次の行に置くことができないことです。そうすると、括弧の前にセミコロンが挿入され、望ましくない影響を引き起こす可能性があります。次のように書いてください。
if i < f() {
g()
}
このように書かないでください。
if i < f() // wrong!
{ // wrong!
g()
}
Control structures
Goの制御構造はCのそれに関連していますが、重要な点で異なります。do
やwhile
ループはなく、わずかに一般化されたfor
のみがあります。switch
はより柔軟で、if
とswitch
はfor
のようなオプションの初期化文を受け入れます。break
とcontinue
文は、ブレークまたは継続するものを識別するためのオプションのラベルを受け入れます。また、型スイッチや多方向通信の多重化器であるselect
などの新しい制御構造もあります。構文もわずかに異なり、括弧はなく、本文は常に波括弧で区切られなければなりません。
If
Goの単純なif
は次のようになります:
if x > 0 {
return y
}
必須の波括弧は、単純なif
文を複数行に書くことを奨励します。本文にreturn
やbreak
のような制御文が含まれている場合、特にそうするのが良いスタイルです。
``````bash
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)
Redeclaration and reassignment
余談ですが、前のセクションの最後の例は、:=
短い宣言形式がどのように機能するかの詳細を示しています。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`````に代入可能であること、
- 宣言によって作成される他の変数が少なくとも1つあること。
この特異な特性は純粋な実用主義であり、たとえば、長い`````if-else`````チェーンで単一の`````err`````値を使用するのを容易にします。これが頻繁に使用されるのを目にするでしょう。
§ Goでは、関数パラメータと戻り値のスコープは関数本体と同じであることに注意する価値があります。これは、関数本体を囲む波括弧の外に出現するにもかかわらずです。
<a name="for"></a>
### For
Goの`````for`````ループはCのそれに似ていますが、同じではありません。`````for`````と`````while`````を統合し、`````do-while`````はありません。3つの形式があり、そのうちの1つだけがセミコロンを持っています。
``````bash
// 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
は、単一の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
}
Goでは、他のCライクな言語ほど一般的ではありませんが、break
文を使用してswitch
を早期に終了させることができます。ただし、周囲のループから抜け出す必要がある場合は、スイッチではなく、ループにラベルを付けてそのラベルに「ブレーク」することで実現できます。この例は両方の使用法を示しています。
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 switch
スイッチは、インターフェース変数の動的型を発見するためにも使用できます。この型スイッチは、括弧内に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
}
Functions
Multiple return values
Goの特異な機能の1つは、関数やメソッドが複数の値を返すことができることです。この形式は、Cプログラムのいくつかの不器用な慣用句を改善するために使用できます: -1
のようなバンド内エラー返却や、アドレスで渡された引数の修正。
Cでは、書き込みエラーは負のカウントで信号され、エラーコードは揮発性の場所に隠されています。Goでは、Write
はカウントとエラーを返すことができます: 「はい、いくつかのバイトを書きましたが、すべてではありません。デバイスが満杯になったからです。」パッケージos
のファイルにあるWrite
メソッドのシグネチャは:
func (file *File) Write(b []byte) (n int, err error)
ドキュメントによれば、書き込まれたバイト数と、n
が!=
した場合に非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)
}
Named result parameters
Go関数の戻り値または結果「パラメータ」に名前を付け、通常の変数のように使用できます。名前を付けると、関数が開始されるときにその型のゼロ値に初期化されます。関数が引数なしでreturn
文を実行すると、結果パラメータの現在の値が返されます。
名前は必須ではありませんが、コードを短く明確にすることができます: それらは文書です。nextInt
の結果に名前を付けると、どの戻り値int
がどれであるかが明らかになります。
func nextInt(b []byte, pos int) (value, nextPos int) {
名前付き結果は初期化され、無修飾の戻り値に結びついているため、明確にするだけでなく簡素化することもできます。次のように、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.
}
遅延関数への引数(関数がメソッドである場合はレシーバーを含む)は、*呼び出し*が実行されるときではなく、*遅延*が実行されるときに評価されます。関数が実行されるときに変数の値が変わることを心配することを避けるだけでなく、単一の遅延呼び出しサイトが複数の関数実行を遅延させることができます。次のような愚かな例があります。
``````bash
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....
}
引数が遅延関数の実行時に評価されるという事実を利用して、トレースルーチンが未トレースルーチンへの引数を設定できます。この例:
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
のセクションでは、その可能性の別の例を見ていきます。
Data
Allocation with new
Goには、組み込み関数new
とmake
の2つの割り当てプリミティブがあります。これらは異なることを行い、異なる型に適用されるため、混乱を招く可能性がありますが、ルールは簡単です。まずnew
について話しましょう。これはメモリを割り当てる組み込み関数ですが、他の言語の同名の関数とは異なり、メモリを初期化することはなく、ゼロにするだけです。つまり、new(T)
はT
型の新しいアイテムのためにゼロクリアされたストレージを割り当て、そのアドレスを返します。Goの用語では、T
型の新しく割り当てられたゼロ値へのポインタを返します。
ゼロ値が有用であるという特性は、推移的に機能します。この型宣言を考えてみましょう。
``````bash
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
`
``````bash
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
`
Constructors and composite literals
時にはゼロ値では不十分で、初期化コンストラクタが必要な場合があります。これは、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個の整数の配列を割り当て、その後、配列の最初の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では、
- 配列は値です。1つの配列を別の配列に割り当てると、すべての要素がコピーされます。
- 特に、配列を関数に渡すと、配列のコピーが渡され、ポインタは渡されません。
- 配列のサイズはその型の一部です。
[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でのほとんどの配列プログラミングは単純な配列ではなくスライスを使用して行われます。
スライスは基になる配列への参照を保持し、1つのスライスを別のスライスに割り当てると、両方が同じ配列を参照します。関数がスライス引数を受け取る場合、スライスの要素に対する変更は呼び出し元に見えるため、基になる配列へのポインタを渡すのと類似しています。したがって、Read
関数はポインタとカウントの代わりにスライス引数を受け入れることができ、スライス内の長さが読み取るデータの上限を設定します。以下は、Read
型の File
メソッドのシグネチャです。
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
は真になります。そうでない場合、seconds
はゼロに設定され、ok
は偽になります。これをまとめて、良いエラーレポートを持つ関数があります:
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}
(アンパサンドに注意してください。)その引用された文字列フォーマットは、%q
を通じて、string
または []byte
型の値に適用されるときにも利用可能です。代替フォーマット %#q
は、可能であればバッククォートを使用します。(%q
フォーマットは整数やルーンにも適用され、単一引用のルーン定数を生成します。)また、%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
を呼び出すことができます。ただし、このアプローチについて理解しておくべき重要な詳細があります。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
を引数のリストとして扱うように指示します。そうでなければ、v
を単一のスライス引数として渡すだけです。
印刷には、ここでカバーした以上のことがあります。詳細については、godoc
パッケージの fmt
ドキュメントを参照してください。
ところで、...
パラメータは特定の型、たとえば整数のリストの最小値を選択するための最小関数の ...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 は任意の型のプレースホルダーです。呼び出し元によって型 T
が決定されるGoの関数を書くことは実際にはできません。だからこそ、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
で呼び出すためです。これは文字列フォーマットではなく、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
を満たします。これは便利です。たとえば、1つに印刷できます。
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
ByteSlice
のアドレスを渡します。なぜなら、*ByteSlice
のみが io.Writer
を満たすからです。受信者に対するポインタと値のルールは、値メソッドはポインタと値の両方で呼び出すことができますが、ポインタメソッドはポインタのみに呼び出すことができるということです。
このルールは、ポインタメソッドが受信者を変更できるために生じます。値で呼び出すと、メソッドは値のコピーを受け取ることになり、したがって、変更は破棄されます。したがって、言語はこの間違いを許可しません。ただし、便利な例外があります。値がアドレス可能な場合、言語はポインタメソッドを値に対して呼び出す一般的なケースを自動的にアドレス演算子を挿入することで処理します。私たちの例では、変数 b
はアドレス可能であるため、Write
メソッドを b.Write
で呼び出すことができます。コンパイラはそれを (&b).Write
に書き換えます。
ところで、バイトのスライスに Write
を使用するアイデアは、bytes.Buffer
の実装の中心です。
インターフェースとその他の型
インターフェース
Goのインターフェースは、オブジェクトの動作を指定する方法を提供します。何かがこれを行うことができるなら、それはここで使用できます。すでにいくつかの簡単な例を見てきました。カスタムプリンターは String
メソッドによって実装でき、Fprintf
は Write
メソッドを持つ任意のものに出力を生成できます。1つまたは2つのメソッドのみを持つインターフェースは、Goコードで一般的であり、通常は io.Writer
のようにメソッドから派生した名前が付けられます。
型は複数のインターフェースを実装できます。たとえば、コレクションは、sort
パッケージのルーチンによってソートできます。sort.Interface
を実装している場合、Len()
、Less(i, j int) bool
、Swap(i, j int)
を含み、カスタムフォーマッタを持つこともできます。この作り話の例では、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 + "]"
}
変換
String
メソッドの Sequence
は、Sprint
がすでにスライスに対して行っている作業を再作成しています。(また、複雑さは O(N²) であり、良くありません。)Sequence
を []int
に変換してから Sprint
を呼び出すことで、努力を共有(およびスピードアップ)できます。
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}
このメソッドは、Sprintf
メソッドから String
メソッドを安全に呼び出すための変換技術の別の例です。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
を持つ新しい値です。その型は、インターフェースが保持する具体的な型であるか、値が変換できる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
は依然として存在し、型は文字列ですが、ゼロ値、つまり空の文字列になります。
この能力の例として、以下はこのセクションの冒頭で開いた型スイッチに相当する 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
パッケージのストリーミング暗号アルゴリズムを、連鎖させるブロック暗号から分離できます。Block
インターフェースはcrypto/cipher
パッケージ内でブロック暗号の動作を指定し、単一のデータブロックの暗号化を提供します。次に、bufio
パッケージとの類推により、このインターフェースを実装する暗号パッケージを使用して、ブロック暗号の詳細を知らずにStream
インターフェースで表されるストリーミング暗号を構築できます。
``````bash
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
<a name="interface_methods"></a>
### インターフェースとメソッド
ほとんど何でもメソッドを持つことができるため、ほとんど何でもインターフェースを満たすことができます。例として、`````http`````パッケージにある`````Handler`````インターフェースがあります。`````Handler`````を実装する任意のオブジェクトは、HTTPリクエストを処理できます。
``````bash
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
`
簡潔にするために、POSTを無視し、HTTPリクエストは常にGETであると仮定しましょう。この単純化は、ハンドラの設定方法には影響しません。ページが訪問された回数をカウントするハンドラの簡単な実装は次のとおりです。
``````bash
// 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)
}
`````ArgServer`````をHTTPサーバーにするには、まず正しいシグネチャを持つように修正します。
``````bash
// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
`
``````bash
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
)と未使用の変数(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
インターフェースを実装していない限り、コンパイルされません。
ただし、一部のインターフェースチェックは実行時に発生します。1つの例は、encoding/json
パッケージにあり、Marshaler
インターフェースを定義しています。JSONエンコーダがそのインターフェースを実装する値を受け取ると、エンコーダはその値のマーシャリングメソッドを呼び出して、標準の変換の代わりにJSONに変換します。エンコーダは、次のようなtype assertionでこのプロパティを実行時にチェックします:
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)
}
``````bash
// 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つのインターフェースも満たします。
埋め込みがサブクラス化と異なる重要な点があります。型を埋め込むと、その型のメソッドは外側の型のメソッドになりますが、呼び出されるとき、メソッドの受信者は内側の型であり、外側の型ではありません。私たちの例では、Read
のbufio.ReadWriter
メソッドが呼び出されると、上記の転送メソッドとまったく同じ効果があります。受信者はreader
のフィールドであり、ReadWriter
自体ではありません。
埋め込みはまた、単純な便利さを提供します。この例は、通常の名前付きフィールドと並んで埋め込まれたフィールドを示しています。
type Job struct {
Command string
*log.Logger
}
``````bash
job.Println("starting now...")
`
``````bash
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
`
または、合成リテラルを使用して、
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
埋め込まれたフィールドに直接参照する必要がある場合、フィールドの型名は、パッケージ修飾子を無視してフィールド名として機能します。Read
メソッドのReadWriter
構造体で行ったように。ここで、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
というフィールドまたはメソッドを含んでいる場合、Command
フィールドはJob
を支配します。
次に、同じネストレベルで同じ名前が現れると、通常はエラーです。log.Logger
を埋め込むことは誤りであり、Job
構造体がLogger
という別のフィールドまたはメソッドを含んでいる場合は誤りです。ただし、重複した名前が型定義の外でプログラム内で言及されない場合は問題ありません。この資格は、外部から埋め込まれた型に対する変更からの保護を提供します。別のサブタイプの別のフィールドと衝突するフィールドが追加されても、どちらのフィールドも使用されない場合は問題ありません。
並行性
コミュニケーションによる共有
並行プログラミングは大きなトピックであり、ここではGo特有のハイライトの一部しかスペースがありません。
多くの環境での並行プログラミングは、共有変数への正しいアクセスを実装するために必要な微妙さによって困難になります。Goは、共有値がチャネルで渡され、実際には別々の実行スレッドによって積極的に共有されることはないという異なるアプローチを奨励します。任意の時点で、1つのゴルーチンだけがその値にアクセスできます。データ競合は、設計上発生しません。この考え方を奨励するために、次のスローガンに要約しました:
メモリを共有することでコミュニケーションしないでください;
代わりに、コミュニケーションによってメモリを共有してください。
このアプローチは行き過ぎることがあります。たとえば、参照カウントは整数変数の周りにミューテックスを置くことで最もよく行われるかもしれません。しかし、高レベルのアプローチとして、アクセスを制御するためにチャネルを使用することで、明確で正しいプログラムを書くのが容易になります。
このモデルについて考える1つの方法は、1つのCPUで実行される典型的な単一スレッドプログラムを考えることです。同期プリミティブは必要ありません。次に、別のそのようなインスタンスを実行します。それも同期は必要ありません。次に、それら2つを通信させます。通信が同期者であれば、他の同期は必要ありません。たとえば、Unixパイプラインはこのモデルに完全に適合します。Goの並行性へのアプローチは、Hoareのコミュニケーティング・シーケンシャル・プロセス(CSP)に起源を持っていますが、Unixパイプの型安全な一般化とも見なすことができます。
ゴルーチン
それらはゴルーチンと呼ばれています。なぜなら、既存の用語(スレッド、コルーチン、プロセスなど)は不正確な意味合いを伝えるからです。ゴルーチンはシンプルなモデルを持っています:それは同じアドレス空間内で他のゴルーチンと並行して実行される関数です。軽量で、スタック領域の割り当てにかかるコストはわずかです。そして、スタックは小さく始まり、必要に応じてヒープストレージを割り当て(および解放)することで成長します。
ゴルーチンは複数のOSスレッドに多重化されているため、1つがブロックされると(たとえば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
に渡され、値がチャネルに送信され、リクエストが処理され、その後、次の消費者のために「セマフォ」を準備するためにチャネルから値を受信します。チャネルバッファの容量は、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.
}
}
ただし、この設計には問題があります。`````Serve`````は、受信リクエストごとに新しいゴルーチンを作成しますが、同時に実行できるのは`````MaxOutstanding`````個だけです。その結果、リクエストが速すぎると、プログラムは無限のリソースを消費する可能性があります。この欠点に対処するために、`````Serve`````を変更してゴルーチンの作成を制限します:
``````bash
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
`
(Goのバージョン1.22以前では、このコードにはバグがあります。ループ変数がすべてのゴルーチンで共有されます。詳細については、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
}
私たちは、ループ内で独立して部分を起動します。1つの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()
``````bash
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
をフリーリストに戻します。この場合、バッファは床に落とされ、ガベージコレクタによって回収されます。(default
ステートメントのselect
句は、他のケースが準備されていないときに実行されます。つまり、selects
は決してブロックされません。)この実装は、バッファ付きチャネルとガベージコレクタを利用して、わずか数行で漏れバケットフリーリストを構築します。
エラー
ライブラリのルーチンは、しばしば呼び出し元に何らかのエラーの指示を返さなければなりません。前述のように、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()
}
``````bash
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
文は別のtype assertionです。失敗した場合、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
を避けるべきです。問題をマスクしたり回避したりできる場合は、プログラム全体を停止させるよりも、物事を続行させる方が常に良いです。1つの反例は初期化中です:ライブラリが本当に自分自身をセットアップできない場合、パニックを起こすのは合理的かもしれません。
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
リカバー
`````recover`````への呼び出しは巻き戻しを停止し、`````panic`````に渡された引数を返します。巻き戻し中に実行される唯一のコードは遅延関数内にあるため、`````recover`````は遅延関数内でのみ有用です。
`````recover`````の1つの用途は、サーバー内で失敗しているゴルーチンをシャットダウンし、他の実行中のゴルーチンを殺さないことです。
``````bash
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
を呼び出すことで条件が完全に処理されます。
リカバリパターンが整った状態で、`````do`````関数(およびそれが呼び出すもの)は、`````panic`````を呼び出すことで、あらゆる悪い状況からクリーンに抜け出すことができます。このアイデアを使用して、複雑なソフトウェアのエラーハンドリングを簡素化できます。`````regexp`````パッケージの理想化されたバージョンを見てみましょう。これは、ローカルエラー型を持つ`````panic`````を呼び出すことで解析エラーを報告します。`````Error`````の定義、`````error`````メソッド、および`````Compile`````関数は次のとおりです。
``````bash
// 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
}
`
エラーハンドリングが整った状態で、`````error`````メソッド(型にバインドされたメソッドであるため、組み込みの`````error`````型と同じ名前を持つことは問題ありません)は、手動で解析スタックを巻き戻すことを心配せずに解析エラーを報告するのを容易にします:
``````bash
if pos == 0 {
re.error("'*' illegal at start of expression")
}
`
このパターンは便利ですが、パッケージ内でのみ使用すべきです。Parse
は内部のpanic
呼び出しをerror
値に変換します。クライアントにpanics
を公開しません。これは良いルールです。
ところで、この再パニックのイディオムは、実際のエラーが発生した場合にパニック値を変更します。しかし、元の失敗と新しい失敗の両方がクラッシュレポートに表示されるため、問題の根本原因は依然として可視化されます。したがって、この単純な再パニックアプローチは通常十分です—結局のところ、クラッシュですから—しかし、元の値のみを表示したい場合は、予期しない問題をフィルタリングし、元のエラーで再パニックするためにもう少しコードを書くことができます。それは読者のための演習として残されています。
ウェブサーバー
完全なGoプログラム、ウェブサーバーで締めくくりましょう。これは実際にはウェブリサーバーの一種です。Googleはchart.apis.google.com
でデータを自動的にチャートやグラフにフォーマットするサービスを提供しています。しかし、データをURLにクエリとして入れる必要があるため、インタラクティブに使用するのは難しいです。ここにあるプログラムは、短いテキストを与えると、QRコードを生成するためにチャートサーバーを呼び出す、データの1つの形式に対してより良いインターフェースを提供します。この画像は、携帯電話のカメラでキャッチされ、例えば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は数行で多くのことを実現できるほど強力です。