3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【翻訳】Goの落とし穴集 (原題: 50 Shades of Go)

Last updated at Posted at 2023-08-17

少し前の記事ですが、勉強になったので、翻訳で共有させて頂きます。

元記事のタイトルは 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs です。(50 shades of ~ はFifty Shades of Grey という小説のもじりだと思います。)

※作者の方の許可も頂けました


概要

Goはシンプルで楽しい言語ですが、他の言語と同様に、いくつかの落とし穴があります… 落とし穴の多くは、必ずしもGo言語に責任があるとは言えません。これらのミスの中には、別の言語からGoに移る際に自然と引っかかってしまう罠もありますし、開発者の誤った思い込みや詳細の見落としに起因するものもあります。

これらの落とし穴の多くは、時間をかけて公式の仕様、Wiki、メーリングリストの議論、Rob Pikeによる多くの素晴らしい投稿やプレゼンテーション、ソースコードを読んで勉強すれば、明白に思えるかもしれません。しかし、すべての人が同じ方法で始めるわけではありませんし、それはそれで良いのです。Goを新たに学ぶ方々にとって、ここにある情報はコードのデバッグに何時間も費やすことを避ける手助けとなるでしょう。

完全な初心者:

中級の初心者:

上級の初心者:

Cgo (別名、勇敢な初心者):

開始の波括弧 ( { ) を別の行に配置することはできない

  • レベル: 初心者

他の多く波括弧( { ) の使用する言語では、それをどこに配置するかを選ぶことができます。 しかしGoは違います。 Goではありがたいことに、(トークンを先読みせず) 行末に自動的にセミコロンを挿入するので、こんな動作になるのです。 そうです、Goにはセミコロンがあります :-)

Fails:

package main

import "fmt"

func main()
{ //error, can't have the opening brace on a separate line
    fmt.Println("hello there!")
}

Compile Error:

/tmp/sandbox826898458/main.go:6: syntax error: unexpected semicolon or newline before {

Works:

package main

import "fmt"

func main() {
    fmt.Println("works!")
}

未使用変数

  • レベル: 初心者

Goでは、使用していない変数がある場合、コードのコンパイルは失敗します。しかし、例外もあります。関数内で宣言した変数は使用する必要がありますが、グローバル変数は未使用でも問題ありません。また関数引数も未使用でも問題ありません。

未使用変数に新しい値を代入しても、コードのコンパイルはまだ失敗します。コンパイラを満足させるためには、何らかの方法で変数の値を使用する必要があるのです。

Fails:

package main

var gvar int //not an error

func main() {
    var one int   //error, unused variable
    two := 2      //error, unused variable
    var three int //error, even though it's assigned 3 on the next line
    three = 3

    func(unused string) {
        fmt.Println("Unused arg. No compile error")
    }("what?")
}

Compile Errors:

/tmp/sandbox473116179/main.go:6: one declared and not used /tmp/sandbox473116179/main.go:7: two declared and not used /tmp/sandbox473116179/main.go:8: three declared and not used

Works:

package main

import "fmt"

func main() {
    var one int
    _ = one

    two := 2
    fmt.Println(two)

    var three int
    three = 3
    one = three

    var four int
    four = four
}

別の選択肢としては、未使用の変数をコメントアウトするか、削除することもできます :-)

未使用インポート

  • レベル: 初心者

あるパッケージをインポートした際に、そのパッケージのエクスポートされた関数、インターフェース、構造体、または変数のいずれも使用しない場合、コードのコンパイルは失敗します。

上記を使用せず、それでも本当にパッケージのインポートが必要な場合は、コンパイルを通すためにブランク識別子(_)をそのパッケージ名として使用することができます。ブランク識別子は、副作用のためにパッケージをインポートする際に使用されます。

翻訳メモ

Fails:

package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
}

Compile Errors:

/tmp/sandbox627475386/main.go:4: imported and not used: "fmt" /tmp/sandbox627475386/main.go:5: imported and not used: "log" /tmp/sandbox627475386/main.go:6: imported and not used: "time"

Works:

package main

import (
    _ "fmt"
    "log"
    "time"
)

var _ = log.Println

func main() {
    _ = time.Now
}

別の選択肢としては、未使用のインポートを削除するか、コメントアウトすることもできます :-) それには、 goimports が役立ちます。

短い変数宣言(:=)は関数内でのみ使用可能

  • レベル: 初心者

Fails:

package main

myvar := 1 //error

func main() {
}

Compile Error:

/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body

Works:

package main

var myvar = 1

func main() {
}

短い変数宣言(:=)を使用して変数を再宣言する

  • レベル: 初心者

単独の文で変数を再宣言することはできませんが、復数の変数を同時に宣言する場合で、少なくとも1つ新しい変数が含まれていれば、その再宣言は許可されます。

再宣言された変数は同じブロック内にあるべきです。そうでないと、シャドーイングされた変数を持つことになってしまいます。

Fails:

package main

func main() {
    one := 0
    one := 1 //error
}

Compile Error:

/tmp/sandbox706333626/main.go:5: no new variables on left side of :=

Works:

package main

func main() {
    one := 0
    one, two := 1,2

    one,two = two,one
}

短い変数宣言(:=)を使ってフィールド値の設定は出来ない

  • レベル: 初心者

Fails:

package main

import (
  "fmt"
)

type info struct {
  result int
}

func work() (int,error) {
    return 13,nil
}

func main() {
  var data info

  data.result, err := work() //error
  fmt.Printf("info: %+v\n",data)
}

Compile Error:

prog.go:18: non-name data.result on left side of :=

この落とし穴に対するチケットは存在するのですが、Rob Pikeは"今のまま"が気に入っているため、変更される可能性は低いです :-) 一時的な変数を使用するか、すべての変数を事前に宣言してから、普通の代入演算子を使用してください。

Works:

package main

import (
  "fmt"
)

type info struct {
  result int
}

func work() (int,error) {
    return 13,nil
  }

func main() {
  var data info

  var err error
  data.result, err = work() //ok
  if err != nil {
    fmt.Println(err)
    return
  }

  fmt.Printf("info: %+v\n",data) //prints: info: {result:13}
}

意図しない変数のシャドーイング

  • レベル: 初心者

短い変数宣言(:=)の構文は非常に便利(特に動的言語から来る人にとっては)で、これを普通の代入操作と同じように扱ってしまいがちです。もし、新しいコードブロック内でこのミスをすると、コンパイルエラーは出ませんが、アプリは期待通りの動作をしないでしょう。

package main

import "fmt"

func main() {
    x := 1
    fmt.Println(x)     //prints 1
    {
        fmt.Println(x) //prints 1
        x := 2
        fmt.Println(x) //prints 2
    }
    fmt.Println(x)     //prints 1 (bad if you need 2)
}

これは経験豊富なGoの開発者にとっても、陥りやすい罠です。犯しやすいミスで、見つけるのが難しいこともあります。

vet コマンドを使えば、これらの問題の一部を見つけることが出来ます。 vet コマンドは、デフォルトでは シャドーイングされた変数のチェックを実行しないので、忘れずに -shadow フラグを付けましょう: go tool vet -shadow your_file.go

また、 vet コマンドはすべてのシャドーイングされた変数を報告するわけではないので注意が必要です。より厳密にシャドーイングされた変数の検出を行いたい場合、 go-nyet を使用してください。

明示的な型を持たない変数を"nil"では初期化出来ない

  • レベル: 初心者

"nil"識別子は、インターフェース、関数、ポインタ、マップ、スライス、チャネルの"ゼロ値"として使用することができます。もし変数の型を指定しないと、コンパイルは失敗します。なぜなら、型を推測することができないからです。

Fails:

package main

func main() {
    var x = nil //error

    _ = x
}

Compile Error:

/tmp/sandbox188239583/main.go:4: use of untyped nil

Works:

package main

func main() {
    var x interface{} = nil

    _ = x
}

"nil" スライスとマップの使用

  • レベル: 初心者

"nil" スライスにアイテムを追加することは問題ありませんが、マップで同じことをするとランタイム時にパニックが発生します。

Works:

package main

func main() {
    var s []int
    s = append(s,1)
}

Fails:

package main

func main() {
    var m map[string]int
    m["one"] = 1 //error

}

マップの容量

  • レベル: 初心者

マップ作成時にマップの容量を指定することはできますが、マップに cap() 関数を使用することはできません。

Fails:

package main

func main() {
    m := make(map[string]int,99)
    cap(m) //error
}

Compile Error:

/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap

文字列を"nil"にすることはできない

  • レベル: 初心者

これは、"nil" 識別子を文字列変数に代入することに慣れている開発者にとっての罠です。

Fails:

package main

func main() {
    var x string = nil //error

    if x == nil { //error
        x = "default"
    }
}

Compile Errors:

/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)

Works:

package main

func main() {
    var x string //defaults to "" (zero value)

    if x == "" {
        x = "default"
    }
}

配列の関数引数

  • レベル: 初心者

CやC++の開発者にとって、配列はポインタです。関数に配列を渡すとき、その関数は同じメモリの場所を参照するので、元データを更新することができます。しかし、Goの配列は値です。そのため、関数に配列を渡すと、関数は元の配列データのコピーを取得します。これは配列のデータを更新しようとしていると、問題となります。

package main

import "fmt"

func main() {
    x := [3]int{1,2,3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])
}

元の配列データを更新する必要がある場合、配列のポインタ型を使用してください。

package main

import "fmt"

func main() {
    x := [3]int{1,2,3}

    func(arr *[3]int) {
        (*arr)[0] = 7
        fmt.Println(arr) //prints &[7 2 3]
    }(&x)

    fmt.Println(x) //prints [7 2 3]
}

もう一つの選択肢はスライスを使用することです。関数がスライス変数のコピーを取得しても、その変数は依然として元データを参照しています。

package main

import "fmt"

func main() {
    x := []int{1,2,3}

    func(arr []int) {
        arr[0] = 7
        fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [7 2 3]
}

スライスと配列で"range"節を使ったときの予期せぬ値

  • レベル: 初心者

この落とし穴は、他の言語で"for-in"や"foreach"文に慣れている場合に遭遇しやすいです。Goの"range"節は別物で、これは二つの値を生成します: 1つ目の値はアイテムのインデックスであり、2つ目の値はアイテムのデータです。

Bad:

package main

import "fmt"

func main() {
    x := []string{"a","b","c"}

    for v := range x {
        fmt.Println(v) //prints 0, 1, 2
    }
}

Good:

package main

import "fmt"

func main() {
    x := []string{"a","b","c"}

    for _, v := range x {
        fmt.Println(v) //prints a, b, c
    }
}

スライスと配列は一次元

  • レベル: 初心者

Goは多次元の配列やスライスをサポートしているように見えるかもしれませんが、そうとも言えません。とはいえ、配列の配列やスライスのスライスを作成することは可能です。しかし、動的な多次元配列に依存する数値計算アプリケーションでは、パフォーマンスや複雑さの面で理想的とは言えません。

動的な多次元配列の作成は、生の一次元配列、"独立した"スライスのスライス、および"共有データ"のスライスのスライスを使用して実現できます。

生の一次元配列を使用する場合、インデックスによる値の取得、境界のチェック、および配列の拡張が必要なときのメモリ再割り当て、は実装者が責任を持って行う必要があります。

"独立した"スライスのスライスを使用する場合、二段階のプロセスを踏みます。最初に、外側のスライスを作成します。次に、各内部スライスを割り当てます。この内部のスライスは互いに独立していて、他の内部スライスに影響を与えることなく、それらを拡大したり縮小したりすることができます。

package main

func main() {
    x := 2
    y := 4

    table := make([][]int,x)
    for i:= range table {
        table[i] = make([]int,y)
    }
}

"共有データ"のスライスを使用して動的な多次元配列を作成する場合、3つのステップが必要です。まず、生のデータを保持するデータ"コンテナ"スライスを作成する必要があります。次に、外側のスライスを作成します。最後に、生のデータスライスを再スライスして、各内部のスライスを初期化します。

package main

import "fmt"

func main() {
    h, w := 2, 4

    raw := make([]int,h*w)
    for i := range raw {
        raw[i] = i
    }
    fmt.Println(raw,&raw[4])
    //prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>

    table := make([][]int,h)
    for i:= range table {
        table[i] = raw[i*w:i*w + w]
    }

    fmt.Println(table,&table[1][0])
    //prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}

Goの多次元配列/多次元スライスに関する仕様やプロポーザルは存在しますが、現時点では優先度は低いようです。

存在しないマップキーへのアクセス

  • レベル: 初心者

これは、(他の言語のように)"nil"が返ってくることを期待する開発者にとっての落とし穴です。対応するデータ型の"ゼロ値"が"nil"である場合、返される値は"nil"になりますが、他のデータ型では異なる値になります。マップレコードが存在するかどうかを判断するために、適切な"ゼロ値"を確認することは可能ですが、常に信頼出来る方法ではありません(例えば、"ゼロ値"がfalseであるブール値のマップの場合、どうしますか?)。指定したキーの値が存在するかを知る最も確実な方法は、マップアクセス時に返される2つ目の値を確認することです。

Bad:

package main

import "fmt"

func main() {
    x := map[string]string{"one":"a","two":"","three":"c"}

    if v := x["two"]; v == "" { //incorrect
        fmt.Println("no entry")
    }
}

Good:

package main

import "fmt"

func main() {
    x := map[string]string{"one":"a","two":"","three":"c"}

    if _,ok := x["two"]; !ok {
        fmt.Println("no entry")
    }
}

文字列は不変

  • レベル: 初心者

文字列内の個別の文字を、インデックス演算子を使って更新しようとすると失敗します。文字列は読み取り専用のバイトスライス(いくつか追加的な特性があります)です。文字列を更新する必要がある場合、代りにバイトスライスを使用して、必要に応じて文字列型に変換します。

Fails:

package main

import "fmt"

func main() {
    x := "text"
    x[0] = 'T'

    fmt.Println(x)
}

Compile Error:

/tmp/sandbox305565531/main.go:7: cannot assign to x[0]

Works:

package main

import "fmt"

func main() {
    x := "text"
    xbytes := []byte(x)
    xbytes[0] = 'T'

    fmt.Println(string(xbytes)) //prints Text
}

ただし、これは文字列内の文字を更新する本当に正しい方法ではないので注意して下さい。なぜかというと、文字によっては1文字で複数のバイトに格納される場合があるからです。テキスト文字列を更新する必要がある場合、最初に文字列をruneスライスに変換しましょう。ただし、runeスライス使った場合でも、アキュートアクセントを持つ文字などの場合に、単一の文字が複数のruneにまたがることがあります。この「文字」の複雑で曖昧な性質が、Goの文字列がバイトシーケンスとして表される理由です。

文字列とバイトスライスの変換

  • レベル: 初心者

文字列をバイトスライスに変換すると(そしてその逆も)、元のデータの完全なコピーが取得されることになります。これは、他の言語におけるキャスト操作や、Goでバイトスライスを再スライスしたときに、新しいスライス変数が元のバイトスライスの内部にある配列を参照しているのとは異なります。

翻訳メモ

Goには []byte <-> string の変換時に、余分なメモリ割り当てを回避するため、幾つかの最適化がなされています。(さらなる最適化も予定されています)。

1つ目の最適化は、 map[string] のマップ値の検索で、 []byte をキーとして使用する場合です: m[string(key)] 。このケースでは、コピーによるメモリ割り当てが回避されます。

2つ目の最適化は、文字列をバイトスライスに変換する for range 節です。:for i,v := range []byte(str) {...}。 このケースでも同様に、コピーによる追加のメモリ割り当てを回避します。

文字列とインデックス演算子

  • レベル: 初心者

文字列に対するインデックス演算子は、(他の言語のように)文字を返すのではなく、バイト値を返します。

package main

import "fmt"

func main() {
    x := "text"
    fmt.Println(x[0]) //print 116
    fmt.Printf("%T",x[0]) //prints uint8
}

特定の文字列の"文字"(ユニコードのコードポイント/ルーン)にアクセスする必要がある場合、 for range 節を使用してください。公式の"unicode/utf8"パッケージや、実験的なutf8stringパッケージ(golang.org/x/exp/utf8string)も役立ちます。utf8stringパッケージには便利な At() 関数が含まれています。また、文字列をルーンのスライスに変換するのも一つの選択肢です。

文字列が常にUTF8であるとは限らない

  • レベル: 初心者

文字列の値は、UTF8テキストである必要はありません。任意のバイトを含むことも可能です。文字列がUTF8であるのは、文字列リテラルが使用されるときだけです。また、文字列リテラルを使う場合でも、エスケープシーケンスを使用すれば他のデータを含めることは可能です。

文字列がUTF8テキストかどうかを知るためには、"unicode/utf8" パッケージの ValidString() 関数を使用してください。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    data1 := "ABC"
    fmt.Println(utf8.ValidString(data1)) //prints: true

    data2 := "A\xfeC"
    fmt.Println(utf8.ValidString(data2)) //prints: false
}

文字列の長さ

  • レベル: 初心者

あなたがPythonの開発者で、以下のようなコードがあるとしましょう:

data = u''
print(len(data)) #prints: 1

これを類似のGoのコードスニペットに変換すると、驚くかもしれません。

package main

import "fmt"

func main() {
    data := "♥"
    fmt.Println(len(data)) //prints: 3
}

Pythonではユニコード文字列の文字数を取得できるのに対して、Goの組み込みの len() 関数はバイト数を返します。

Goで同じ結果を得るためには、"unicode/utf8" パッケージの RuneCountInString() 関数を使用してください。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    data := "♥"
    fmt.Println(utf8.RuneCountInString(data)) //prints: 1

厳密には、 RuneCountInString() 関数は文字数を返すわけではありません。なぜなら、1つの文字が複数のルーンに跨る場合があるからです。

package main

import (
    "fmt"
    "unicode/utf8"
)

func main() {
    data := "é"
    fmt.Println(len(data))                    //prints: 3
    fmt.Println(utf8.RuneCountInString(data)) //prints: 2
}

スライス、配列、マップのリテラルを複数行で記述する場合のカンマの欠落

  • レベル: 初心者

Fails:

package main

func main() {
    x := []int{
    1,
    2 //error
    }
    _ = x
}

Compile Errors:

/tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline in composite literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }

Works:

package main

func main() {
    x := []int{
    1,
    2,
    }
    x = x

    y := []int{3,4,} //no error
    y = y
}

宣言を1行にまとめる場合、末尾のカンマを残しておいてもコンパイラエラーは発生しません。

log.Fatallog.Panic はログを取るだけではない

  • レベル: 初心者

一般的にログライブラリは、幾つかの異なるログレベルを提供します。それらログライブラリとGoのlogパッケージが異なるのは、このパッケージに含まれる Fatal*()Panic*() 関数を呼び出すと、ログ出力以外のこともやってくれる点です。 アプリがこれらの関数を呼び出すと、Goはアプリを終了させます :-)

package main

import "log"

func main() {
    log.Fatalln("Fatal Level: log entry") //app exits here
    log.Println("Normal Level: log entry")
}

組み込みのデータ構造に対する操作は同期されない

  • レベル: 初心者

Goはネイティブで並行処理をサポートする多くの機能を持っています。しかし、その中に並行安全なデータコレクションは含まれていません :-) データコレクションの更新がアトミック(不可分)であることを保証するのは実装者の責任です。ゴルーチンとチャネルはこれらのアトミック操作を実装するための推奨される方法ですが、アプリの実装によっては"sync"パッケージを活用することもできます。

文字列をrange節でループするときのループ変数

  • レベル: 初心者

rangeが返す2つの変数のうちの、1つ目の値であるインデックス値は、2つ目の値として返される現在の"文字"(ユニコードのコードポイント/ルーン)の最初のバイトのインデックスです。他の言語のように、現在の"文字"のインデックスではありません。実際の文字は複数のルーンで表される場合もあるので注意しましょう。また、文字を一つずつ扱う必要がある場合は、"norm"パッケージ(golang.org/x/text/unicode/norm)を確認してみましょう。

文字列変数を持つ for range 節は、データをUTF8テキストとして解釈しようとします。解釈できないバイトシーケンスについては、実際のデータの代わりに0xfffdルーン(別名ユニコード置換文字)を返します。文字列変数に任意の(非UTF8テキスト)データが格納されているケースで、すべての格納されているデータを(ユニコード置換文字に変換せずに)そのまま取得するためには、文字列をバイトスライスに変換してループしましょう。

package main

import "fmt"

func main() {
    data := "A\xfe\x02\xff\x04"
    for _,v := range data {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok)

    fmt.Println()
    for _,v := range []byte(data) {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfe 0x2 0xff 0x4 (good)
}

"for range"節でマップをループする

  • レベル: 初心者

これは、マップをループする際にアイテムが特定の順序(たとえば、キーの値順など)で出現すると思い込んでいると陥る落とし穴です。マップに対するループ処理は、毎回異なる結果になります。Goのランタイムは、さらに一歩進んでいて、マップのループ処理の順序をランダム化しようと努めます。しかしそれが常に成功するわけではないので、たまたま連続して同一順序でのマップのループ処理の結果が得られることもあります。5回、同一順序でマップのループ処理が行われても驚かないようにしましょう。

package main

import "fmt"

func main() {
    m := map[string]int{"one":1,"two":2,"three":3,"four":4}
    for k,v := range m {
        fmt.Println(k,v)
    }
}

Go Playground( https://play.golang.org/ )を使用する場合、コードに変更を加えない限り再コンパイルされないので、常に同じ結果が得られます。

"switch"文におけるフォールスルーの挙動

  • レベル: 初心者

"switch"文の中の"case"ブロックは、デフォルトでbreakします。これは、デフォルトで次の"case"ブロックへフォールスルーする他の言語とは異なる点です。

package main

import "fmt"

func main() {
    isSpace := func(ch byte) bool {
        switch(ch) {
        case ' ': //error
        case '\t':
            return true
        }
        return false
    }

    fmt.Println(isSpace('\t')) //prints true (ok)
    fmt.Println(isSpace(' '))  //prints false (not ok)
}

"case"ブロックをフォールスルーさせるには、"case"ブロックの最後に"fallthrough"ステートメントを使用します。もしくは、"case"ブロック内で式リストを使用してswitch文を書き直すこともできます。

package main

import "fmt"

func main() {
    isSpace := func(ch byte) bool {
        switch(ch) {
        case ' ', '\t':
            return true
        }
        return false
    }

    fmt.Println(isSpace('\t')) //prints true (ok)
    fmt.Println(isSpace(' '))  //prints true (ok)
}

インクリメントとデクリメント

  • レベル: 初心者

多くの言語にはインクリメントとデクリメント演算子があります。しかし、他の言語とは異なり、Goはこの演算子を先に置く書き方をサポートしていません。また、この演算子を式の中で使用することもできません。

Fails:

package main

import "fmt"

func main() {
    data := []int{1,2,3}
    i := 0
    ++i //error
    fmt.Println(data[i++]) //error
}

Compile Errors:

/tmp/sandbox101231828/main.go:8: syntax error: unexpected + /tmp/sandbox101231828/main.go:9: syntax error: unexpected +, expecting :

Works:

package main

import "fmt"

func main() {
    data := []int{1,2,3}
    i := 0
    i++
    fmt.Println(data[i])
}

ビット否定演算子

  • レベル: 初心者

多くの言語では、NOTの単項演算子(別名、ビット補数演算子)として ~ を使用します。しかし、GoではそれをXOR演算子 ( ^ ) で代用します。

Fails:

package main

import "fmt"

func main() {
    fmt.Println(~2) //error
}

Compile Error:

/tmp/sandbox965529189/main.go:6: the bitwise complement operator is ^

Works:

package main

import "fmt"

func main() {
    var d uint8 = 2
    fmt.Printf("%08b\n",^d)
}

Goでは、XOR演算子としても ^ を使用するので、混乱する人もいるかも知れません。

単項のNOT演算(例:NOT 0x02)を表現したい場合、二項のXOR演算(例:0x02 XOR 0xff)で表現することができます。そう考えると、単項のNOT演算を表すために ^ が再利用されることも納得出来るかもしれません。

Goには、'AND NOT'のビット演算子 (&^) もあり、これがNOT演算子の分かりにくさに拍車をかけています。この演算子は、 A AND (NOT B) を括弧省いて書くための、特別な機能/ハックのように見えます。

package main

import "fmt"

func main() {
    var a uint8 = 0x82
    var b uint8 = 0x02
    fmt.Printf("%08b [A]\n",a)
    fmt.Printf("%08b [B]\n",b)

    fmt.Printf("%08b (NOT B)\n",^b)
    fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n",b,0xff,b ^ 0xff)

    fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n",a,b,a ^ b)
    fmt.Printf("%08b & %08b = %08b [A AND B]\n",a,b,a & b)
    fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n",a,b,a &^ b)
    fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n",a,b,a & (^b))
}

演算子の優先順位の違い

  • レベル: 初心者

上の"ビットクリア"演算子 ( &^ ) は例外ですが、Goには他の多くの言語と同様、標準的な演算子があります。しかし、その演算子の優先順位は必ずしも他の言語と同じではありません。

package main

import "fmt"

func main() {
    fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n",0x2 & 0x2 + 0x4)
    //prints: 0x2 & 0x2 + 0x4 -> 0x6
    //Go:    (0x2 & 0x2) + 0x4
    //C++:    0x2 & (0x2 + 0x4) -> 0x2

    fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n",0x2 + 0x2 << 0x1)
    //prints: 0x2 + 0x2 << 0x1 -> 0x6
    //Go:     0x2 + (0x2 << 0x1)
    //C++:   (0x2 + 0x2) << 0x1 -> 0x8

    fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n",0xf | 0x2 ^ 0x2)
    //prints: 0xf | 0x2 ^ 0x2 -> 0xd
    //Go:    (0xf | 0x2) ^ 0x2
    //C++:    0xf | (0x2 ^ 0x2) -> 0xf
}

エクスポートされていない構造体のフィールドはエンコードされない

  • レベル: 初心者

構造体を(json、xml、gob等に)エンコードする際、構造体のフィールド名が小文字始まりだと、そのフィールドはエンコード対象となりません。そのため、再度その構造体をデコードすると、それらのエクスポートされていないフィールドには、ゼロ値が入ります。

package main

import (
    "fmt"
    "encoding/json"
)

type MyData struct {
    One int
    two string
}

func main() {
    in := MyData{1,"two"}
    fmt.Printf("%#v\n",in) //prints main.MyData{One:1, two:"two"}

    encoded,_ := json.Marshal(in)
    fmt.Println(string(encoded)) //prints {"One":1}

    var out MyData
    json.Unmarshal(encoded,&out)

    fmt.Printf("%#v\n",out) //prints main.MyData{One:1, two:""}
}

アクティブなゴルーチンがあってもアプリは終了する

  • レベル: 初心者

アプリはすべてのゴルーチンが完了するのを待つわけではありません。これは、一般的に初心者によくあるミスです。誰もがどこかからスタートしますので、初心者のミスを恥じる必要はありません :-)

package main

import (
    "fmt"
    "time"
)

func main() {
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        go doit(i)
    }
    time.Sleep(1 * time.Second)
    fmt.Println("all done!")
}

func doit(workerId int) {
    fmt.Printf("[%v] is running\n",workerId)
    time.Sleep(3 * time.Second)
    fmt.Printf("[%v] is done\n",workerId)
}

出力される結果:

[0] is running [1] is running all done!

最も一般的な対応方法は、"WaitGroup"変数を使用することです。これにより、メインのゴルーチンはすべてのワーカーゴルーチンが終了するのを待つことができます。もしアプリ内に、メッセージ処理ループを持ち、長時間実行されるワーカーがある場合、終了するタイミングをそれらのゴルーチンに伝える方法も必要です。これに対しては、各ワーカーに"kill"メッセージを送信することができます。また、別の選択肢としては、すべてのワーカーが受信しているチャネルを閉じることもできます。これは、一度にすべてのゴルーチンにシグナルを送るシンプルな方法です。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    done := make(chan struct{})
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doit(i,done,wg)
    }

    close(done)
    wg.Wait()
    fmt.Println("all done!")
}

func doit(workerId int,done <-chan struct{},wg sync.WaitGroup) {
    fmt.Printf("[%v] is running\n",workerId)
    defer wg.Done()
    <- done
    fmt.Printf("[%v] is done\n",workerId)
}

この状態でアプリを起動すると、以下の出力が確認出来るでしょう:

[0] is running [0] is done [1] is running [1] is done

メインのゴルーチンが終了する前に、ワーカーが完了したようです。素晴らしい!しかし、同時に以下の出力結果も得られるはずです:

fatal error: all goroutines are asleep - deadlock!

よろしくない結果ですね :-) 何が起こっているのでしょうか? なぜデッドロック?ワーカーは終了し、 wg.Done() を実行したのだから、アプリは正常に動くべきだ。

実はデッドロックの原因は、各ワーカーが元の"WaitGroup"変数のコピーを取得するためです。ワーカーが wg.Done() を実行しても、メインのゴルーチン内の"WaitGroup"変数に影響を与えていないのです。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    done := make(chan struct{})
    wq := make(chan interface{})
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doit(i,wq,done,&wg)
    }

    for i := 0; i < workerCount; i++ {
        wq <- i
    }

    close(done)
    wg.Wait()
    fmt.Println("all done!")
}

func doit(workerId int, wq <-chan interface{},done <-chan struct{},wg *sync.WaitGroup) {
    fmt.Printf("[%v] is running\n",workerId)
    defer wg.Done()
    for {
        select {
        case m := <- wq:
            fmt.Printf("[%v] m => %v\n",workerId,m)
        case <- done:
            fmt.Printf("[%v] is done\n",workerId)
            return
        }
    }
}

これで正常に動くようになりました :-)

バッファリングされていないチャネルへの送信は、対象の受信者の準備が完了するとすぐに返される

  • レベル: 初心者

チャネルの送信側は、受信側がメッセージを処理するまでブロックされません。メッセージの送信側が処理を完了する前に、受信側のゴルーチンがメッセージを受けて処理を処理を完了しきれるかどうかは、実行環境によります。

package main

import "fmt"

func main() {
    ch := make(chan string)

    go func() {
        for m := range ch {
            fmt.Println("processed:",m)
        }
    }()

    ch <- "cmd.1"
    ch <- "cmd.2" //won't be processed
}
翻訳メモ
> The sender will not be blocked until your message is processed by the receiver.

この一文のは書き間違い?
確認中

クローズされたチャネルへの送信はパニックを引き起こす

  • レベル: 初心者

クローズされたチャネルからの受信は安全です。受信文での ok の返り値は、データが受信されなかったことを示すため false がセットされます。バッファリングされたチャネルから受信している場合、最初にバッファされたデータを取得し、バッファが空になったら ok の返り値は false となります。

クローズされたチャネルへのデータ送信はパニックを引き起こします。これはドキュメントに記載されている挙動ですが、Goの経験が浅い開発者は受信と同じ挙動を期待しやすく、初心者にとってあまり分かりやすいとは言えないでしょう。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func(idx int) {
            ch <- (idx + 1) * 2
        }(i)
    }

    //get the first result
    fmt.Println(<-ch)
    close(ch) //not ok (you still have other senders)
    //do other work
    time.Sleep(2 * time.Second)
}

こちらの修正方法は、アプリによって異なります。簡単な変更で済む場合もあれば、アプリの設計自体を変更する必要があるかもしれません。いずれにせよ、クローズされたチャネルにデータを送信しようとしないように、アプリを変更する必要があります。

上記のバグを含むサンプルコードは、特別なキャンセル用チャネルを使用して、残っているワーカーにその結果がもう必要ないことを伝えることで修正できます。

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    done := make(chan struct{})
    for i := 0; i < 3; i++ {
        go func(idx int) {
            select {
            case ch <- (idx + 1) * 2: fmt.Println(idx,"sent result")
            case <- done: fmt.Println(idx,"exiting")
            }
        }(i)
    }

    //get first result
    fmt.Println("result:",<-ch)
    close(done)
    //do other work
    time.Sleep(3 * time.Second)
}

"nil" チャネルの使用

  • レベル: 初心者

nil チャネルへの送信および受信操作は永遠にブロックされます。これはしっかりとドキュメントに記載されている挙動なのですが、新しいGoの開発者は驚くかもしれません。

package main

import (
    "fmt"
    "time"
)

func main() {
    var ch chan int
    for i := 0; i < 3; i++ {
        go func(idx int) {
            ch <- (idx + 1) * 2
        }(i)
    }

    //get first result
    fmt.Println("result:",<-ch)
    //do other work
    time.Sleep(2 * time.Second)
}

上記のコードを実行すると、次のランタイムエラーが表示されます: fatal error: all goroutines are asleep - deadlock!

このnilの挙動は、 select 文の case ブロックを動的に有効化・無効化する方法として利用できます。

package main

import "fmt"
import "time"

func main() {
    inch := make(chan int)
    outch := make(chan int)

    go func() {
        var in <- chan int = inch
        var out chan <- int
        var val int
        for {
            select {
            case out <- val:
                out = nil
                in = inch
            case val = <- in:
                out = outch
                in = nil
            }
        }
    }()

    go func() {
        for r := range outch {
            fmt.Println("result:",r)
        }
    }()

    time.Sleep(0)
    inch <- 1
    inch <- 2
    time.Sleep(3 * time.Second)
}

値レシーバーを持つメソッドは元の値を変更できない

  • レベル: 初心者

メソッドレシーバーは通常の関数引数のようなものです。レシーバーが値として宣言されている場合、そのメソッドはレシーバー引数のコピーを取得します。これはつまり、レシーバーに変更を加えても元の値には影響しない、ということです。ただし、レシーバー自体がmapやslice変数であり、そのコレクション内のアイテムを更新している場合や、更新しているフィールドがポインタである場合は、元の値に影響を及ぼします。

package main

import "fmt"

type data struct {
    num int
    key *string
    items map[string]bool
}

func (this *data) pmethod() {
    this.num = 7
}

func (this data) vmethod() {
    this.num = 8
    *this.key = "v.key"
    this.items["vmethod"] = true
}

func main() {
    key := "key.1"
    d := data{1, &key, make(map[string]bool)}

    fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
    //prints num=1 key=key.1 items=map[]

    d.pmethod()
    fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
    //prints num=7 key=key.1 items=map[]

    d.vmethod()
    fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
    //prints num=7 key=v.key items=map[vmethod:true]
}

HTTPレスポンスボディのクローズ

  • レベル: 中級者

標準のhttpライブラリを使用してリクエストを行うと、httpレスポンス変数が得られますが、レスポンスボディを読み取らない場合であっても、これは閉じる必要があります。空のレスポンスの場合でも、閉じる必要があるので注意が必要です。特にGoを始めたばかりの開発者は閉じるのを忘れがちです。

また、Goの初心者の中には、レスポンスボディを閉じようとして、間違った別でクローズ処理を書いてしまう開発者もいます。

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    defer resp.Body.Close()//not ok
    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

このコードは成功したリクエストに対しては動作しますが、httpリクエストが失敗すると resp 変数は nil になる可能性があります。そうなった場合、パニックが発生します。

httpレスポンスのエラーチェックの後に defer を使用してレスポンスボディを閉じるのが最も一般的なやり方です。

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    if err != nil {
        fmt.Println(err)
        return
    }

    defer resp.Body.Close()//ok, most of the time :-)
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

大抵の場合、httpリクエストが失敗すると、 resp 変数は nil となり、 err 変数は非nilとなります。しかし、リダイレクトの失敗の場合は、両方の変数が非nilとなります。これは、リークが発生する可能性があることを意味します。

このリークを修正するには、httpレスポンスの受取りと、エラーハンドリングの間に、非nilのレスポンスボディをクローズする処理を追加します。もしくは、1つの defer 呼び出しで、すべての失敗したリクエストと成功したリクエストのレスポンスボディをクローズします。

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    resp, err := http.Get("https://api.ipify.org?format=json")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(body))
}

初期の resp.Body.Close() の実装では、残っているレスポンスボディの読み取りと破棄も含まれていました。その為、http接続でのkeepaliveが有効になっている状況では、次のリクエストのためにコネクションが再利用されることが保証されていました。最新の実装では、httpクライアントの挙動は変わっています。現在は、残っているレスポンスボディの読み取りと破棄は、実装者の責任になっています。つまり、読み取りと破棄を行わなかった場合、コネクションは再利用されずに、閉じられる可能性があります。この小さな落とし穴は、Go 1.5のドキュメントに記載されることになっています。

もしhttp接続の再利用が重要なアプリの場合、レスポンス処理ロジックの最後に以下のようなコードを追加する必要があるかもしれません:

_, err = io.Copy(ioutil.Discard, resp.Body)

この操作が必要なのは、レスポンスボディ全体をすぐに読み取らないケースです。例えば以下のようなコードでJSON APIのレスポンスを処理している場合が該当するかもしれません:

json.NewDecoder(resp.Body).Decode(&data)

HTTP接続のクローズ

  • レベル: 中級者

HTTPサーバーの中には、ネットワーク接続を一定時間開いたままにするものがあります(これはHTTP 1.1の仕様およびサーバーの「keep-alive」の設定に基づいています)。Goの標準httpライブラリは、デフォルトでは接続先のHTTPサーバーが要求するまでネットワーク接続を閉じません。これは、特定の状況下では、アプリケーションがソケットやファイルディスクリプタを使い果たしてしまう可能性があることを意味しています。

リクエストが完了した後に接続を閉じるようhttpライブラリに指示するには、リクエスト変数の Close フィールドを true に設定します。

または、 Connection リクエストヘッダーを追加し、 close に設定することもできます。こうすると接続先HTTPサーバーも Connection: close ヘッダーで応答するはずです。httpライブラリは、この応答ヘッダーを検出すると、接続を閉じます。

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    req, err := http.NewRequest("GET","http://golang.org",nil)
    if err != nil {
        fmt.Println(err)
        return
    }

    req.Close = true
    //or do this:
    //req.Header.Add("Connection", "close")

    resp, err := http.DefaultClient.Do(req)
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(len(string(body)))
}

HTTP接続の再利用をグローバルに無効にすることもできます。これを行うには、カスタムのHTTPトランスポート設定を作成する必要があります。

package main

import (
    "fmt"
    "net/http"
    "io/ioutil"
)

func main() {
    tr := &http.Transport{DisableKeepAlives: true}
    client := &http.Client{Transport: tr}

    resp, err := client.Get("http://golang.org")
    if resp != nil {
        defer resp.Body.Close()
    }

    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(resp.StatusCode)

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(len(string(body)))
}

同一のHTTPサーバーに大量のリクエストを送信する場合、ネットワーク接続を開いたままにしておいても問題ありません。しかし、短時間で多くの異なるHTTPサーバーに1つや2つのリクエストを送信するようなアプリの場合、レスポンスを受け取った直後にネットワーク接続を閉じるのが良いでしょう。ファイルディスクリプタの制限を増やすのも良いアイデアかもしれません。とは言え、正しい解決策はアプリの実装によって異なります。

JSON Encoderは改行文字を追加する

  • レベル: 中級者

JSONをエンコードする関数のテストを書いている最中、予期していない値が返ってきてテストが失敗することに気づきました。何が起こったのでしょうか?もしJSON Encoderオブジェクトを使用しているなら、エンコードされたJSONオブジェクトの最後に追加の改行文字が入っているはずです。

package main

import (
  "fmt"
  "encoding/json"
  "bytes"
)

func main() {
  data := map[string]int{"key": 1}

  var b bytes.Buffer
  json.NewEncoder(&b).Encode(data)

  raw,_ := json.Marshal(data)

  if b.String() == string(raw) {
    fmt.Println("same encoded data")
  } else {
    fmt.Printf("'%s' != '%s'\n",raw,b.String())
    //prints:
    //'{"key":1}' != '{"key":1}\n'
  }
}

JSON Encoderオブジェクトはストリーミングのために設計されています。普通、JSONでのストリーミングは、改行によって区切られたJSONオブジェクトを扱うことを意味します。Encode関数が改行文字を追加するのはこのためです。この挙動はドキュメントに記載されていますが、見落とされるか忘れられがちです。

JSON パッケージはJSONのキーと文字列の値のHTML特殊文字をエスケープする

  • レベル: 中級者

この挙動はキュメントに記載はされているのですが、把握するにはJSONパッケージのドキュメント全体を慎重に読む必要があります。 ドキュメントの SetEscapeHTML メソッドの項目には、 &<> の文字のデフォルトのエンコードの挙動について記載があります。

この実装に関して、Goの開発チームは非常に残念な設計判断をしたと言えるでしょう。理由は幾つかあります。まず初めに、 json.Marshal コール時にこの挙動を無効化することはできません。2つ目に、これはセキュリティ機能としては不完全な実装です。なぜなら、この実装はXSSの脆弱性対策として、HTMLエンコードを行えばすべてのWebアプリケーションは十分に守られるという想定の上に成り立っているからです。実際のところデータが使用される状況は様々であり、各コンテキストにおいて独自のエンコード方法が必要となります。そして最後に、この実装はJSONの主なユースケースがWebページであると仮定しています。これは良くありません。なぜなら、設定系のライブラリやREST/HTTP APIがデフォルトで動作しなくなるからです。

package main

import (
  "fmt"
  "encoding/json"
  "bytes"
)

func main() {
  data := "x < y"

  raw,_ := json.Marshal(data)
  fmt.Println(string(raw))
  //prints: "x \u003c y" <- probably not what you expected

  var b1 bytes.Buffer
  json.NewEncoder(&b1).Encode(data)
  fmt.Println(b1.String())
  //prints: "x \u003c y" <- probably not what you expected

  var b2 bytes.Buffer
  enc := json.NewEncoder(&b2)
  enc.SetEscapeHTML(false)
  enc.Encode(data)
  fmt.Println(b2.String())
  //prints: "x < y" <- looks better
}

Goのチームへの提案… どうかこの挙動をオプトインにしてください。

インターフェースの値にJSONの数値をアンマーシャルする

  • レベル: 中級者

デフォルトでGoは、JSONデータをインターフェース型としてデコード/アンマーシャルするときに、JSON内の数値をfloat64の数字として扱います。これは、以下のコードがパニックを起こして失敗することを意味します:

package main

import (
  "encoding/json"
  "fmt"
)

func main() {
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  if err := json.Unmarshal(data, &result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status = result["status"].(int) //error
  fmt.Println("status value:",status)
}

Runtime Panic:

panic: interface conversion: interface is float64, not int

デコードしようとしているJSONの値が整数である場合、対応として、いくつかの選択肢があります。

選択肢1:そのままの浮動小数点の値を使用する :-)

選択肢2:浮動小数点の値を必要な整数型に変換する。

package main

import (
  "encoding/json"
  "fmt"
)

func main() {
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  if err := json.Unmarshal(data, &result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status = uint64(result["status"].(float64)) //ok
  fmt.Println("status value:",status)
}

選択肢3:JSONのアンマーシャルに Decoder 型を使用し、数値は Number インターフェース型で表すように設定する。

package main

import (
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  var decoder = json.NewDecoder(bytes.NewReader(data))
  decoder.UseNumber()

  if err := decoder.Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status,_ = result["status"].(json.Number).Int64() //ok
  fmt.Println("status value:",status)
}

Number の値の文字列表現を利用して、異なる数値型にアンマーシャルすることもできます:

package main

import (
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {
  var data = []byte(`{"status": 200}`)

  var result map[string]interface{}
  var decoder = json.NewDecoder(bytes.NewReader(data))
  decoder.UseNumber()

  if err := decoder.Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
  }

  var status uint64
  if err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status); err != nil {
    fmt.Println("error:", err)
    return
  }

  fmt.Println("status value:",status)
}

選択肢4:構造体の型を使用して、必要な数値型にマッピングする。

package main

import (
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {
  var data = []byte(`{"status": 200}`)

  var result struct {
    Status uint64 `json:"status"`
  }

  if err := json.NewDecoder(bytes.NewReader(data)).Decode(&result); err != nil {
    fmt.Println("error:", err)
    return
  }

  fmt.Printf("result => %+v",result)
  //prints: result => {Status:200}
}

選択肢5:値のデコードを遅らせる必要がある場合、数値を json.RawMessage 型にマッピングする構造体を使用する。 この方法は、JSONフィールドの型や構造が変わる可能性があるときの、条件付きのデコードを行う場合に役立ちます。

package main

import (
  "encoding/json"
  "bytes"
  "fmt"
)

func main() {
  records := [][]byte{
    []byte(`{"status": 200, "tag":"one"}`),
    []byte(`{"status":"ok", "tag":"two"}`),
  }

  for idx, record := range records {
    var result struct {
      StatusCode uint64
      StatusName string
      Status json.RawMessage `json:"status"`
      Tag string             `json:"tag"`
    }

    if err := json.NewDecoder(bytes.NewReader(record)).Decode(&result); err != nil {
      fmt.Println("error:", err)
      return
    }

    var sstatus string
    if err := json.Unmarshal(result.Status, &sstatus); err == nil {
      result.StatusName = sstatus
    }

    var nstatus uint64
    if err := json.Unmarshal(result.Status, &nstatus); err == nil {
      result.StatusCode = nstatus
    }

    fmt.Printf("[%v] result => %+v\n",idx,result)
  }
}

JSONの文字列値には、16進数やその他の非UTF8エスケープシーケンスを含めてはならない

  • レベル: 中級者

GoはJSONの文字列の値がUTF8でエンコードされていることを期待しています。これは、JSONの文字列に、任意のエスケープされた16進数バイナリデータを持つことができないことを意味します(また、バックスラッシュ文字もエスケープする必要があります)。これはどちらかというとJSONが抱える問題点で、それをGoが継承してしまったわけですが、Goのアプリ内でも頻繁に発生するので言及しておく意味はありそうです。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data string `json:"data"`
}

func main() {
  raw := []byte(`{"data":"\xc2"}`)
  var decoded config

  if err := json.Unmarshal(raw, &decoded); err != nil {
        fmt.Println(err)
    //prints: invalid character 'x' in string escape code
    }

}

Unmarshal/Decodeの呼び出しは、Goが16進数エスケープシーケンスを検出すると失敗します。文字列内にバックスラッシュを含める必要がある場合は、もう1つバックスラッシュを付けてエスケープしてください。16進数エンコードされたバイナリデータを使用したい場合、まずバックスラッシュをエスケープしてからJSONデコードし、その後でJSONデータの文字列に独自の16進数エスケープを適用してください。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data string `json:"data"`
}

func main() {
  raw := []byte(`{"data":"\\xc2"}`)

  var decoded config

  json.Unmarshal(raw, &decoded)

  fmt.Printf("%#v",decoded) //prints: main.config{Data:"\\xc2"}
  //todo: do your own hex escape decoding for decoded.Data
}

別の選択肢としては、JSONオブジェクト内でバイト配列(スライス)型を使用することができます。しかしこの場合、バイナリデータはbase64でエンコードする必要があります。

package main

import (
  "fmt"
  "encoding/json"
)

type config struct {
  Data []byte `json:"data"`
}

func main() {
  raw := []byte(`{"data":"wg=="}`)
  var decoded config

  if err := json.Unmarshal(raw, &decoded); err != nil {
          fmt.Println(err)
      }

  fmt.Printf("%#v",decoded) //prints: main.config{Data:[]uint8{0xc2}}
}

他に注意すべき点としては、ユニコードの置換文字(U+FFFD)が挙げられます。Goは無効なUTF8の代わりに置換文字を使用するので、Unmarshal/Decodeの呼び出しが失敗せずとも、取得する文字列の値が期待したものでない場合があります。

構造体、配列、スライス、およびマップの比較

  • レベル: 中級者

構造体の各フィールドが等価演算子(==)で比較できる場合、 等価演算子を使用して構造体変数を比較することができます。

package main

import "fmt"

type data struct {
    num int
    fp float32
    complex complex64
    str string
    char rune
    yes bool
    events <-chan string
    handler interface{}
    ref *byte
    raw [10]byte
}

func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",v1 == v2) //prints: v1 == v2: true
}

構造体のフィールドのいずれかが比較可能でない場合、等価演算子を使用するとコンパイル時にエラーが発生します。ちなみに配列が比較可能なのは、含まれるデータが比較可能な場合だけなので注意しましょう。

package main

import "fmt"

type data struct {
    num int                //ok
    checks [10]func() bool //not comparable
    doit func() bool       //not comparable
    m map[string] string   //not comparable
    bytes []byte           //not comparable
}

func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",v1 == v2)
}

Goは、比較演算子で比較できない変数を比較するために、いくつかのヘルパー関数を提供しています。

最も汎用的な方法は、reflectパッケージのDeepEqual()関数を使用することです。

package main

import (
    "fmt"
    "reflect"
)

type data struct {
    num int                //ok
    checks [10]func() bool //not comparable
    doit func() bool       //not comparable
    m map[string] string   //not comparable
    bytes []byte           //not comparable
}

func main() {
    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}

DeepEqual() が遅いということ(この処理速度の一点が採用の可否につながる場合もある訳ですが)を除いたとしても、 DeepEqual() には独自の注意点があります。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false
}

DeepEqual() は空のスライスを"nil"スライスと等しいとはみなしません。この挙動はは、 bytes.Equal() 関数とは異なります。 bytes.Equal() は"nil"と空のスライスを等しいとみなします。

package main

import (
    "fmt"
    "bytes"
)

func main() {
    var b1 []byte = nil
    b2 := []byte{}
    fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true
}

DeepEqual() はスライスの比較を常に完璧に行ってくれる訳ではありません。

package main

import (
    "fmt"
    "reflect"
    "encoding/json"
)

func main() {
    var str string = "one"
    var in interface{} = "one"
    fmt.Println("str == in:",str == in,reflect.DeepEqual(str, in))
    //prints: str == in: true true

    v1 := []string{"one","two"}
    v2 := []interface{}{"one","two"}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2))
    //prints: v1 == v2: false (not ok)

    data := map[string]interface{}{
        "code": 200,
        "value": []string{"one","two"},
    }
    encoded, _ := json.Marshal(data)
    var decoded map[string]interface{}
    json.Unmarshal(encoded, &decoded)
    fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded))
    //prints: data == decoded: false (not ok)
}

もし、バイトスライス(または文字列)がテキストデータを含んでいて、大文字小文字を区別せずに値を比較したい場合、"bytes"や"strings"のパッケージの ToUpper()ToLower() を使いたいと思うかも知れません(==、bytes.Equal()、またはbytes.Compare()での比較の前の話です)。これは英語のテキストでは機能しますが、他の多くの言語では機能しません。代わりに strings.EqualFold()bytes.EqualFold() を使用するべきでしょう。

もし、バイトスライスが秘密情報(例:暗号化ハッシュ、トークンなど)を含み、ユーザーからのデータと比較検証する必要がある場合、 reflect.DeepEqual()bytes.Equal() 、または bytes.Compare() を使用してはいけません。これらの関数を使用すると、アプリケーションはタイミング攻撃に対して脆弱となります。タイミング情報を漏らさないためには、'crypto/subtle'パッケージからの関数(例えば、 subtle.ConstantTimeCompare() )を使用してください。

Panicからの復旧

  • レベル: 中級者

recover() 関数は、panicをキャッチ/インターセプトするために使用できます。 recover() の呼び出しは、deferされた関数内で行われた場合のみ、期待通りの動作をします。

Incorrect:

package main

import "fmt"

func main() {
    recover() //doesn't do anything
    panic("not good")
    recover() //won't be executed :)
    fmt.Println("ok")
}

Works:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("recovered:", recover())
    }()

    panic("not good")
}

recover() の呼び出しは、deferされた関数内で直接呼び出された場合にのみ機能します。

Fails:

package main

import "fmt"

func doRecover() {
    fmt.Println("recovered =>",recover()) //prints: recovered => <nil>
}

func main() {
    defer func() {
        doRecover() //panic is not recovered
    }()

    panic("not good")
}

"range"節でのスライス、配列、マップの値の更新と参照

  • レベル: 中級者

"range"節で生成されるデータ値は、実際のコレクションの要素のコピーです。それらはオリジナルのアイテムへの参照ではありません。これは、値を更新してもオリジナルのデータが変更されないことを意味します。また、値のアドレスを取得しても、オリジナルのデータへのポインタを得ることはできません。

package main

import "fmt"

func main() {
    data := []int{1,2,3}
    for _,v := range data {
        v *= 10 //original item is not changed
    }

    fmt.Println("data:",data) //prints data: [1 2 3]
}

もとのコレクションのレコード値を更新する必要がある場合、インデックス演算子を使用してデータにアクセスしてください。

package main

import "fmt"

func main() {
    data := []int{1,2,3}
    for i,_ := range data {
        data[i] *= 10
    }

    fmt.Println("data:",data) //prints data: [10 20 30]
}

もしコレクションがポインタ値を保持している場合、話は少し違ってきます。このケースでは、"for range"節の2番目の値を使用してターゲットの場所に保存されているデータを更新することができます。 ただし、元のレコードが別の値を指すようにしたい場合は、依然としてインデックス演算子を使用する必要があります。

package main

import "fmt"

func main() {
    data := []*struct{num int} {{1},{2},{3}}

    for _,v := range data {
        v.num *= 10
    }

    fmt.Println(data[0],data[1],data[2]) //prints &{10} &{20} &{30}
}

スライス内の"隠れた"データ

  • レベル: 中級者

スライスを再スライスすると、新しいスライスは元のスライスの配列を参照します。この挙動を忘れてしまうと、予期せぬメモリ使用が生じる可能性があります。たとえば、アプリ内で一時的に大きなスライスを確保し、そのスライスから別の新しいスライスを作成して、元のスライスの一部分を参照する場合などです。

package main

import "fmt"

func get() []byte {
    raw := make([]byte,10000)
    fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
    return raw[:3]
}

func main() {
    data := get()
    fmt.Println(len(data),cap(data),&data[0]) //prints: 3 10000 <byte_addr_x>
}

この落とし穴を避けるためには、一時的なスライスから必要なデータをコピーしてください(再スライスするのではなく)。

package main

import "fmt"

func get() []byte {
    raw := make([]byte,10000)
    fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
    res := make([]byte,3)
    copy(res,raw[:3])
    return res
}

func main() {
    data := get()
    fmt.Println(len(data),cap(data),&data[0]) //prints: 3 3 <byte_addr_y>
}

スライスデータの"破損"

  • レベル: 中級者

ファイルのパス(スライスに保存されている)を書き換える必要があるとしましょう。各ディレクトリを参照するようにパスを再スライスして、最初のフォルダ名を変更し、その後で名前を組み合わせて新しいパスを作成します。

package main

import (
    "fmt"
    "bytes"
)

func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')
    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)
    path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB (not ok)

    fmt.Println("new path =>",string(path))
}

このコードは期待通りに動きませんでした。"AAAAsuffix/BBBBBBBBB"と出力されるはずが、"AAAAsuffix/uffixBBBB"になったのです。これは、両方のディレクトリのスライスが、元となったパスのスライスの中にある、同じ配列データを参照していたためです。つまりこれは、元のパスも変更されたことを意味します。アプリの実装によっては、この挙動も問題になる場合があります。

この問題は、新しいスライスを割り当てて必要なデータをコピーすることで修正できます。別の選択肢として、フルスライス式を使用することもできます。

package main

import (
    "fmt"
    "bytes"
)

func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')
    dir1 := path[:sepIndex:sepIndex] //full slice expression
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)
    path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB (ok now)

    fmt.Println("new path =>",string(path))
}

フルスライス式の最後のパラメータは、新しいスライスの容量を制御します。これにより、フルスライス式で作られたスライスにappendすると、dir2のスライスのデータを上書きするのではなく、新しいバッファの割り当てが発生します。

"古くなった" スライス

  • レベル: 中級者

複数のスライスが同じデータを参照することがあります。この状況は、例えば、既存のスライスから新しいスライスを作成するときに起こり得ます。アプリがこの挙動に依存しているとすれば、"古くなった"スライスについて考慮する必要があります。

リスライスして新たに作られたスライスの1つにデータを追加していくと、ある時点で元の配列がこれ以上の新しいデータを保持できなくなり、新しい配列の割り当てが発生します。このとき、他のスライスは古い配列(古いデータを含む)を指したままです。

import "fmt"

func main() {
    s1 := []int{1,2,3}
    fmt.Println(len(s1),cap(s1),s1) //prints 3 3 [1 2 3]

    s2 := s1[1:]
    fmt.Println(len(s2),cap(s2),s2) //prints 2 2 [2 3]

    for i := range s2 { s2[i] += 20 }

    //still referencing the same array
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [22 23]

    s2 = append(s2,4)

    for i := range s2 { s2[i] += 10 }

    //s1 is now "stale"
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [32 33 14]
}

型宣言とメソッド

  • レベル: 中級者

既存の(インターフェースでない)型から新しい型を定義して型宣言するとき、その既存の型に定義されたメソッドは継承されません。

Fails:

package main

import "sync"

type myMutex sync.Mutex

func main() {
    var mtx myMutex
    mtx.Lock() //error
    mtx.Unlock() //error
}

Compile Errors:

/tmp/sandbox106401185/main.go:9: mtx.Lock undefined (type myMutex has no field or method Lock) /tmp/sandbox106401185/main.go:10: mtx.Unlock undefined (type myMutex has no field or method Unlock)

元の型のメソッドが必要な場合、新しいstruct型を定義して、その中の匿名フィールドとして元の型を埋め込むことが出来ます。

Works:

package main

import "sync"

type myLocker struct {
    sync.Mutex
}

func main() {
    var lock myLocker
    lock.Lock() //ok
    lock.Unlock() //ok
}

また、インターフェースで型宣言した場合も、そのメソッドを保持します。

Works:

package main

import "sync"

type myLocker sync.Locker

func main() {
    var lock myLocker = new(sync.Mutex)
    lock.Lock() //ok
    lock.Unlock() //ok
}

"for switch" と "for select" のコードブロックから抜ける

  • レベル: 中級者

ラベルなしの "break" 文を使った場合に抜け出せるのは、内側の switch/select ブロックだけです。"return" 文が望ましくない状況では、外部ループにラベルを定義して"break"する方法を検討すると良いでしょう。

package main

import "fmt"

func main() {
    loop:
        for {
            switch {
            case true:
                fmt.Println("breaking out...")
                break loop
            }
        }

    fmt.Println("out!")
}

"goto" 文も同様に機能しますが…

"for" 文のループ変数とクロージャ

  • レベル: 中級者

これはGoで最もよくあるミスです。for文のループ変数は各ループで再利用されます。つまり、 for ループ内で作成される各クロージャ(別名、関数リテラル)は、同じ変数を参照することを意味します(そして、その変数の値を取得するのは、それらゴルーチンが実行を開始する時です)。

Incorrect:

package main

import (
    "fmt"
    "time"
)

func main() {
    data := []string{"one","two","three"}

    for _,v := range data {
        go func() {
            fmt.Println(v)
        }()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three
}

一番簡単な解決方法(ゴルーチン側に変更を加えない解決方法)は for ループのブロック内で、現在のループ変数の値をローカル変数に保存することです。

Works:

package main

import (
    "fmt"
    "time"
)

func main() {
    data := []string{"one","two","three"}

    for _,v := range data {
        vcopy := v //
        go func() {
            fmt.Println(vcopy)
        }()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

別の解決策としては、現在のループ変数を、無名関数のゴルーチンにパラメータとして渡す方法があります。

package main

import (
    "fmt"
    "time"
)

func main() {
    data := []string{"one","two","three"}

    for _,v := range data {
        go func(in string) {
            fmt.Println(in)
        }(v)
    }

    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

以下は、この落とし穴の少し複雑なバージョンです。

Incorrect:

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data := []field{{"one"},{"two"},{"three"}}

    for _,v := range data {
        go v.print()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: three, three, three
}

Works:

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data := []field{{"one"},{"two"},{"three"}}

    for _,v := range data {
        v := v
        go v.print()
    }

    time.Sleep(3 * time.Second)
    //goroutines print: one, two, three
}

以下のコードを実行したとき、何が表示されると思いますか(理由も考えてみましょう)?

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data := []*field{{"one"},{"two"},{"three"}}

    for _, v := range data {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

deferされた関数をコールしたときの引数の評価

  • レベル: 中級者

deferされた関数呼び出しの引数は、defer文が評価されるときに評価されます(関数が実際に実行されるときではありません)。メソッド呼び出しを遅延する場合も同じルールが適用されます。明示的なメソッドのパラメーターやクロージャ変数と同様に、構造体の値も保存されます。

package main

import "fmt"

func main() {
    var i int = 1

    defer fmt.Println("result =>",func() int { return i * 2 }())
    i++
    //prints: result => 2 (not ok if you expected 4)
}

関数がポインタ引数持つ場合は、 defer 文が評価された時にポインタだけを保存するので、後でポインタが指す値が変わることはありえます。

package main

import (
  "fmt"
)

func main() {
  i := 1
  defer func (in *int) { fmt.Println("result =>", *in) }(&i)

  i = 2
  //prints: result => 2
}

遅延された関数呼び出しの実行

  • レベル: 中級者

遅延された実行呼び出しは、その呼び出しを含む関数の終了時に(deferした順番と逆順に)実行されます。その呼び出しを含むコードブロックの終了時ではありません。遅延されたコード実行のルールと、変数スコープのルールを混同するのは、新しいGoの開発者が犯しやすい間違いです。これは、長い関数内に for 文があり、そのfor文のループ毎にリソースのクリーンアップを遅延実行しようとすると、問題になる可能性があります。

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    if len(os.Args) != 2 {
        os.Exit(-1)
    }

    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir(){
        os.Exit(-1)
    }

    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fi.Mode().IsRegular() {
            return nil
        }

        targets = append(targets,fpath)
        return nil
    })

    for _,target := range targets {
        f, err := os.Open(target)
        if err != nil {
            fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
            break
        }
        defer f.Close() //will not be closed at the end of this code block
        //do something with the file...
    }
}

この問題の解決方法の1つは、コードブロックを関数でラップすることです。

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    if len(os.Args) != 2 {
        os.Exit(-1)
    }

    start, err := os.Stat(os.Args[1])
    if err != nil || !start.IsDir(){
        os.Exit(-1)
    }

    var targets []string
    filepath.Walk(os.Args[1], func(fpath string, fi os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        if !fi.Mode().IsRegular() {
            return nil
        }

        targets = append(targets,fpath)
        return nil
    })

    for _,target := range targets {
        func() {
            f, err := os.Open(target)
            if err != nil {
                fmt.Println("bad target:",target,"error:",err)
                return
            }
            defer f.Close() //ok
            //do something with the file...
        }()
    }
}

もう一つの選択肢は、defer文を取り除くことです :-)

失敗した型アサーション

  • レベル: 中級者

型アサーションが失敗した場合、アサーション文で使用されたターゲット型の"ゼロ値"が返されます。これが変数のシャドーイングと混在すると、予期しない振る舞いが起こる可能性があります。

Incorrect:

package main

import "fmt"

func main() {
    var data interface{} = "great"

    if data, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",data)
    } else {
        fmt.Println("[not an int] value =>",data)
        //prints: [not an int] value => 0 (not "great")
    }
}

Works:

package main

import "fmt"

func main() {
    var data interface{} = "great"

    if res, ok := data.(int); ok {
        fmt.Println("[is an int] value =>",res)
    } else {
        fmt.Println("[not an int] value =>",data)
        //prints: [not an int] value => great (as expected)
    }
}

ブロックされたゴルーチンとリソースのリーク

  • レベル: 中級者

Rob Pikeは、2012年のGoogle I/Oの「Go Concurrency Patterns」というプレゼンテーションで、いくつかの基本的な並行性のパターンについて話しました。そのうちの1つに、多くのターゲットから最初の結果をフェッチする、というパターンがあります。

func First(query string, replicas ...Search) Result {
    c := make(chan Result)
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

この関数は、各検索レプリカごとにゴルーチンを起動します。各ゴルーチンは、その検索結果を結果チャネルに送信します。そしてこの関数は、結果チャネルからの最初の値を返却します。

このとき、他のゴルーチンからの結果はどうなるでしょうか? また、ゴルーチン自体はどうなるでしょうか?

First() 関数の結果チャネルはバッファリングされていません。これは、最初のゴルーチンのみが返されることを意味します。他のすべてのゴルーチンは、結果を送信しようとして立ち往生しています。つまり、復数のレプリカがある場合、この関数呼び出し毎にリソースがリークするということです。

リークを回避するためには、すべてのゴルーチンが終了することを確認する必要があります。解決策の1つとして考えられるのは、すべての結果を保持できるサイズの、バッファリングされた結果チャネルを使用することです。

func First(query string, replicas ...Search) Result {
    c := make(chan Result,len(replicas))
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

もう一つの考えられる案としては、 1つだけ値をバッファ出来る結果チャネルと default ケースを使った select 文を用意することです。 default ケースは、結果チャネルがメッセージを受信できない場合でも、ゴルーチンが停滞しないように保証します。

もしくは、ワーカーを中断するためのキャンセル専用チャネルを使うことも出来ます。

func First(query string, replicas ...Search) Result {
    c := make(chan Result)
    done := make(chan struct{})
    defer close(done)
    searchReplica := func(i int) {
        select {
        case c <- replicas[i](query):
        case <- done:
        }
    }
    for i := range replicas {
        go searchReplica(i)
    }

    return <-c
}

なぜ、あのプレゼン資料にバグが含まれていたでしょうか? Rob Pikeは単にスライドを複雑にしたくなかったのです。それは理解はできますが、新しいGoの開発者が、何も考えずにバグが含まれているコードをそのまま使ってしまうと問題になり得ます。

復数のゼロサイズ変数に同じアドレスが割り当てられる

  • レベル: 中級者

2つの異なる変数があった場合、それぞれには別のアドレスが割り当てられるべきですね?実は、Goでは必ずしもそうではありません :-) サイズがゼロの変数の場合、それぞれがメモリ内で全く同じアドレスを共有することがあります。

package main

import (
  "fmt"
)

type data struct {
}

func main() {
  a := &data{}
  b := &data{}

  if a == b {
    fmt.Printf("same address - a=%p b=%p\n",a,b)
    //prints: same address - a=0x1953e4 b=0x1953e4
  }
}

最初のiotaが必ずゼロから始まるわけではない

  • レベル: 中級者

iota 識別子はインクリメント演算子のように思えるかもしれません。新しい定数宣言を開始し、 最初に iota を使用するとゼロが得られ、2回目に使用すると1が得られる、といった具合です。しかし、この挙動が常に当てはまるわけではありません。

package main

import (
  "fmt"
)

const (
  azero = iota
  aone  = iota
)

const (
  info  = "processing"
  bzero = iota
  bone  = iota
)

func main() {
  fmt.Println(azero,aone) //prints: 0 1
  fmt.Println(bzero,bone) //prints: 1 2
}

実際には、 iota は定数宣言ブロック内で現在の行を指すインデックス演算子です。したがって、 定数宣言ブロック内で最初に iota を使った行が1行目でない場合、初期値はゼロになりません。

値インスタンスにおけるポインタレシーバーメソッドの使用

  • レベル: 上級者

変数の値がアドレス指定可能であれば、その値のポインタレシーバーのメソッドを呼び出すことは問題ありません。言い換えると、メソッドの値レシーバーのバージョンは、場合によっては必要がないということです。

ただし、すべての変数がアドレス指定可能なわけではありません。マップの要素はアドレス指定可能ではありません。インターフェースを通じて参照される変数もアドレス指定可能ではありません。

package main

import "fmt"

type data struct {
    name string
}

func (p *data) print() {
    fmt.Println("name:", p.name)
}

type printer interface {
    print()
}

func main() {
    d1 := data{"one"}
    d1.print() //ok

    var in printer = data{"two"} //error
    in.print()

    m := map[string]data {"x":data{"three"}}
    m["x"].print() //error
}

Compile Errors:

/tmp/sandbox017696142/main.go:21: cannot use data literal (type data) as type printer in assignment: data does not implement printer (print method has pointer receiver) /tmp/sandbox017696142/main.go:25: cannot call pointer method on m["x"] /tmp/sandbox017696142/main.go:25: cannot take the address of m["x"]

マップ値のフィールドの更新

  • レベル: 上級者

マップがstructの値を持つ場合、そのstructのフィールドを個別に更新することはできません。

Fails:

package main

type data struct {
    name string
}

func main() {
    m := map[string]data {"x":{"one"}}
    m["x"].name = "two" //error
}

Compile Error:

/tmp/sandbox380452744/main.go:9: cannot assign to m["x"].name

なぜ動かないかというと、これはマップの要素がアドレス指定可能でないためです。

さらに新しいGoの開発者を混乱させるのが、スライスの要素はアドレス指定可能、ということです。

package main

import "fmt"

type data struct {
    name string
}

func main() {
    s := []data {{"one"}}
    s[0].name = "two" //ok
    fmt.Println(s)    //prints: [{two}]
}

少し前まで、マップ要素の構造体フィールドを更新できるコンパイラ(gccgo)もあったのですが、その動作はすぐに修正されました :-) また、この機能はGo1.3に含まれる機能として検討もされていました。しかし、その時点でサポートするほど重要ではなかったため、現在もまだ開発のTODOリストに残っています。

この問題の最初の回避策は、一時的な変数を使用することです。

package main

import "fmt"

type data struct {
    name string
}

func main() {
    m := map[string]data {"x":{"one"}}
    r := m["x"]
    r.name = "two"
    m["x"] = r
    fmt.Printf("%v",m) //prints: map[x:{two}]
}

別の回避策としては、ポインタのマップを使用する方法があります。

package main

import "fmt"

type data struct {
    name string
}

func main() {
    m := map[string]*data {"x":{"one"}}
    m["x"].name = "two" //ok
    fmt.Println(m["x"]) //prints: &{two}
}

ちなみに、このコードを実行するとどうなるでしょう?

package main

type data struct {
    name string
}

func main() {
    m := map[string]*data {"x":{"one"}}
    m["z"].name = "what?" //???
}

"nil" インターフェースと "nil" インターフェースの値

  • レベル: 上級者

これはGoの中で二番目によく遭遇する落とし穴です。インターフェースは、ポインタのように見える場合があるかもしれませんが、ポインタではありません。インターフェース変数が"nil"となるのは、その型と値のフィールドが"nil"の時だけです。

インターフェースの型と値のフィールドは、そのインターフェース変数を作成するために使用される変数の型と値に基づいて設定されます。インターフェース変数が"nil"に等しいかどうかを確認しようとする際に、このことが予期しない動作を引き起こす場合があります。

package main

import "fmt"

func main() {
    var data *byte
    var in interface{}

    fmt.Println(data,data == nil) //prints: <nil> true
    fmt.Println(in,in == nil)     //prints: <nil> true

    in = data
    fmt.Println(in,in == nil)     //prints: <nil> false
    //'data' is 'nil', but 'in' is not 'nil'
}

関数がインターフェースを返すときには、この罠に気をつけましょう。

Incorrect:

package main

import "fmt"

func main() {
    doit := func(arg int) interface{} {
        var result *struct{} = nil

        if(arg > 0) {
            result = &struct{}{}
        }

        return result
    }

    if res := doit(-1); res != nil {
        fmt.Println("good result:", res) //prints: good result: <nil>
        //'res' is not 'nil', but its value is 'nil'
    }
}

Works:

package main

import "fmt"

func main() {
    doit := func(arg int) interface{} {
        var result *struct{} = nil

        if(arg > 0) {
            result = &struct{}{}
        } else {
            return nil //return an explicit 'nil'
        }

        return result
    }

    if res := doit(-1); res != nil {
        fmt.Println("good result:",res)
    } else {
        fmt.Println("bad result (res is nil)") //here as expected
    }
}

スタックとヒープ変数

  • レベル: 上級者

Go言語では、変数がスタック上かヒープ上に割り当てられるかを常に知ることはできません。C++では、newオペレータを使用して変数を作成することは常にヒープ変数を持っていることを意味します。しかしGoでは、 new()make() 関数を使用したとしても、変数の割り当て場所を決めるのはコンパイラです。コンパイラは、変数のサイズや「エスケープ解析」の結果に基づいて変数を格納する場所を選択します。これはまた、関数がローカル変数の参照を返しても問題ないことを意味します。CやC++のような他の言語では、これは許されていません。

変数がどこに割り当てられているかを知る必要がある場合は、"go build" または "go run" に "-m" gc フラグを渡してください(例: go run -gcflags -m app.go )。

GOMAXPROCS、並行性、並列性

  • レベル: 上級者

Go 1.4以前は1つの実行コンテキスト / OSスレッドのみを使用していました。これは、一度に実行できるgoroutineが1つだけであることを意味します。Go1.5からは、 runtime.NumCPU() によって論理CPUコア数を取得し、それを実行コンテキスト数として設定します。この runtime.NumCPU() が返す値は、プロセスのCPUアフィニティ設定によっては、システム上の論理CPUコアの合計数と一致しない場合もあります。この数値は、 GOMAXPROCS 環境変数を変更するか、 runtime.GOMAXPROCS() 関数で調整できます。

よくある誤解として、 GOMAXPROCS がgoroutineを実行するために使用するCPUの数を表している、というものがあります。 runtime.GOMAXPROCS() 関数のドキュメントを読むと、この混乱は更に深まります。 GOMAXPROCS 変数の説明( https://golang.org/pkg/runtime/ )の方が、OSスレッドについての説明がよくなされています。

GOMAXPROCS はCPUの数よりも大きな値に設定することができます。Go1.10から、 GOMAXPROCS の上限はなくなりました。 以前は GOMAXPROCS の最大値は256であり、1.9で1024に増加しました。

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println(runtime.GOMAXPROCS(-1)) //prints: X (1 on play.golang.org)
    fmt.Println(runtime.NumCPU())       //prints: X (1 on play.golang.org)
    runtime.GOMAXPROCS(20)
    fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 20
    runtime.GOMAXPROCS(300)
    fmt.Println(runtime.GOMAXPROCS(-1)) //prints: 256
}

読み書き操作の順序変更

  • レベル: 上級者

Goは処理を順番を並べ替えることがありますが、並び替えが発生したゴルーチン内では、全体の動作は変わらないことを保証します。しかし、複数のゴルーチンにまたがる実行順序は保証されません。

package main

import (
    "runtime"
    "time"
)

var _ = runtime.GOMAXPROCS(3)

var a, b int

func u1() {
    a = 1
    b = 2
}

func u2() {
    a = 3
    b = 4
}

func p() {
    println(a)
    println(b)
}

func main() {
    go u1()
    go u2()
    go p()
    time.Sleep(1 * time.Second)
}

このコードを数回実行すると、以下のようなaとbの変数の組み合わせが表示される場合があります:

1 2

3 4

0 2

0 0

1 4

ab の組み合わせで最も興味深いのは"02"です。これは、 ba よりも先に更新されたことを示しています。

複数のゴルーチンにわたる読み取りと書き込みの操作順序を保つ必要がある場合、チャネルを使用するか、"sync"パッケージ内の適切なツールを使用する必要があります。

割り込みスケジューリング

  • レベル: 上級者

ゴルーチンが予測不能な挙動をし、他のゴルーチンの実行を阻止するような場合があります。これは、スケジューラが実行されることを許可しない for ループがあると起こりえます。

package main

import "fmt"

func main() {
    done := false

    go func(){
        done = true
    }()

    for !done {
    }
    fmt.Println("done!")
}

for ループの中身は空でも空でなくても関係ありません。スケジューラの実行をトリガーしないコードが含まれている限り、問題になります。

スケジューラは、GC、"go"ステートメント、ブロッキングなチャネル操作、ブロッキングなシステムコール、およびロック操作後に実行されます。また、非インライン関数のコール時に実行れることもあります。

package main

import "fmt"

func main() {
    done := false

    go func(){
        done = true
    }()

    for !done {
        fmt.Println("not done!") //not inlined
    }
    fmt.Println("done!")
}

forループ内で呼び出す関数がインライン化されているかどうかを確認するには、"go build"や"go run"に"-m" gcフラグを渡してください(例: go build -gcflags -m

また、別の方法として、スケジューラを明示的に呼び出すこともできます。これは, "runtime"パッケージの Gosched() 関数でできます。

package main

import (
    "fmt"
    "runtime"
)

func main() {
    done := false

    go func(){
        done = true
    }()

    for !done {
        runtime.Gosched()
    }
    fmt.Println("done!")
}

上記のコードにはレースコンディションが含まれているので注意して下さい。スケジューリングの落とし穴を示すために意図的にそうしています。

"C"のインポートと複数行のインポートブロック

  • レベル: Cgo

Cgoを使用するためには"C"パッケージをインポートする必要があります。これは単一行のインポートで行うことも、インポートブロックで行うこともできます。

package main

/*
#include <stdlib.h>
*/
import (
  "C"
)

import (
  "unsafe"
)

func main() {
  cs := C.CString("my go string")
  C.free(unsafe.Pointer(cs))
}

インポートブロック形式を使用している場合、同じブロック内で他のパッケージをインポートすることはできません。

Compile Error:

./main.go:13:2: could not determine kind of name for C.free

"C"のインポートとCgoのコメントの間に空行があってはならない

  • レベル: Cgo

Cgoでの最初の落とし穴の一つは、 import "C" 文の上にあるcgoコメントの位置です。

package main

/*
#include <stdlib.h>
*/

import "C"

import (
  "unsafe"
)

func main() {
  cs := C.CString("my go string")
  C.free(unsafe.Pointer(cs))
}

Compile Error:

./main.go:15:2: could not determine kind of name for C.free

import "C" 文の上に空行がないことを確認しましょう

可変長引数を持つCの関数は呼び出せない

  • レベル: Cgo

可変長引数を持つCの関数は、直接呼び出せません。

package main

/*
#include <stdio.h>
#include <stdlib.h>
*/
import "C"

import (
  "unsafe"
)

func main() {
  cstr := C.CString("go")
  C.printf("%s\n",cstr) //not ok
  C.free(unsafe.Pointer(cstr))
}

Compile Error:

./main.go:15:2: unexpected type: …

可変長引数を持つCの関数は、決まった数の引数を持つ関数でラップする必要があります。

package main

/*
#include <stdio.h>
#include <stdlib.h>

void out(char* in) {
  printf("%s\n", in);
}
*/
import "C"

import (
  "unsafe"
)

func main() {
  cstr := C.CString("go")
  C.out(cstr) //ok
  C.free(unsafe.Pointer(cstr))
}
3
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?