...

Go の良い書き方

はじめに

Go は新しい言語です。既存の言語からのアイデアを用いていますが,効果的な Go プログラムは他の言語で書かれたプログラムとは異なる特性を持っています。 C++ や Java のプログラムをそのまま Go に移植しても,良い結果は得られないでしょう。 Java プログラムは Java で書かれ,Go で書かれていないからです。 反対に,Go の角度から問題を考えると,うまくいく,そしてかなり違ったプログラムを書けるようになります。 つまり,Go を上手に書くには,Go の特徴とイディオムを理解する必要があります。 さらに,名前付け,フォーマット,プログラム構成といった,すでに確立されている方法を知ることも重要です。 そうすれば,あなたが書いたプログラムをほかの Go プログラマも容易に理解できるでしょう。

このドキュメントはクリアで慣用的な Go コードを書くためのヒントを提供します。 このドキュメントを読むなら, 言語仕様Go ツアー最初の Go で学んだことが強化されるでしょう。それで,もしまだ読んでいないなら,この 3 つのドキュメントを先に読むことをお勧めします。

Go パッケージソース は,コアライブラリに関する情報を提供するだけでなく,Go 言語の使用例ともなっています。 さらに,多くのパッケージには実行可能な例が載せられていて, golang.org ウェブサイトから直接実行できます。たとえば, これです (必要なら, "例" をクリックして開いてください)。 問題にどのようにアプローチしたら良いのか,あるいはどのように実装したら良いのか疑問がある場合, ライブラリの中にあるドキュメント,コード,そして例を見れば,答え,アイデアやバックグラウンドについて知見が得られるでしょう。

フォーマット

フォーマット論争は加熱しやすいですが,最も意味のない議論になりがちです。 自分独自のフォーマットスタイルをもっても構いませんが, その必要がなければ最善です。 皆が同じスタイルを持っていれば,この問題に割く時間が少なくて済みます。 問題は,どうすれば長いスタイルガイドを習得せずに,これが達成できるかです。

Go では変わったアプローチをしていて, ほとんどのフォーマットは機械的に処理します。 gofmt プログラム (go fmt でも実行可能で,ソースファイルレベルではなく,パッケージレベルで動作します) が Go プログラムを読み込み, 標準スタイルのインデントにして,縦方向にも整列します。コメントは,必要に応じてフォーマットに変更を加えます。 どのようにレイアウトされるか知るには,gofmt を実行します。 フォーマットが正しくないように思える時は, なんとかして自分の思うようにフォーマットさせようとするのではなく, プログラムを修正してください。(あるいは,gofmt のバグを報告してください)。

例として,構造体のフィールドのコメントを揃えるために時間を無駄にする必要はありません。 gofmt が自動的にフォーマットしてくれます。 次のように型宣言されているとしましょう。

type T struct {
    name string // オブジェクト名
    value int // オブジェクトの値
}

gofmt がカラムを揃えてくれます。

type T struct {
    name    string // オブジェクト名
    value   int    // オブジェクトの値
}

標準パッケージにあるすべての Go コードは gofmt を使ってフォーマットされています。

その他のフォーマットの詳細について,簡単に言うと次のようになります。

インデント
インデントにはタブを用い,gofmt はデフォルトでタブを出力します。 スペースは,どうしても使わなければならないときだけ使ってください。
行の長さ
Go には行の長さに制限はありません。長すぎるのではと心配する必要はありません。 長すぎると感じるなら,改行し,一つタブを付け足してインデントします。
括弧
Go は C や Java に比べて括弧は少なくなります。制御構文 (Gif, for, switch) の構文に括弧はありません。 また,演算子の優先順位はシンプルです。それで,
x<<8 + y<<16
は,他の言語とは違い,スペースが示している演算順序となります。

コメント

Go には, C スタイルの /* */ ブロックコメントと C++ スタイルの // 行コメントがあります。 通常は行コメントを使います。 ブロックコメントは,パッケージコメントによく使われます。 また,文中にコメントを書いたり,多くの行にわたるコードを無効にするのに役立ちます。

godoc プログラム(Web サーバ)は,Go ソースファイルを処理してパッケージの中身のドキュメントを抽出します。 トップレベル宣言の前にあるコメント(途中に空行がないもの)は, 宣言といっしょに抽出され,その項目の説明文となります。 このコメントの内容とスタイルが,godoc が出力するドキュメントの質を左右します。

すべてのパッケージは, パッケージコメント (パッケージ文の前のコメント) を持つべきです。 ファイルが複数あるパッケージでは,どれか任意の 1 つのファイルにパッケージコメントを書きます。 パッケージコメントは,パッケージを紹介し, パッケージ全体に関係する情報を提供します。 godoc ページには最初の文が出力されます。 続く文には,より詳細なドキュメントを書きます。

/*
regexp パッケージは,正規表現のシンプルなライブラリを実装します。

受け付ける正規表現の構文は,

    正規表現:
        連結 { '|' 連結 }
    連結:
        { クロージャ }
    クロージャ:
        項目 [ '*' | '+' | '?' ]
    項目:
        '^'
        '$'
        '.'
        文字
        '[' [ '^' ] 文字範囲 ']'
        '(' 正規表現 ')'
*/
package regexp

シンプルなパッケージであれば,パッケージコメントを短くできます。

// path パッケージは,スラッシュで区切られた
// パスを操作する便利関数を実装します。

コメントには,** 等の余分なフォーマットは必要ありません。 出力は固定幅フォントで表示されるかわかりませんので,空白を使って整列させないでください。gofmt と同様,godoc が調整します。 コメントはプレーンテキストで,HTML や _this_ 等の他のアノテーションはそのまま出力されるので,使うべきではありません。 godoc は,インデントされたテキストがプログラム部分を表示するのに適するよう,固定幅フォントで表示します。 fmt パッケージ のパッケージコメントはこの機能を使っています。

文脈によっては, godoc はまったくコメントのフォーマットを調整しません。 それで,初めから見栄えが良くなるように書きましょう。 正しい綴り,句読点,文法に注意します。そして長い行は途中に改行を入れましょう。

パッケージ中の,トップレベル宣言の直前のコメントはすべて,その宣言の ドキュメントコメント となります。 プログラム中の,エクスポートされる(大文字で始まる)名前には,ドキュメントコメントを書くべきです。

ドキュメントコメントは完全な文になっているのが理想的です。そうすれば,様々な方法の自動表示で用いることができます。 最初の文は,宣言された名前で始めます。そして,1 文で要約するようにします。

// Compile は,正規表現をパースし,成功すれば,
// テキストに対してマッチさせる Regexp を返します。
func Compile(str string) (*Regexp, error) {

すべてのドキュメントコメントをその名前で始めれば, go ツールのサブコマンド doc を実行し,出力を grep して検索できます。 例えば, "Compile" という名前を忘れてしまい,正規表現のパース関数を探しているとしましょう。 次のコマンドで探せます。

$ go doc -all regexp | grep -i parse

もしパッケージ中のすべてのドキュメントコメントが,"この関数は" で始まっているとしたら, grep は名前を探すのに役立ちません。 でも,ドキュメントコメントを名前で始めておけば,このように探し出すことができます。

$ go doc -all regexp | grep -i parse
    Compile parses a regular expression and returns, if successful, a Regexp
    MustCompile is like Compile but panics if the expression cannot be parsed.
    parsed. It simplifies safe initialization of global variables holding
$

Go の宣言構文には宣言のグルーピングがあります。 1 つのドキュメントコメントで関連のある定数や変数のグループを始めることができます。 宣言全体が表示されるため,コメントはたいてい形式的になります。

// 正規表現のパースに失敗した場合に返されるエラーコード
var (
    ErrInternal      = errors.New("regexp: internal error")
    ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
    ...
)

グルーピングによって,それぞれに関係性があることを示せます。 たとえば,排他ロック(mutex) で守られている変数グループを表記できます。

var (
    countLock   sync.Mutex
    inputCount  uint32
    outputCount uint32
    errorCount  uint32
)

名前

Go では,他の言語と同様に名前が重要です。 名前は意味にも影響を及ぼし,パッケージ外の名前の可視性は,最初の文字が大文字かどうかによって決まります。それで, Go プログラムの命名規則について話しましょう。

パッケージ名

パッケージがインポートされると,パッケージ名でその中身にアクセスします。

import "bytes"

とインポートしたなら, bytes.Buffer と書けます。 パッケージを使用する皆が同じ名前を使用してその内容を参照できると便利です。そのために,パッケージ名は短く,簡潔にしましょう。 慣例により,パッケージには小文字の単一単語名を付けます。 アンダースコアや大文字小文字の混在は必要ありません。 パッケージを使用する全員がその名前を入力するため,簡潔にしすぎるかもしれませんが,あらかじめ名前の衝突を心配する必要はありません。 パッケージ名は,インポートのデフォルト名にすぎません。 すべてのソースコードにわたって一意である必要はなく,まれに衝突が発生した場合,インポートパッケージはローカルで使用する別の名前を選択できます。 いずれの場合でも,インポートのファイル名によって使用されるパッケージが決まるため,混乱はほとんどありません。

もう一つの慣例は,パッケージ名はソースディレクトリのベース名にするということです。 src/encoding/base64 のパッケージは "encoding/base64" としてインポートされますが, encoding_base64encodingBase64 ではなく base64 という名前になります。

パッケージをインポートする側はその名前を使用して内容を参照するため,パッケージ内のエクスポートされた名前はその事実を使用して,重複した名前付けを回避できます。 (import . 表記を使用しないでください。これは,テスト対象のパッケージの外部で実行する必要があるテストを簡略化できますが,そうでない場合は避ける必要があります。) たとえば,バッファ付きリーダーは, bufio パッケージ内で, BufReader ではなく Reader と呼ばれます。こうすれば,ユーザーにとって bufio.Reader として簡潔で明快な名前となります。 さらに,インポートされたエンティティは常にパッケージ名がつくため, bufio.Readerio.Reader と競合しません。 同様に,コンストラクタ として ring.Ring の新しいインスタンスを作成する関数は,通常 NewRing と名付けます。しかし,パッケージによってエクスポートされるのは Ring のみであり,パッケージ名は ring ですから,単に New と名付けることができます。そうすれば,パッケージの使用側は ring.New となります。 このように,パッケージ構造を利用して,適切な名前を選択してください。

もう 1 つの例は, once.Do です。 once.Do(setup) は読みやすく, once.DoOrWaitUntilDone(setup) としても改善されません。長い名前にすれば自動的に読みやすくなるわけではありません。役立つドキュメンテーションコメントは,余分な長い名前よりも価値があることがよくあります。

Getters

Go は,ゲッターとセッターの自動サポートを提供しません。ゲッターとセッターを自分で提供することには何の問題もありません。それは適切なこともありますが,ゲッターの名前に Get を入れることは慣用的でも必要でもありません。 owner (小文字,非エクスポート) というフィールドがある場合,ゲッターメソッドは GetOwner ではなく, Owner (大文字,エクスポート) とすべきです。エクスポートに大文字の名前を使用するため,メソッドとフィールドを区別できます。必要に応じて,セッター関数は SetOwner とします。こうした名前は読みやすいものです:

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

Interface names

慣例により, 1 メソッドインターフェースは,メソッド名に -er サフィックスをつけて名詞にします。 Reader , Writer, Formatter, CloseNotifier など。

多くの名前がこのように関数名にちなんで付けられており,読みやすくなっています。 ReadWriteCloseFlushString などには特有のシグネチャと意味があります。混乱を避けるために,同じシグネチャと意味を持たない限り,作成するメソッドにこれらの名前をつけないでください。逆に,型が既知の型のメソッドと同じ意味を持つメソッドを実装する場合は,同じ名前とシグネチャを付けます。文字列変換メソッドは, ToString ではなく, String としましょう。

MixedCaps

最後に, Go の規則では,アンダースコアではなく MixedCaps または mixedCaps を使用して複数単語名を記述します。

Semicolons

C のように, Go の正式な文法はセミコロンを使用してステートメントを終了しますが, C とは異なり,これらのセミコロンはソースに表示されません。レクサーは単純なルールを使用してスキャン時にセミコロンを自動的に挿入するため,入力テキストにはほとんどセミコロンがありません。

ルールはこのようになっています。改行の前の最後のトークンが識別子 ( int float64 などの単語を含む),数値や文字列定数などの基本的なリテラル,または以下のトークンの場合,

break continue fallthrough return ++ -- ) }

レクサーは常にトークンの後にセミコロンを挿入します。要約すると, “ ステートメントを終了できるトークンの後に改行がある場合は,セミコロンを挿入します ” 。

セミコロンは右中括弧の直前でも省略できるため,次のようなステートメント

    go func() { for { dst <- <-src } }()

にセミコロンは不要です。慣用的な Go プログラムには, for ループ句などの場所にのみセミコロンがあり,初期化要素,条件要素,および継続要素を分離します。また, 1 行に複数のステートメントを書く場合にも,分離するため必要です。

このセミコロン挿入ルールのため,制御構造 (if, for, switch ,または select) の次の行にオープン括弧を置くことはできません。改行があると,括弧の前にセミコロンが挿入され,望ましくない効果が生じます。このように書きます。

if i < f() {
    g()
}

このようにではありません。

if i < f()  // 間違い!
{           // 間違い!
    g()
}

制御構造

Go の制御構造は C の制御構造に関連していますが,重要な点で異なります。 do または while ループはありません。わずかに一般化された for ループのみです。 switch はより柔軟です。 if および switch は, for のようにオプションの初期化ステートメントを受け入れます。 break および continue ステートメントは,オプションのラベルを使用して,どこに中断または続行するかを指定できます。また,型スイッチや多方向通信マルチプレクサーを含む新しい制御構造 select があります。構文もわずかに異なります。括弧はなく,ボディは常に中括弧で区切る必要があります。

If

Go では,単純な if は次のようになります。

if x > 0 {
    return y
}

中括弧は必須であり,単純な if ステートメントでも複数行で記述することを推奨します。これは良い書き方です。 returnbreak などの制御ステートメントが本体に含まれている場合は特にそうです。

ifswitch は初期化ステートメントを受け入れます。ローカル変数のセットアップに使用するのが一般的です。

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)

このステートメントは, ferr の 2 つの変数を宣言します。数行後, f.Stat を呼び出すときに,

d, err := f.Stat()

とすると, derr を宣言しているように見えます。ただし,両方のステートメントに err があることに注意してください。 この重複は文法的に正しいです。 err は最初のステートメントで宣言されていますが, 2 番目のステートメントでは 再代入 のみです。つまり, f.Stat の呼び出しは,すでに宣言された既存の err 変数を使用し,新しい値を代入するだけです。

:= 宣言では,変数 v が既に宣言されていても,次の場合に使用可能です。

この珍しい特徴は純粋に便宜的なものであり,1 つの err 値を使いまわすことができます。特に,長い if-else があるときに頻繁に使用されます。

§ Go では,関数のパラメーターと戻り値は,ボディを囲む中かっこの外側に書きますが,関数のボディと同じスコープです。

For

Go for ループは C と似ていますが,同じではありません。 forwhile を統合したもので, do-while はありません。 3 つの形式があり,そのうちの 1 つだけにセミコロンがあります。

// C の for 相当
for init; condition; post { }

// C の while 相当
for condition { }

// 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 は, 1 つの 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 チェーンを として書くことは可能です。 > スイッチ

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 を早期に終了できます。ただし,場合によっては,スイッチではなく,周囲のループから抜け出す必要があります。 Go では,ループにラベルを付け,そのラベルを " ブレイク " することで実現できます。この例は,両方の使用法を示しています。

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 スイッチ は,括弧内にキーワード 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 プログラムのいくつかの不格好なイディオムを改善するために使用できます。インバンドエラーは, 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)
    }

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 を実行する関数が戻る直前に実行される関数呼び出し ( deferred 関数) をスケジュールします。これは,関数がどのパスを返すかに関係なく解放する必要があるリソースなどの状況に対処するための,異常ではあるが効果的な方法です。標準的な例は,ミューテックスのロック解除またはファイルのクローズです。

// 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 つの利点があります。最初に,ファイルを閉じることを決して忘れないことを保証します。これは,後で関数を編集して新しい戻りパスを追加する場合に犯しやすい間違いです。第二に,クローズがオープンの近くにあることを意味します。これは,関数の最後に配置するよりもずっと明確です。

遅延関数 (関数がメソッドの場合はレシーバーを含む) の引数は, call の実行時ではなく, defer の実行時に評価されます。関数の実行時に変数が値を変更する心配を回避することに加えて,これは, 1 つの遅延呼び出しサイトが複数の関数の実行を延期できることを意味します。これはばかげた例です。

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

遅延関数は LIFO の順序で実行されるため,このコードにより,関数が戻るときに 4 3 2 1 0 が出力されます。より妥当な例は,プログラムを介して関数の実行をトレースする簡単な方法です。次のような簡単なトレースルーチンをいくつか作成できます。

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

defer の実行時に遅延関数の引数が評価されるという事実を活用することで,より良い結果を得ることができます。トレースルーチンは,トレース解除ルーチンへの引数を設定できます。この例 :

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

プリント

entering: b
in b
entering: a
in a
leaving: a
leaving: b

他の言語からのブロックレベルのリソース管理に慣れているプログラマーにとっては, defer は独特のように思えるかもしれませんが,その最も興味深い強力なアプリケーションは,ブロックベースではなく機能ベースであるという事実からきています。 panicrecover のセクションでは,その可能性の別の例を見るでしょう。

Data

Allocation with new

Go には,組み込み関数 newmake の 2 つの割り当てプリミティブがあります。それらは異なることを行い,異なる型に適用され,混乱を招く可能性がありますが,ルールは単純です。最初に new について話しましょう。これはメモリを割り当てる組み込み関数ですが,他のいくつかの言語の同名の名前とは異なり,メモリを 初期化 せず, ゼロ のみです。つまり, new(T) は,型 T の新しいアイテムにゼロ化ストレージを割り当て,そのアドレス,型 *T の値を返します。 Go の用語では, T 型の新しく割り当てられたゼロ値へのポインターを返します。

new によって返されるメモリはゼロになるため,データ構造を設計する際に,各型のゼロ値をさらに初期化せずに使用できるように調整すると役立ちます。これは,データ構造のユーザーが new でデータ構造を作成し,作業を開始できることを意味します。たとえば, bytes.Buffer のドキュメントには, " Buffer のゼロ値はすぐに使用できる空のバッファーです " と記載されています。同様に, sync.Mutex には明示的なコンストラクターまたは Init メソッドがありません。代わりに, sync.Mutex のゼロ値はロック解除されたミューテックスとして定義されます。

zero-value-is-use プロパティは推移的に機能します。この型宣言を検討する。

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer の値も,割り当て後または宣言だけですぐに使用できます。次のスニペットでは, pv の両方が,さらに調整することなく正しく機能します。

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
}

ボイラープレートがたくさんあります。 composite literal を使用して単純化できます。これは,評価されるたびに新しいインスタンスを作成する式です。

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{} は同等です。

配列,スライス,およびマップの複合リテラルも作成できます。フィールドラベルは,必要に応じてインデックスまたはマップキーになります。これらの例では, EnoneEio ,および 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"}

Allocation with make

割り当てに戻ります。組み込み関数 make(T, args) は, new(T) とは異なる目的を果たします。スライス,マップ,およびチャンネルのみを作成し,型 T ( * ではなく) の initialized ( zeroed ではない) 値を返します。 T) 。区別する理由は,これらの 3 つの型が,使用前に初期化する必要のあるデータ構造への参照を内部で表しているためです。たとえば,スライスは,データ (配列内) へのポインター,長さ,および容量を含む 3 項目記述子であり,それらの項目が初期化されるまで,スライスは nil です。スライス,マップ,およびチャンネルの場合, make は内部データ構造を初期化し,使用する値を準備します。例えば,

make([]int, 10, 100)

100 個の int の配列を割り当て,長さ 10 ,配列の最初の 10 要素を指す容量 100 のスライス構造を作成します。 (スライスを作成する場合,容量は省略できます。詳細については,スライスのセクションを参照する。) 対照的に, new([]int) は,新しく割り当てられたゼロ化されたスライス構造へのポインタを返します。つまり, nil スライス値へのポインター。

これらの例は, newmake の違いを示しています。

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 で割り当てるか,変数のアドレスを明示的に取得します。

Arrays

配列は,メモリの詳細なレイアウトを計画するときに役立ち,割り当てを回避するのに役立つ場合がありますが,主に次のセクションの主題であるスライスのビルディングブロックです。そのトピックの基礎を築くために,配列に関するいくつかの言葉を以下に示します。

Go と C で配列が機能する方法には大きな違いがあります。 Go では,

value プロパティは便利ですが,高価でもあります。 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 ではありません。代わりにスライスを使用する。

Slices

スライスは配列をラップして,データのシーケンスへのより一般的で強力かつ便利なインターフェースを提供します。変換行列などの明示的な次元を持つ項目を除き, Go のほとんどの配列プログラミングは,単純な配列ではなくスライスを使用して行われます。

スライスは,内部の配列への参照を保持します。あるスライスを別のスライスに割り当てると,両方が同じ配列を参照します。関数がスライス引数を取る場合,スライスの要素に加えられた変更は呼び出し元に見えるようになります。これは,内部の配列へのポインターの受け渡しに似ています。したがって, Read 関数は,ポインターとカウントではなくスライス引数を受け入れることができます。スライス内の長さは,読み取るデータ量の上限を設定します。パッケージ osFile 型の Read メソッドのシグネチャは次のとおりです。

func (f *File) Read(buf []byte) (n int, err error)

このメソッドは,読み取られたバイト数とエラー値 (存在する場合) を返します。大きなバッファー buf の最初の 32 バイトに読み込むには,バッファーを slice (ここでは動詞として使用) にします。

    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 でアクセス可能なスライスの 容量 は,スライスが想定できる最大長を報告します。以下は,スライスにデータを追加する関数です。データが容量を超える場合,スライスは再割り当てされます。結果のスライスが返されます。この関数は, lencapnil スライスに適用された場合に有効であり, 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
}

Appendslice の要素を変更できますが,スライス自体 (ポインター,長さ,および容量を保持するランタイムデータ構造) 値で渡されます。

スライスに追加するというアイデアは非常に有用で,組み込み関数 append によってキャプチャされます。ただし,その関数の設計を理解するには,もう少し情報が必要なので,後で説明します。

Two-dimensional slices

Go の配列とスライスは 1 次元です。 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 つは, 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:]
}

Maps

マップは,ある型の値 ( key ) を別の型の値 ( element または value) 。キーは,整数,浮動小数点および複素数,文字列,ポインター,インターフェース (動的型が等式をサポートしている限り) ,構造体,配列など,等号演算子が定義されている任意の型にすることができます。スライスは等号が定義されていないため,マップキーとして使用できません。スライスと同様に,マップは内部のデータ構造への参照を保持します。マップのコンテンツを変更する関数にマップを渡すと,変更は呼び出し元に表示されます。

マップは,コロンで区切られたキーと値のペアを使用した通常の複合リテラル構文を使用して構築できるため,初期化中にマップを簡単に構築できます。

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]

明らかな理由により,これは “comma 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

Printing

Go の書式付き表示では, C の printf ファミリーに似たスタイルが使用されますが,よりリッチで一般的です。関数は fmt パッケージにあり,名前は大文字です : fmt.Printffmt.Fprintffmt.Sprintf など。文字列関数 (Sprintf など) は,渡されたバッファを埋めるのではなく,文字列を返します。

フォーマット文字列を提供する必要はありません。 PrintfFprintf ,および 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

整数の小数など,デフォルトの変換のみが必要な場合は,キャッチオール形式 %v (“ value”) を使用できます。結果は, Print および Println が生成するものとまったく同じです。さらに,そのフォーマットは,配列,スライス,構造体,マップを含め, 任意の 値を出力できます。前のセクションで定義したタイムゾーンマップの表示ステートメントを次に示します。

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

出力が得られます :

map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]

マップの場合, Printf とその友達はキーで辞書式に出力をソートします。

構造体を表示するとき,変更されたフォーマット %+v は,構造体のフィールドに名前を付け,任意の値に対して,代替フォーマット %#v が値を完全に表示します Go 構文。

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

プリント

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}

(アンパサンドに注意する。) この引用符付き文字列形式は, string または []byte 型の値に適用されると, %q からも利用できます。代替形式 %#q は,可能であれば代わりに逆引用符を使用します。 (%q 形式は整数とルーンにも適用され,単一引用符で囲まれたルーン定数を生成します。) また, %x は文字列,バイト配列,およびバイトスライスでも機能します。整数の場合と同様に,長い 16 進文字列を生成し,形式 (% x) にスペースを入れると,バイト間にスペースが挿入されます。

別の便利な形式は %T で,これは値の type を出力します。

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 value および T へのポインターを表示する必要がある場合, String のレシーバーは値型の場合,この例では構造体型の方が効率的で慣用的であるため,ポインターを使用しました。詳細については, ポインターと値レシーバー に関する以下のセクションを参照する。

String メソッドは Sprintf を呼び出すことができます。これは,表示ルーチンが完全にリエントラントであり,この方法でラップできるためです。ただし,このアプローチについて理解する必要がある重要な詳細が 1 つあります。 String< に再帰する方法で Sprintf を呼び出して String メソッドを構築しないでください。 /code> メソッドを無期限に。これは, Sprintf 呼び出しがレシーバーを文字列として直接表示しようとした場合に発生する可能性があり,これによりメソッドが再度呼び出されます。この例が示すように,それはよくある間違いです。

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

また,簡単に修正できます。引数をメソッドを持たない基本的な文字列型に変換します。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

初期化セクション には,この再帰を回避する別の手法があります。

別の表示方法は,表示ルーチンの引数を別のそのようなルーチンに直接渡すことです。 Printf のシグネチャは,その最終引数に ...interface{} 型を使用して,フォーマットの後に任意の数のパラメーター (任意の型) を表示できることを指定します。

func Printf(format string, v ...interface{}) (n int, err error) {

関数 Printf 内では, v[]interface{} 型の変数のように機能しますが,別の可変機能関数に渡されると機能します通常の引数リストのように。上記で使用した log.Println 関数の実装を次に示します。実際のフォーマットのために,引数を直接 fmt.Sprintln に渡します。

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

v の後にネストする Sprintln の呼び出しで v の後に ... を記述して, v を引数のリスト。それ以外の場合は, 1 つのスライス引数として v を渡すだけです。

ここで説明した以外にも,表示にはさらに多くの機能があります。詳細については,パッケージ fmtgodoc ドキュメントを参照する。

ちなみに, ... パラメーターは特定の型にすることができます。たとえば,整数のリストの最小のものを選択する min 関数の場合は ...int です。

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

Append

append 組み込み関数の設計を説明するために必要な欠落部分があります。 append の署名は,上記のカスタム Append 関数とは異なります。概略的には,次のようなものです。

func append(slice []T, elements ...T) []T

ここで, T は特定の型のプレースホルダーです。 Go では,型 T が呼び出し元によって決定される関数を実際に書くことはできません。それが append が組み込まれている理由です。コンパイラからのサポートが必要です。

append が行うことは,要素をスライスの最後に追加し,結果を返すことです。手書きの Append と同様に,内部の配列が変更される可能性があるため,結果を返す必要があります。この簡単な例

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

[1 2 3 4 5 6] を表示します。したがって, appendPrintf のように機能し,任意の数の引数を収集します。

しかし, Append の機能を実行してスライスをスライスに追加したい場合はどうでしょうか ? 簡単 : 上記の Output の呼び出しで行ったように,呼び出しサイトで ... を使用します。このスニペットは,上記のものと同じ出力を生成します。

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

... がなければ,型が間違っているためコンパイルできません。 yint 型ではありません。

Initialization

見た目は C や C++ の初期化と見た目は大きく異なりませんが, Go の初期化はより強力です。初期化中に複雑な構造を構築でき,初期化されたオブジェクト間の順序の問題は,異なるパッケージ間でも正しく処理されます。

Constants

Go の定数はまさにその定数です。関数でローカルとして定義されている場合でも,コンパイル時に作成され,数字,文字 (ルーン) ,文字列,またはブール値のみを使用できます。コンパイル時の制限のため,それらを定義する式は,コンパイラーによって評価可能な定数式でなければなりません。たとえば, 1<<3 は定数式ですが, math.Sin(math.Pi/4) は,関数呼び出しが math であるためではありません。罪は実行時に発生する必要があります。

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)
}

YB1.00YB として表示され, ByteSize(1e13)9.09TB として表示されます。

ここで Sprintf を使用して ByteSizeString メソッドを実装することは,変換のためではなく, Sprintf%f は文字列形式ではありません。 Sprintf は,必要な場合にのみ String メソッドを呼び出します文字列,および %f には浮動小数点値が必要です。

Variables

変数は定数と同様に初期化できますが,初期化子は実行時に計算される一般式にすることができます。

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

The init function

最後に,各ソースファイルは独自の 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")
}

Methods

Pointers vs. Values

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)

io.Writer を満たすのは *ByteSlice のみであるため, ByteSlice のアドレスを渡します。レシーバーのポインターと値に関するルールは,ポインターと値に対して値メソッドを呼び出すことができますが,ポインターメソッドはポインターに対してのみ呼び出すことができます。

この規則は,ポインターメソッドがレシーバーを変更できるために発生します。それらを値で呼び出すと,メソッドは値のコピーを受け取るため,変更は破棄されます。したがって,この言語はこの間違いを許しません。ただし,便利な例外があります。値がアドレス可能である場合,言語はアドレス演算子を自動的に挿入することにより,値に対してポインターメソッドを呼び出す一般的なケースを処理します。この例では,変数 b はアドレス可能であるため, b.Write だけで Write メソッドを呼び出すことができます。コンパイラはそれを (&b).Write に書き換えます。

ところで,バイトスライスで Write を使用するという考え方は, bytes.Buffer の実装の中心です。

Interfaces and other types

Interfaces

Go のインターフェースは,オブジェクトの動作を指定する方法を提供します。何かが this できる場合は, こちら で使用できます。すでにいくつかの簡単な例を見てきました。カスタムプリンターは String メソッドで実装できますが, FprintfWrite メソッドであらゆるものへの出力を生成できます。 1 つまたは 2 つのメソッドのみを持つインターフェースは Go コードで一般的であり,通常は Write を実装するものの io.Writer など,メソッドから派生した名前が付けられます。

型は複数のインターフェースを実装できます。たとえば, Len()sort.Interface を実装している場合,コレクションはパッケージ sort のルーチンでソートできます。 >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 + "]"
}

Conversions

SequenceString メソッドは, Sprint がすでにスライスに対して行っている作業を再現しています。 (複雑さ O(N²) もあり,貧弱です。)Sequence を単純な [] int Sprint を呼び出す前の code> 。

func (s Sequence) String() string {
    s = s.Copy()
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

このメソッドは, String メソッドから安全に Sprintf を呼び出すための変換手法の別の例です。 2 つの型 (Sequence[]int) は型名を無視すると同じであるため,これらの間で変換することは正当です。変換は新しい値を作成するのではなく,既存の値に新しい型があるかのように一時的に動作するだけです。 (整数から浮動小数点への変換など,新しい値を作成する他の有効な変換があります。)

式の型を変換して別のメソッドセットにアクセスすることは, Go プログラムのイディオムです。例として,既存の型 sort.IntSlice を使用して,例全体をこれに減らすことができます。

type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    s = s.Copy()
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

現在, Sequence に複数のインターフェース (並べ替えと表示) を実装させる代わりに,データ項目の機能を使用して複数の型 (Sequencesort.IntSlice および []int) ,それぞれがジョブの一部を実行します。これは実際にはもっと珍しいことですが,効果的です。

Interface conversions and type assertions

型スイッチ は変換の形式です。スイッチはインターフェースを取り,スイッチの各ケースで,ある意味でそのケースの型に変換します。以下は, 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 を保持していることを知っていて,それを抽出したいだけですか ? ワンケース型のスイッチでも可能ですが, 型アサーション でも同様です。型アサーションはインターフェース値を取得し,そこから指定された明示的な型の値を抽出します。構文は,型スイッチを開く句から借用していますが, 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()
}

Generality

インターフェースを実装するためだけに型が存在し,そのインターフェースを超えてメソッドをエクスポートしたことがない場合,型自体をエクスポートする必要はありません。インターフェースのみをエクスポートすると,値がインターフェースで説明されている以上の興味深い動作を持たないことが明確になります。また,共通メソッドのすべてのインスタンスでドキュメントを繰り返す必要がなくなります。

このような場合,コンストラクターは実装型ではなくインターフェース値を返す必要があります。たとえば,ハッシュライブラリでは, crc32.NewIEEEadler32.New の両方がインターフェース型 hash.Hash32 を返します。 Go プログラムで Adler-32 を CRC-32 アルゴリズムに置き換えるには,コンストラクター呼び出しを変更するだけです。残りのコードは,アルゴリズムの変更による影響を受けません。

同様のアプローチにより,さまざまな crypto パッケージのストリーミング暗号アルゴリズムを,チェーン暗号化されたブロック暗号から分離できます。 crypto/cipher パッケージの Block インターフェースは,データの単一ブロックの暗号化を提供するブロック暗号の動作を指定します。次に, bufio パッケージと同様に,このインターフェースを実装する暗号パッケージを使用して,ブロックの詳細を知らなくても, Stream インターフェースで表されるストリーミング暗号を構築できます。暗号化。

crypto/cipher インターフェースは次のようになります。

type Block interface {
    BlockSize() int
    Encrypt(dst, src []byte)
    Decrypt(dst, src []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

ブロック暗号をストリーミング暗号に変換するカウンターモード (CTR) ストリームの定義は次のとおりです。ブロック暗号の詳細が抽象化されていることに注意する。

// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
func NewCTR(block Block, iv []byte) Stream

NewCTR は, 1 つの特定の暗号化アルゴリズムとデータソースだけでなく, Block インターフェースと Stream の実装にも適用されます。インターフェース値を返すため, CTR 暗号化を他の暗号化モードに置き換えることはローカライズされた変更です。コンストラクター呼び出しは編集する必要がありますが,周囲のコードは結果を Stream としてのみ処理する必要があるため,違いに気付かないでしょう。

Interfaces and methods

ほとんどすべてのものにメソッドをアタッチできるため,ほとんどすべてのものがインターフェースを満たすことができます。 1 つの実例は, 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)
}

(テーマを維持しながら, Fprintfhttp.ResponseWriter にどのように表示できるかに注意する。) 参考のために,このようなサーバーを 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)
}

ページが訪問されたことを通知する必要がある内部状態がプログラムにある場合はどうなりますか ? チャンネルを Web ページに結び付けます。

// 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)
}

HandlerFuncServeHTTP というメソッドを持つ型であるため,その型の値は HTTP リクエストを処理できます。メソッドの実装を見てください。レシーバーは関数 f であり,メソッドは f を呼び出します。それは奇妙に思えるかもしれませんが,たとえば,受信者がチャンネルであり,チャンネルで送信するメソッドとそれほど違いはありません。

ArgServer を HTTP サーバーにするには,まず正しい署名を持つように変更します。

// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

ArgServerHandlerFunc と同じ署名を持つようになったため, Sequence に変換したように,その型に変換してメソッドにアクセスできます。 >IntSliceIntSlice.Sort にアクセスします。設定するコードは簡潔です :

http.Handle("/args", http.HandlerFunc(ArgServer))

誰かがページ /args にアクセスすると,そのページにインストールされているハンドラーの値は ArgServer であり, HandlerFunc と入力します。 HTTP サーバーは,その型の ServeHTTP メソッドを呼び出し, ArgServer をレシーバーとして,次に ArgServer を呼び出します (呼び出し < HandlerFunc.ServeHTTP 内の code>f(w , req)) 。引数が表示されます。

このセクションでは,構造体,整数,チャンネル,および関数から HTTP サーバーを作成しました。これは,インターフェースが (ほぼ) 任意の型に対して定義できる単なるメソッドのセットであるためです。

The blank identifier

for range ループ および のコンテキストで,空の識別子について何度か言及しました。 href="#maps"> マップ 。空白の識別子は,任意の型の任意の値で割り当てまたは宣言でき,値は無害に破棄されます。 Unix の /dev/null ファイルへの書き込みに少し似ています。変数が必要なプレースホルダーとして使用される書き込み専用の値を表しますが,実際の値は無関係です。すでに見た以上の用途があります。

The blank identifier in multiple assignment

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)
}

Unused imports and variables

パッケージをインポートしたり,変数を使用せずに宣言するとエラーになります。未使用のインポートはプログラムを膨張させ,コンパイルを遅くします。一方,初期化されているが使用されていない変数は,少なくとも無駄な計算であり,おそらくより大きなバグを示しています。しかし,プログラムが活発に開発されている場合,未使用のインポートと変数が頻繁に発生し,それらを削除してコンパイルを続行し,後で必要になるだけにするのは面倒です。空白の識別子は回避策を提供します。

この半分書かれたプログラムには, 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
}

慣例により,インポートエラーを黙らせるためのグローバル宣言は,インポート後すぐに来てコメントし,それらを見つけやすくし,後でクリーンアップするためのリマインダーとして使用する必要があります。

Import for side effect

前の例の fmtio などの未使用のインポートは,最終的に使用または削除する必要があります。空の割り当ては,コードを進行中の作業として識別します。ただし,明示的な使用をせずに,副作用のみのためにパッケージをインポートすると便利な場合があります。たとえば, init 関数中に, net/http/pprof パッケージが登録されます。デバッグ情報を提供する HTTP ハンドラー。エクスポートされた API がありますが,ほとんどのクライアントに必要なのはハンドラーの登録と Web ページからのデータへのアクセスだけです。副作用のためだけにパッケージをインポートするには,パッケージの名前を空の識別子に変更します。

import _ "net/http/pprof"

この形式のインポートでは,パッケージが副作用のためにインポートされていることが明確になります。これは,パッケージの使用法が他にないためです。このファイルでは,名前がありません。 (もし,その名前を使用しなかった場合,コンパイラはプログラムを拒否します。)

Interface checks

上記の インターフェース の説明で見たように,型はインターフェースを実装することを明示的に宣言する必要はありません。代わりに,型はインターフェースのメソッドを実装するだけでインターフェースを実装します。実際には,ほとんどのインターフェース変換は静的であるため,コンパイル時にチェックされます。たとえば, *os.Fileio.Reader が必要な関数に渡すと, *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 への変換を伴う割り当てでは, *RawMessageMarshaler を実装する必要があります,そのプロパティはコンパイル時にチェックされます。 json.Marshaler インターフェースが変更された場合,このパッケージはコンパイルされなくなり,更新が必要であることが通知されます。

この構造内の空白の識別子の外観は,変数を作成するためではなく,型チェックのためだけに宣言が存在することを示しています。ただし,インターフェースを満たすすべての型に対してこれを実行しないでください。慣例により,このような宣言は,コードに静的な変換が既に存在しない場合にのみ使用されますが,これはまれなイベントです。

Embedding

Go は,サブクラス化の典型的な型駆動型の概念を提供しませんが, “borrow” する機能があります。構造体またはインターフェース内に 埋め込み 型による実装の断片。

インターフェースの埋め込みは非常に簡単です。以前に io.Reader および io.Writer インターフェースについて言及しました。定義は次のとおりです。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

io パッケージは,そのようなメソッドを実装できるオブジェクトを指定する他のいくつかのインターフェースもエクスポートします。たとえば, io.ReadWriter は, ReadWrite の両方を含むインターフェースです。 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.Readerbufio.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.Readerio.Writer ,および io.ReadWriter の 3 つのインターフェースもすべて満たしています。

埋め込みがサブクラス化と異なる重要な方法があります。型を埋め込むと,その型のメソッドは外側の型のメソッドになりますが,呼び出されると,メソッドのレシーバーは外側の型ではなく内側の型になります。この例では, bufio.ReadWriterRead メソッドが呼び出されると,上記の転送方法とまったく同じ効果があります。受信者は, ReadWriter 自体ではなく, ReadWriterreader フィールドです。

埋め込みも簡単にできます。この例は,通常の名前付きフィールドと一緒に埋め込みフィールドを示しています。

type Job struct {
    Command string
    *log.Logger
}

Job 型には, Print, Printf, Println ,および *log.Logger の他のメソッドがあります。 。もちろん, Logger にフィールド名を付けることもできますが,そうする必要はありません。そして今,初期化されたら, Job にログインできます。

job.Println("starting now...")

LoggerJob 構造体の通常のフィールドなので,次のように 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 メソッドでしたように,フィールド名として機能します。 /code> 構造体。ここで, 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.LoggerCommand というフィールドまたはメソッドが含まれている場合, JobCommand フィールドがそれを支配します。

次に,同じネストレベルに同じ名前が表示される場合,通常はエラーです。 Job 構造体に Logger という別のフィールドまたはメソッドが含まれている場合, log.Logger を埋め込むのは誤りです。ただし,型定義以外のプログラムで重複した名前が言及されていない場合は,問題ありません。この資格は,外部から埋め込まれた型に加えられた変更に対する保護を提供します。どちらのフィールドも使用されていない場合,別のサブ型の別のフィールドと競合するフィールドが追加されても問題はありません。

Concurrency

Share by communicating

コンカレントプログラミングは大きなトピックであり,ここには Go 固有のハイライトのいくつかのスペースのみがあります。

多くの環境での平行プログラミングは,シェア変数への正しいアクセスを実装するために必要な微妙さにより困難になります。 Go は,チャンネル上で共有された値が受け渡され,実際には,別々の実行スレッドによってアクティブに共有されることのない異なるアプローチを推奨します。一度に 1 つのゴルーチンのみが値にアクセスできます。設計上,データの競合は発生しません。この考え方を奨励するために,スローガンに減らしました。

Do not communicate by sharing memory; instead, share memory by communicating.

このアプローチは,あまりにも遠すぎます。参照カウントは,たとえば,整数変数の周りにミューテックスを配置することで最適に実行できます。しかし,高レベルのアプローチとして,チャンネルを使用してアクセスを制御すると,明確で正しいプログラムを簡単に作成できます。

このモデルについて考える 1 つの方法は, 1 つの CPU で実行される典型的なシングルスレッドプログラムを検討することです。同期プリミティブは必要ありません。次に,このような別のインスタンスを実行します。同期も不要です。次に,これら 2 つの通信を許可します。通信がシンクロナイザーである場合,他の同期の必要はありません。たとえば, Unix パイプラインはこのモデルに完全に適合します。 Go の並行性へのアプローチは Hoare の Communicating Sequential Processes (CSP) に由来していますが, Unix パイプの型セーフな一般化として見ることもできます。

Goroutines

スレッド,コルーチン,プロセスなどの既存の用語が不正確な意味合いを伝えるため,これらは ゴルーチン と呼ばれます。ゴルーチンには単純なモデルがあります。それは,同じアドレス空間で他のゴルーチンと平行に実行される関数です。軽量であり,スタックスペースの割り当てとほとんど同じコストです。また,スタックは小さく起動するため,安価であり,必要に応じてヒープストレージを割り当てて (そして解放して) 成長します。

ゴルーチンは複数の OS スレッドに多重化されるため, I/O を待機している間など,ブロックする必要がある場合,他のスレッドは引き続き実行されます。その設計は,スレッドの作成と管理の複雑さの多くを隠します。

関数またはメソッド呼び出しの前に go キーワードを付けて,新しいゴルーチンで呼び出しを実行します。呼び出しが完了すると,ゴルーチンは静かに終了します。 (この効果は,バックグラウンドでコマンドを実行するための Unix シェルの & 表記に似ています。)

go list.Sort()  // run list.Sort concurrently; don't wait for it.

関数リテラルは,ゴルーチンの呼び出しに便利です。

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

Go では,関数リテラルはクロージャーです。実装は,関数によって参照される変数がアクティブである限り存続するようにします。

これらの例は,関数に完了を通知する方法がないため,あまり実用的ではありません。そのためには,チャンネルが必要です。

Channels

マップと同様に,チャンネルは 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.
    }
}

MaxOutstanding ハンドラーが process を実行すると,既存のハンドラーの 1 つが終了し,バッファーから受信するまで,それ以上は満杯のチャンネルバッファーへの送信をブロックします。

ただし,この設計には問題があります。 Serve は,リクエストの MaxOutstanding だけがいつでも実行できる場合でも,着信リクエストごとに新しいゴルーチンを作成します。その結果,リクエストが速すぎる場合,プログラムは無制限のリソースを消費する可能性があります。 Serve を変更してゴルーチンの作成を制御することにより,この欠陥に対処できます。明らかな解決策はここにありますが,後で修正するバグがあることに注意する。

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Buggy; see explanation below.
            <-sem
        }()
    }
}

バグは, Go for ループでは,ループ変数が各反復で再利用されるため, req 変数がすべてのゴルーチンで共有されることです。それは私たちが望むものではありません。 req がゴルーチンごとに一意であることを確認する必要があります。これを行う 1 つの方法は, req の値を引数としてゴルーチンのクロージャーに渡すことです。

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

このバージョンと以前のバージョンを比較して,クロージャーの宣言方法と実行方法の違いを確認する。別の解決策は,次の例のように,同じ名前の新しい変数を作成することです。

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

書くのは奇妙に思えるかもしれません

req := req

しかし, Go でこれを行うのは合法的で慣用的です。同じ名前の新しいバージョンの変数を取得し,ループ変数を意図的にローカルにシャドウイングしますが,各ゴルーチンに固有です。

サーバーを作成するという一般的な問題に戻ると,リソースを適切に管理する別のアプローチは,リクエストチャンネルからのすべての読み取りを一定数の 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.
}

Channels of channels

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 システムのフレームワークであり,ミューテックスはありません。

Parallelization

これらのアイデアの別の用途は,複数の 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 の値ですが,同様の名前のシェル環境変数を設定するか,正の数で関数を呼び出すことでオーバーライドできます。ゼロで呼び出すと,値が照会されます。したがって,ユーザーのリソースリクエストを尊重する場合は,次のように記述する必要があります。

var numCPU = runtime.GOMAXPROCS(0)

Be sure not to confuse the ideas of concurrency—structuring a program as independently executing components—and parallelism—executing calculations in parallel for efficiency on multiple CPUs. Although the concurrency features of Go can make some problems easy to structure as parallel computations, Go is a concurrent language, not a parallel one, and not all parallelization problems fit Go's model. For a discussion of the distinction, see the talk cited in this blog post.

A leaky buffer

The tools of concurrent programming can even make non-concurrent ideas easier to express. Here's an example abstracted from an RPC package. The client goroutine loops receiving data from some source, perhaps a network. To avoid allocating and freeing buffers, it keeps a free list, and uses a buffered channel to represent it. If the channel is empty, a new buffer gets allocated. Once the message buffer is ready, it's sent to the server on 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 句は,他のケースの準備ができていないときに実行されます。つまり, selects がブロックすることはありません。) バッファリングされたチャンネルとブックキーピングのガベージコレクターに依存して,ほんの数行でバケツフリーリストを取得します。

Errors

多くの場合,ライブラリルーチンは,呼び出し元に何らかのエラー表示を返す必要があります。前述のとおり, 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()
}

PathErrorError は,次のような文字列を生成します。

open /etc/passwx: no such file or directory

問題のあるファイル名,操作,およびトリガーされたオペレーティングシステムエラーを含むこのようなエラーは,それを引き起こした呼び出しから遠く離れて表示された場合でも役立ちます。単純な " そのようなファイルやディレクトリがない " よりもはるかに有益です。

実行可能な場合,エラー文字列は,エラーを生成した操作またはパッケージに名前を付ける接頭辞を付けるなどして,その原因を特定する必要があります。たとえば,パッケージ image では,不明な形式によるデコードエラーの文字列表現は "image: 不明な形式 " です。

正確なエラーの詳細を気にする呼び出し元は,型スイッチまたは型アサーションを使用して,特定のエラーを探し,詳細を抽出できます。 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 になり, enil になります。成功した場合, ok は true になります。これは,エラーの型が *os.PathError であり,次に e であることを意味します。エラーの詳細を調べることができます。

Panic

呼び出し元にエラーを報告する通常の方法は,追加の戻り値として error を返すことです。正規の Read メソッドはよく知られたインスタンスです。バイトカウントと error を返します。しかし,エラーが回復不能な場合はどうでしょうか ? プログラムが単に続行できない場合があります。

この目的のために,プログラムを停止する実行時エラーを実際に作成する組み込み関数 panic があります (ただし,次のセクションを参照する) 。この関数は,任意の型の 1 つの引数を取ります。多くの場合,プログラムの終了時に出力される文字列です。また,無限ループの終了など,不可能なことが発生したことを示す方法でもあります。

// 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 を呼び出すと,巻き戻しが停止し, 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< を使用するライブラリルーチンを呼び出すことができます。 /code> 失敗せず。例として, safelyDo の遅延関数は, recover を呼び出す前にロギング関数を呼び出し,そのロギングコードはパニック状態の影響を受けずに実行されます。

回復パターンを適切に配置すると, do 関数 (およびそれが呼び出すもの) は, panic を呼び出すことで,悪い状況からきれいに抜け出すことができます。このアイデアを使用して,複雑なソフトウェアのエラー処理を簡素化できます。ローカルエラー型で panic を呼び出して解析エラーを報告する, regexp パッケージの理想的なバージョンを見てみましょう。 Errorerror メソッド,および 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 と同じ名前を持つことは問題ありません。 type) を使用すると,解析スタックを手動で巻き戻すことを心配せずに,解析エラーを簡単に報告できます。

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

このパターンは便利ですが,パッケージ内でのみ使用する必要があります。 Parse は,内部の panic 呼び出しを error 値に変換します。 panics をクライアントに公開しません。それは従うべき良いルールです。

ところで,実際のエラーが発生した場合,この再パニックイディオムはパニック値を変更します。ただし,クラッシュレポートには元の障害と新しい障害の両方が表示されるため,問題の根本原因は引き続き表示されます。したがって,通常はこの単純な再パニックアプローチで十分です。結局,クラッシュになりますが,元の値のみを表示する場合は,もう少しコードを記述して,予期しない問題をフィルタリングし,元のエラーで再パニックすることができます。それはリーダーのための演習として残されました。

A web server

完全な Go プログラムである Web サーバーで終了しましょう。これは実際には一種の Web リサーバーです。 Google は, chart.apis.google.com で,データをチャートやグラフに自動フォーマットするサービスを提供しています。ただし,データをクエリとして URL に挿入する必要があるため,インタラクティブに使用するのは困難です。ここのプログラムは, 1 つのデータ形式へのより良いインターフェースを提供します。短いテキストを指定すると,チャートサーバーを呼び出して,テキストをエンコードするボックスのマトリックスである 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 つのスニペット {{ 。 }} は,テンプレートに提示されたデータ (クエリ文字列) を Web ページに表示するように指示します。 HTML テンプレートパッケージは自動的に適切なエスケープを提供するため,テキストを安全に表示できます。

テンプレート文字列の残りの部分は,ページが読み込まれたときに表示する HTML です。説明が速すぎる場合は,テンプレートパッケージの ドキュメント で詳細な説明を参照する。

そして,そこにあなたはそれを持っています : 数行のコードといくつかのデータ駆動型 HTML テキストの便利な Web サーバー。 Go は,数行で多くのことができるほど強力です。