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

Shinichi Jufuku
Feb 8 · 37 min read

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


概要

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

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

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

コンパイルエラー:

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

修正後:

・変数の未使用

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

コンパイルエラー:

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

修正後:

・未使用のimport

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

コンパイルエラー:

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

修正後:

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

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

コンパイルエラー:

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

修正後:

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

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

コンパイルエラー:

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

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

コンパイルエラー:

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

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

修正後:

・偶発的変数シャドウイング

短い変数宣言の構文はとても便利なので(特に動的言語から来た人には)、通常の代入操作のように扱うのは簡単です。 ただし下記のコードではコンパイルエラーは発生しませんが、アプリケーションは期待したとおりに動作しません。

これは、経験豊富なGo開発者にとっても非常によく見られる落とし穴です。このミスは作るのが簡単で、見つけるのが難しいかもしれません。

これらの問題を見つけるためにコマンドを使うことができます。 デフォルトでは、vetはシャドウ変数チェックを実行しません。 必ず フラグを使用してください。

コマンドがすべてのシャドウ変数を報告するわけではないことに注意してください。 より積極的なシャドー変数の検出には を使用してください。

・明示的な型なしで変数を初期化するために “nil”を使用することはできません

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

コンパイルエラー:

main.go:4: use of untyped nil

修正後:

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

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

修正後:

・Mapのcapacity

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

コンパイルエラー:

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

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

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

コンパイルエラー:

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

修正後:

・関数引数に配列

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

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

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

・スライスと配列の “range”句の予期しない値

あなたが他の言語での “for-in”や “foreach”ステートメントに慣れている場合、問題を起こす可能性があります。Goの”range”は 2つの値が生成されます。最初の値はアイテムのインデックス、2番目の値はアイテムのデータです。

【NG】

【OK】

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

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

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

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

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

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

【NG】

【OK】

・文字列は不変です

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

コンパイルエラー:

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

修正後:

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

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

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

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

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

2番目の最適化は、文字列が に変換される 句内の余分な割り当てを回避します。:

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

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

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

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

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

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

・文字列の長さ

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

これを同様のGoコードに変換すると、あなたは驚くかもしれません。

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

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

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

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

コンパイルエラー:

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 }

修正後:

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

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

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

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

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

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

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

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

・”for range”句を使用してマップを反復する

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

また、Go Playground(https://play.golang.org/)を使用しても、変更しない限りコードが再コンパイルされないため、常に同じ結果が得られます。

・「switch」ステートメントでのフォールスルー動作

“switch”ステートメントの “case”ブロックは、連続して処理されません。次の”case”ブロックに処理が移っていく他の言語とは異なります。

各 “case”ブロックの最後に “fallthrough”ステートメントを使用することで、 強制的に次の”case”ブロックに処理を移すことができます。また”case”ブロックで式リストを使用するようにswitchステートメントを書くこともできます。

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

多くの言語にはインクリメント演算子とデクリメント演算子があります。 他の言語とは異なり、Goはプレフィックスバージョンの書き方をサポートしません。 また、これら2つの演算子を式に使用することもできません。

コンパイルエラー:

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

修正後:

・ビットごとのNOT演算子

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

コンパイルエラー:

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

修正後:

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

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

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

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

・未エクスポートの構造体フィールドはエンコードされません

小文字で始まる構造体フィールドは(json、xml、gobなど)エンコードされないため、構造体をデコードすると、それらの未エクスポートフィールドの値はゼロになります。

・アクティブなゴルーチンでアプリが終了する

アプリはすべてのゴルーチンが完了するのを待ちません。

実行した際のレスポンス:

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

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

修正後のレスポンス:

[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”変数には影響しません。

これで期待どおり動作します。

・バッファリングされていないチャネルへの送信は、ターゲットレシーバが準備完了になるとすぐに戻る

あなたのメッセージが受信者によって処理されるまで送信者はブロックされません。コードを実行しているマシンによっては、受信者ゴルーチンは、送信者が実行を続ける前にメッセージを処理するのに十分な時間がある場合とない場合があります。

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

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

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

アプリケーションによっては、修正方法が異なります。それはマイナーなコード変更かもしれませんまたはあなたのアプリケーションデザインの変更を必要とするかもしれません。どちらの方法でも、アプリケーションが閉じたチャネルにデータを送信しようとしないようにする必要があります。

バグのある例は、特別なキャンセルチャネルを使用して残りの作業者に結果が不要になったことを知らせることで修正できます。

・”nil”チャンネルを使う

nilチャネルブロック転送の送受信操作それはよく文書化された振る舞いですが、新しいGo開発者にとっては驚くことかもしれません。

コードを実行すると、次のようなランタイムエラーが発生します。

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

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

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

終わりに

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

Eureka Engineering

Learn about Eureka’s engineering efforts, product developments and more.

Shinichi Jufuku

Written by

API Team at eureka, inc.

Eureka Engineering

Learn about Eureka’s engineering efforts, product developments and more.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade