【初級編】Go言語を始める方のための落とし穴、問題の解決方法やよくある間違い

Shinichi Jufuku
Eureka Engineering
Published in
37 min readFeb 8, 2019

こちらの記事はGo言語初心者向けのサイト「50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs」の中から初級編だけを日本語に翻訳したものとなります。特にGoを始めたばかりの方にはとても役立つ情報かと思いますので是非ご覧ください。また、多少日本語がおかしい部分あるかもしれませんがご了承ください。もし分かりづらい部分は直接本家サイトを参照頂ければ幸いです。

概要

Goはシンプルで楽しい言語ですが、他の言語と同様に、いくつかの問題点があります。それらの多くの問題点は、Goのせいではありません。あなたが他の言語から来ているならば、これらの間違いのいくつかは自然な罠です。

公式情報、wiki、メーリングリストでの議論、Rob Pikeによる素晴らしい投稿やプレゼンテーション、そしてソースコードを読むことで言語を学ぶのに時間がかかるならば、これらの多くの落とし穴は役にたつかもしれません。Goに慣れていない場合は、ここにある情報を使用してコードをデバッグする時間を節約できます。

・開始の中括弧を別の行に配置することはできません

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

コンパイルエラー:

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

修正後:

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

・変数の未使用

未使用の変数がある場合はコンパイルに失敗します。未使用の変数に新しい値を代入しても、コンパイルに失敗します。
ただし、グローバル変数は未使用でも問題ありません。 また関数の引数も未使用で構いません。

package mainvar gvar int //not an errorfunc 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?")
}

コンパイルエラー:

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

修正後:

package mainimport "fmt"func main() {  
var one int
_ = one
two := 2
fmt.Println(two)
var three int
three = 3
one = three
var four int
four = four
}

・未使用のimport

エクスポートした関数、インタフェース、構造体、または変数を使用せずにパッケージをインポートした場合、コンパイルに失敗します。
インポートしたパッケージが本当に必要な場合は、コンパイルの失敗を避けるために、パッケージ名として空白の識別子_を使用できます。 空白の識別子は、副作用のためにパッケージをインポートするために使用されます。

package mainimport (  
"fmt"
"log"
"time"
)
func main() {
}

コンパイルエラー:

main.go:4: imported and not used: “fmt”
main.go:5: imported and not used: “log”
main.go:6: imported and not used: “time”

修正後:

package mainimport (  
_ "fmt"
"log"
"time"
)
var _ = log.Printlnfunc main() {
_ = time.Now
}

goimports ツールを使用すれば未使用のimportを削除してくれます。

・ 短い変数宣言は関数内でのみ使用できます

package mainmyvar := 1 //errorfunc main() {  
}

コンパイルエラー:

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

修正後:

package mainvar myvar = 1func main() {  
}

・短い変数宣言を使用した変数の再宣言

変数を再宣言することはできませんが、1つの新しい変数を含めた複数での変数宣言では許可されています。

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

コンパイルエラー:

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

package mainfunc main() {  
one := 0
one, two := 1,2
one,two = two,one
}

・短い変数宣言を使用してフィールド値を設定することはできません

package mainimport (  
"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)
}

コンパイルエラー:

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

一時変数を使用するか、すべての変数を事前宣言して標準代入演算子を使用してください。

修正後:

package mainimport (  
"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 mainimport "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”識別子は、インタフェース、関数、ポインタ、マップ、スライス、およびチャネルの “ゼロ値”として使用できます。 変数の型を指定しないと、型を推測できないため、コンパイラはコードのコンパイルに失敗します。

package mainfunc main() {  
var x = nil //error
_ = x
}

コンパイルエラー:

main.go:4: use of untyped nil

修正後:

package mainfunc main() {  
var x interface{} = nil
_ = x
}

・”nil”スライスとマップを使う

項目を “nil”スライスに追加しても問題ありませんが、マップで同じことをするとランタイムパニックが発生します。

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

修正後:

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

・Mapのcapacity

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

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

コンパイルエラー:

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

・文字列を “nil”にすることはできません

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

package mainfunc main() {  
var x string = nil //error
if x == nil { //error
x = "default"
}
}

コンパイルエラー:

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

修正後:

package mainfunc main() {  
var x string //defaults to "" (zero value)
if x == "" {
x = "default"
}
}

・関数引数に配列

配列を関数に渡すと、関数は同じメモリ位置を参照するため、元のデータを更新できます。 Goの配列は値なので、配列を関数に渡すと、関数は元の配列データのコピーを取得します。 配列データを更新しようとしている場合、これは問題になる可能性があります。

package mainimport "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 mainimport "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 mainimport "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”は 2つの値が生成されます。最初の値はアイテムのインデックス、2番目の値はアイテムのデータです。

【NG】

package mainimport "fmt"func main() {  
x := []string{"a","b","c"}
for v := range x {
fmt.Println(v) //prints 0, 1, 2
}
}

【OK】

package mainimport "fmt"func main() {  
x := []string{"a","b","c"}
for _, v := range x {
fmt.Println(v) //prints a, b, c
}
}

・スライスと配列は一次元です

Goは多次元の配列とスライスをサポートしているように見えますが、サポートしていません。 ただし、配列の配列またはスライスのスライスを作成することは可能です。

スライスのスライスを使用して動的多次元配列を作成するには2段階のプロセスが必要です。 まず、外側のスライスを作成します。 次に、内側の各スライスを作成する必要があり、内側のスライスは互いに独立しています。また他の内側のスライスに影響を与えずに長さを調整することも可能です。

package mainfunc main() {  
x := 2
y := 4
table := make([][]int,x)
for i:= range table {
table[i] = make([]int,y)
}
}

スライスのスライスを使用して同じアドレスを参照する動的多次元配列を作成するには、3段階のプロセスが必要です。 まず、生データを保持するスライスを作成します。 次に、外側のスライスを作成します。 最後に、生データのスライスを再スライスして各内側スライスを初期化します。

package mainimport "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>
}

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

これは、(他の言語で行われているように) “nil”識別子を取得するための方法です。 対応するデータ型の「ゼロ値」が「nil」の場合、戻り値は「nil」になりますが、他のデータ型の場合は異なります。 マップレコードが存在するかどうかを判断するために適切な「ゼロ値」を確認することができますが、必ずしも信頼できるとは限りません(たとえば、「ゼロ値」がfalseのbool値のマップがある場合は戻り値はboolになる)。 特定のマップレコードが存在するかどうかを知る最も確実な方法は、マップアクセス操作によって返された2番目の値を確認することです。

【NG】

package mainimport "fmt"func main() {  
x := map[string]string{"one":"a","two":"","three":"c"}
if v := x["two"]; v == "" { //incorrect
fmt.Println("no entry")
}
}

【OK】

package mainimport "fmt"func main() {  
x := map[string]string{"one":"a","two":"","three":"c"}
if _,ok := x["two"]; !ok {
fmt.Println("no entry")
}
}

・文字列は不変です

インデックス演算子を使用して文字列変数内の個々の文字を更新しようとすると失敗します。 文字列は読み取り専用のバイトスライスです(追加のプロパティがいくつかあります)。 文字列を更新する必要がある場合は、必要に応じて文字列型に変換する代わりにバイトスライスを使用してください。

package mainimport "fmt"func main() {  
x := "text"
x[0] = 'T'
fmt.Println(x)
}

コンパイルエラー:

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

修正後:

package mainimport "fmt"func main() {  
x := "text"
xbytes := []byte(x)
xbytes[0] = 'T'
fmt.Println(string(xbytes)) //prints Text
}

この操作は与えられた文字が複数バイトで格納される可能性があるので、テキスト文字列内の文字を更新する正しい方法ではないことに注意してください。 テキスト文字列を更新する必要がある場合は、まずそれをruneスライスに変換してください。 runeスライスを使っても、1つの文字が複数のルーンにまたがることがあります。これは、たとえば、重いアクセント文字がある場合に起こります。 この複雑であいまいな “文字”の性質が、Go文字列がバイト列として表現される理由です。

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

文字列をバイトスライスに(そしてその逆に)変換すると、元のデータの完全なコピーが得られます。 他の言語でのキャスト操作とは異なり、新しいスライス変数が元のバイトスライスで使用されているのと同じ配列を指す場所を再スライスすることとは異なります。

余分な割り当てを避けるために、Goには[] byteからstringへの変換とstringから[] byteへの変換の最適化がいくつかあります。

最初の最適化は、 []byteキーが map[string]内のエントリm[string(key)]を検索するために使用されるときの余分な割り当てを回避します。

2番目の最適化は、文字列が []byteに変換される for range句内の余分な割り当てを回避します。: for i,v := range []byte(str){...}

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

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

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

特定の文字列 “characters”(unicode code points/rune)にアクセスする必要がある場合は、for range句を使用してください。 公式の “unicode / utf8”パッケージと実験的なutf8stringパッケージも役に立ちます。 utf8stringパッケージには便利なAt()メソッドが含まれています。 文字列をruneのスライスに変換することもオプションです。

・文字列がUTF-8テキストとは限らない

文字列値はUTF-8テキストである必要はありません。 それらは任意のバイトを含むことができます。 文字列がUTF8であるのは、文字列リテラルが使用されるときだけです。 それでも、エスケープシーケンスを使用して他のデータを含めることができます。

UTF-8テキスト文字列があるかどうかを知るには、 “unicode / utf8”パッケージのValidString()関数を使います。

package mainimport (  
"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 mainimport "fmt"func main() {  
data := "♥"
fmt.Println(len(data)) //prints: 3
}

組み込みlen()関数は、PythonのUnicode文字列の場合と同様に、文字数ではなくバイト数を返します。

Goで同じ結果を得るにはRuneCountInString()、 “unicode / utf8”パッケージの関数を使います。

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

技術的には、このRuneCountInString()関数は1文字が複数のルーンにまたがる可能性があるため、文字数を返しません。

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

・複数行のスライス、配列、およびマップリテラルにコンマがない

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

コンパイルエラー:

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 }

修正後:

package mainfunc main() {  
x := []int{
1,
2,
}
x = x
y := []int{3,4,} //no error
y = y
}

宣言を1行に折りたたむときに末尾のカンマを残しても、コンパイルエラーは発生しません。

・log.Fatalとlog.Panicはログ出力以上のことをする

ロギングライブラリはしばしば異なるログレベルを提供します。これらのロギングライブラリとは異なり、Goのlogパッケージは、その Fatal*()Panic*()関数を呼び出すとlog出力以上のことを行います。これらの関数を呼び出すと、アプリは強制終了させられます。

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

・組み込みデータ構造操作が同期されていない

Goには並行性をネイティブにサポートするための機能がいくつもありますが、並行性に安全なデータコレクションはその1つではありません。データコレクションの更新がアトミックであることを確認するのはあなたの責任です。これらのアトミック操作を実装するには、goroutine と channel が推奨されますが、さらに簡易に実現するには “sync”パッケージを利用することもできます。

・”range”句の文字列の繰り返し値

インデックス値( “range”操作によって返される最初の値)は、2番目の値で返される現在の “character”(unicode code point/rune)の最初のバイトのインデックスです。他の言語で行われているように、現在の “文字”のインデックスではありません。実際のキャラクターは複数のルーン文字で表されることがあります。文字を扱う必要がある場合は、必ず “norm”パッケージを調べてください。

for rangeで文字列の変数を扱う場合は、UTF8のテキストとしてデータを解釈しようとします。繰り返しの最中に無効なUTF-8シーケンスが現れたときは、2番目の変数は0xFFFD(Unicode replacement character)となり、次の繰り返しのときに文字列内を1バイト進めます。文字列変数に任意の(UTF-8テキストではない)データが格納されている場合は、格納されているすべてのデータをそのまま取得するために、必ずそれらをバイトスライスに変換してください。

package mainimport "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”句を使用してマップを反復する

アイテムが特定の順序(キー値順など)になっていることを想定する場合、問題です。マップの反復ごとに異なる結果が生成されます。

package mainimport "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”ブロックは、連続して処理されません。次の”case”ブロックに処理が移っていく他の言語とは異なります。

package mainimport "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”ブロックの最後に “fallthrough”ステートメントを使用することで、 強制的に次の”case”ブロックに処理を移すことができます。また”case”ブロックで式リストを使用するようにswitchステートメントを書くこともできます。

package mainimport "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はプレフィックスバージョンの書き方をサポートしません。 また、これら2つの演算子を式に使用することもできません。

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

コンパイルエラー:

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

修正後:

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

・ビットごとのNOT演算子

多くの言語は単項NOT演算子(別名ビットごとの補数)として〜を使いますが、GoはXOR演算子に(^)を利用します。

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

コンパイルエラー:

main.go:6: the bitwise complement operator is ^

修正後:

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

必要な場合は、単項NOT演算(NOT 0x02など)をバイナリXOR演算(例:0x02 XOR 0xff)で表すことができます。

Goには、特別な ‘AND NOT’ビット演算子(&^)もあります。これは、NOT演算子の混乱を招きます。 括弧を必要とせずにA AND(NOT B)をサポートすることが出来ます。

package mainimport "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))
}

・演算子の優先順位の違い

“bit clear”演算子(&^)以外に、Goには他の多くの言語で共有されている一連の標準演算子があります。ただし、演​​算子の優先順位は必ずしも同じではありません。

package mainimport "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 mainimport (  
"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 mainimport (  
"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!

最も一般的な解決策の1つは、”WaitGroup”変数を使用することです。それはすべてのワーカーゴルーチンが終了するまでメインゴルーチンを待つことができます。あなたのアプリがメッセージ処理ループを持つ長時間実行ワーカーを持っているなら、あなたはそれらのゴルーチンにそれが終了した事を知らせる必要があります。あなたはそれぞれのワーカーに “kill”メッセージを送ることもできます。他の選択肢は、全ルーチンが受信しているチャネルを閉じることです。すべてのゴルーチンを一度に通知するのは簡単な方法です。

package mainimport (  
"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 mainimport (  
"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 mainimport "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
}

・閉じたチャンネルに送信するとパニックが発生する

閉じたチャンネルからの受信は安全です。 ok受信ステートメントの戻り値は、 falseデータが受信されなかったことを示すように設定されます。あなたがバッファされたチャンネルから受信しているならば、あなたは最初にバッファされたデータを得ます、そしてそれが空になると ok戻り値は falseになるでしょう。

閉じたチャンネルにデータを送信するとパニックが発生します。これは文書化された動作ですが、送信動作が受信動作に似ていると予想されるかもしれない新しいGo開発者にとってはあまり直観的ではありません。

package mainimport (  
"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 mainimport (  
"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 mainimport (  
"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!

この動作は caseselectステートメント内のブロックを動的に有効または無効にする方法として使用できます。

package mainimport "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)
}

・値を受け取る側のメソッドは元の値を変更できない

メソッドレシーバは通常の関数引数と似ています。それが値であると宣言されているなら、あなたの関数/メソッドはあなたのレシーバ引数のコピーを取得します。つまり、レシーバがマップ変数またはスライス変数で、コレクション内の項目を更新している場合、またはレシーバ内で更新しているフィールドがポインタでない限り、レシーバに変更を加えても元の値に影響はありません。

package mainimport "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]
}

終わりに

Goを学んでいくうえで、日本語のドキュメントだと情報が少なかったり古かったりと、まだまだキャッチアップしづらい環境ではあるので、やはり英語のドキュメントを積極的に読むようにしていくのはとても大事だと思いました。今後も英語のドキュメントで良いものをキャッチアップ出来たら記事にあげます。

--

--