Go言語(Golang) はまりどころと解決策
目次
Goの言語仕様はシンプルで他の言語に比べてはまりどころが少なくて学習コストが小さめな言語のように思います。しかし、それでもはまるところがないわけではないので、自分がはまって時間を無駄にしてしまったことを書き留めておきます。
- 目次
- interface とnil (Goのinterfaceは単なる参照ではない)
- メソッド内でレシーバ(this, self)がnilでないことをチェックすることに意味がある
- errorしか返り値がない関数でerrorを処理し忘れる
- 基本型がメソッドを持たない
- stringが単なるバイト列
- 継承がない
- Genericsがない
- goroutine はGCされない
- goroutineはgenerator (yield) の実装には使えない
- 例外が(推奨され)ない
- 繰り返す if err != nil {return err}
- return nil, err → このerrorどこで発生したの?
- 関数より狭いスコープで defer
- structとC++/Javaのクラスとの違い
- 型が後置
- 1.0 が浮動小数点型にならない(時がある)
- 名前が…
interface とnil (Goのinterfaceは単なる参照ではない)
JavaやC#あるいはC++の経験があり、ある程度クラスの内部構造への理解がある人はGoのinterfaceの実体もデータへの参照だと考えると思います。 しかし実はそれは正しくありません。Goのinterfaceの実体は参照(ポインタ)ではありません。Goのinterfaceの実体は参照と型情報のペアです。
さてこの内部構造の違いがGoにどういった影響をもたらすのでしょうか。実はこの内部構造の違い、意外と言語の挙動にはあまり大きな影響を与えません。そのためこの点を理解していなくてもGoでプログラムを書けてしまいます。ただしnilを扱う場合にはGoは予想外の挙動をします。
package main
import "fmt"
type myError struct {
message string
}
func (e *myError) Error() string {
if e == nil {
return "myError: <nil>"
}
return "myError: " + e.message
}
func myFunc(x int) error {
var err *myError
if x < 0 {
err = &myError{
message: "x should be positive or zero.",
}
}
return err
}
func main() {
err := myFunc(10)
fmt.Println(err)
if err != nil {
fmt.Println("err is NOT nil.")
} else {
fmt.Println("err is nil.")
}
}
上のコード(Go Playground)では、myFuncはx >= 0
の時にはvar err *myError
の初期値nil
を返すので、mainの最初のfmt.Println(err)
はmyError: <nil>
を出力します。そして、次のif-elseはerrがnilだから”err is nil.”が表示されると思うかもしれません。JavaやC++ならそうなります。しかしGoでは”err is NOT nil”が表示されます。
何故こうなるのかは、interfaceが型と値への参照のペアであること点を踏まえた上で、errのValueとTypeをreflect.ValueOf
, reflect.TypeOf
を使って表示してみると明らかです。
err のTypeとValueを表示してみると、Typeが*main.myError
で値がValueが<nil>
であることが分かります。errの”値”はnil
ですがerrは型情報を保持しているのです。
型を持っているinterfaceはValueがnil
でもnil
ではないのです。
---- err ----
is nil: false
Type: *main.myError
Value: <nil>
---- trueNil ----
is nil: true
Type: <nil>
Value: <invalid reflect.Value>
ちなみにこれはFrequently Asked Questions (FAQ)に2012年から書いてある問題です。 よくドキュメントを読まずに色々はまって時間をつぶすす前に(自分は数時間つぶしました)、まずFAQくらいは目を通しておいたほうがよいですね。
文献
- Why is my nil error value not equal to nil?
- The Laws of Reflection - The Go Blog
- Go Data Structures: Interfaces
メソッド内でレシーバ(this, self)がnilでないことをチェックすることに意味がある
これははまりどころというより、Goだとnil
に大してメソッド呼び出しを行った場合の挙動が他の人気のある言語と少し異なるので、メソッド側の書き方次第では呼び出し側のnil
チェックをすこし緩和できるよという話ですが。あるいはこの点を理解していないと、呼び出し側でnil
チェックしなくてなぜ大丈夫なのか困惑するという話です。
C++, Java, Python, JavaScript などの他の人気のある言語では
person.sayHello()
のようにメソッド呼び出しをする場合は(C++の場合は.
でなく->
)、person
がnull
でないことを確認しなくてはなりません(C++でsayHello
がnon-virtualの場合も挙動は未定義)。
しかしGoの場合はperson
がstruct
のポインタである場合にはperson
がnil
でも関数の呼び出しは問題なく行えます。
またperson
がインターフェイスでその値がnil
で型情報がstruct
のポインタである(つまり上述したようにインターフェイスそのものはnil
ではない)場合にも、問題なく関数は呼び出されます。
そのため、Goではメソッド内部でレシーバのポインタがnil
であるかを確認することには意味があります。
メソッド内でnil
のチェックが適切に行われている場合、そのメッソドは呼び出し側でnil
チェックをすることなしに呼び出せるようになります。
例えばGoのprotobufの実装はレシーバがnil
の場合でもgetterメッソドは問題なく実行できるように実装されています
(There are getters that return a field’s value if set, and return the field’s default value if unset. The getters work even if the receiver is a nil message.)。
そのためGoのprotobufを利用する場合、例えGetDoc()
の返り値がnil
だったとしても
var url string
doc := response.GetDoc()
if doc != nil {
url = doc.GetURL()
}
のように書かずに単純に
url := response.GetDoc().GetURL()
のように書くことが出来ます(GetURLの内部でdocがnil
かどうかがチェックされている)。呼び出し側でのnil
チェックの必要性は細かいことですが、protobuf
のようなライブラリの使い勝手には大きく影響します。
メソッド内部でレシーバのnil
チェックが行われていない場合は、レシーバ内でrecv.field
を参照した時点、値を代入しようとした時点でpanic
が発生します。
errorしか返り値がない関数でerrorを処理し忘れる
Goではエラーは通常、最後の返り値として呼び出し元に返されます。関数が何か重要な値を返す場合であれば、_
を利用しないかぎりはerrorは無視できないのでerrorを処理し忘れることはあまりないと思います。ただjson.Unmarshalのようなerror以外に重要な情報を返さない関数は、エラー以外に返り値がないからといって、うっかり返り値を受け取るのを忘れるとerrorが失われます。
例えば上のコードは"not json"
はjsonとしてパース出来ないのでエラーが発生しますが、コンパイル時にはエラーが無視されていることは検出されません。これに関しては気をつける以外に解決策はないと思います。
基本型がメソッドを持たない
例えばstringがlenをメソッドとして持ちません。これはC#とかではintですらメソッドを持つのと真逆を行くように思える。 Goのインターフェースで宣言されているメソッドが実装されていれば、そのインターフェースを実装していることになるという仕様と関係している?
stringが単なるバイト列
Java, C#やPython3などのモダンな言語では文字列(string)はUnicode文字の列ですが、Go言語のstringは単なるimmutable(書き換え不可能)なバイト列に過ぎません(In Go, a string is in effect a read-only slice of bytes.)。Goのstringは中身がUTF-8でエンコードされた文字列かも知れませんし、Shift_JISでエンコードされた文字列かもしれません。
これは文字列をUnicodeにしたPython3の真逆を行く感じで正直本当に正しいのかはよく分かりません。
- Goではファイルなどから
string
を読み込む際にエンコードを指定したりはしません。 これは他の言語のようにデフォルトの文字エンコーディングで暗黙的にデコードされているのではなく、 そもそもGoはstring
を作る際にデコードを行わないからです。 len(s)
はs
の文字列のバイナリ列としての長さを返します。文字の数ではないの注意して下さい。- Goのソースコードは必ずUTF-8でなくてはならないので、 ソースコード中に文字列リテラルとして定義された文字列はUTF-8の文字列です。
- stringに対するrange-loop(for range)の場合だけ特別に文字列がUTF-8としてデコードされて”一文字”ずつ処理されるという少し歪な仕様になっている
- A sample on Go Playground
継承がない
Goには継承はありません。 そもそも継承はプログラミング言語にあまり必要ない機能だと思います。 継承が本当に有益なこともありますが、経験上大半のケースでは設計を手抜きするために継承が使われていて、結果長い目で見た際のreadabilityやmaintainabilityが著しく劣化してしまっていることが多いと思います。リスコフの置換原則のような基本的な原則が守られておらず(そもそも多くの人は名前すら知らない)、単に一部のコードをクラス間で共有するために継承が使われていて可読性が著しく低いコードもよく目にします。 そのため、そもそもプログラミング言語が継承をサポートしないというのは良いことなのかなと思います。 たまに継承が非常に有益なのも分かりますが。
Embeddingという機能で複数の型を合成することはできます。多重継承に少し似ていますね。
Genericsがない
GoにはGenericsはありません。 JavaのGeneric TypesとかC++のテンプレートで書けるようなことはGoでは書けません。 ただ配列(スライス), mapについては特別に言語でサポートされているのでJavaやC++で総称型を使うケースの大半はカバーされるとは思います。
goroutine はGCされない
goroutine はガーベッジコレクションの対象ではありません。
goroutineはGCされないので、go
で起動した関数は必ず終了するように気をつけてプログラムを書きましょう。JavaやPythonの実行中のスレッドがGCに回収されないのと同じですね、自然な仕様だと思います。ただgoroutineは割りと気軽に作成できてしまうので、うっかり新しいgoroutineもGCのルートになることを忘れてしまうかも。
またこの制約のためGoで新しい読み込み専用channelだけを返す関数というのは呼び出し側がchannelからデータ最後まで読み込まないとメモリリークが発生する危険性があります。 例えば標準ライブラリのtime.Tickはとても便利ですがリークします(「it “leaks”.」)。 そのため、次に述べるようにPythonのyieldに相当することを実現するのにchannelとgoroutineは使わないほうがよいでしょう。
goroutineはgenerator (yield) の実装には使えない
Go言語にはPythonのyieldに相当する機能はありません。ただgoroutineとchannelを組み合わせればyieldに相当することができるのではと思うかもしれません(Go Playground)。
package main
import "fmt"
func squareGenerator(n int) <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < n; i++ {
ch <- i * i
}
close(ch)
}()
return ch
}
func main() {
for i := range squareGenerator(100) {
if i > 100 {
break
}
fmt.Println(i)
}
}
このコードは意図した通り1, 4, 9…,81, 100を出力します。ただこのやり方には
- channel は比較的(Goにしては)遅いのでパフォーマンスが低下する
- goroutine がGCされないので、channelが最後まで読み込まれないとリークする
という2つの問題があります。 まずchannelはGoの他の要素に比べるとかなり低速です。channelを通じてchannelの書き込み側のgoroutineと読み込み側のgoroutineの間でコンテキストスイッチを行うのは、関数呼び出しなどに比べると数十倍から百倍ぐらい時間がかかります(Go 1.5時点)。
また上に書いたように、実行中のgoroutineはJavaやPythonのスレッドと同じでGCのルートになります。channelへの書き込みでブロックされて停止中のgoroutineもGCの対象ではありません(少なくとも1.5時点では)。そのため、generatorが返したchannelが最後まで呼び出されないとchannelとgoroutineがリークすることになります。
例外が(推奨され)ない
Go言語のFAQにあるように、Goには例外がありません。panic, recoverで例外と同じようなことはできますが、Javaの例外のように気軽に使ってはなりません。個人的にはこのFAQにかかれていることには概ね同意します。
例外で返されたエラーを try {...} catch (Exception e) {...}
みたいに処理しないといけないのは無意味に複雑なように思います。
それだけならよいですが、例外が発生するとコードが想定外の順序で実行されて困ったり、何故かこのコードが実行されないなと思ったら、その前に例外で大域脱出していて、しかもその例外が予想外のところでcatchされ握りつぶされていたり、と例外を大規模なプロジェクトの中で正しく扱うのは中々に困難だと思います。
Goの例外は極力使わず、エラーを値として扱うポリシーはよいもの(特に大規模なプロジェクトで、エラーハンドリングが大切なプロジェクトでは)だと思います。
ただ一方で、ちょっとした使い捨ての便利ツールを書く場合や、とりあえずプロトタイプで正常系だけ書きたい時、 あるいは異常が発生したらプログラムを停止してしまって良いような起動時の初期化処理を書く時には、正直Goのエラーハンドリングはかなり面倒くさいです。 こういうタイプのコードでは外部ライブラリの呼び出しやファイル、データベースなどの外部リソースへのアクセスが大きな割合を占めます。そして、そうした処理はほとんどの場合 error が発生しうるのでそれぞれの処理に対してエラーハンドリングを行う必要があります。 場合によってはコードのかなりの割合の行が
if err != nil {
return nil, err
}
の繰り返しで占められてしまうこともあるでしょう。これに対しては根本的な解決策はないように思います。エラーが発生した場合はエラーメッセージを出力して処理を中断してしまって問題ない
- 使い捨てあるいは内部ツールでエラーハンドリングがあまり重要ではない時
- 正常系だけとりあえずプロトタイプしたい時
には例外の方が便利であり、正直Go言語ではあまり効率的にコードが書けないような気がします。個人的にはそういう用途にはPythonなどを使うのが正しい解決策のように思えます。何でもGoで書く必要はないのですから。
繰り返す if err != nil {return err}
Goでは例外が推奨されずエラー処理を常にきちんと書かなくてはならないので、Goでプログラムを書いていると
if err != nil {
return err
}
// ...
if err != nil {
return err
}
// ...
if err != nil {
return err
}
// ....
のように if err != nil
によるエラーハンドリングを繰り返し繰り返し書かなくてはならないことがあります。
Go Blogにはif err != nil { return err }
のパターンはあまり出現しない(once per page or two)と書かれていますが、
プログラムのタイプによっては(例えばいろいろな外部リソースや外部ライブラリをつなぐようなコード)かなりの頻度で if err != nil
を書かざるを得ないことがあるような気がします。
if err != nil
を入力するショートカット定義したほうがいいんじゃないのという気分になることがたまにあります。
解決策
// ...
の部分のコードが同じ処理の繰り返しであれば前述のGo Blogに書かれているように if err != nil
の繰り返しを避けることが出来ます。if err != nil
が繰り返しているなと思ったら、そもそもif err != nil
以外の部分も繰り返しになっていないか、繰り返している処理をひとまとめにできないかを考えてみるべきでしょう。
一方で、このテクニックが利用できるのは...
の部分の処理が同じ型の処理の組み合わせでコードをまとめられる場合に限られます。そもそもコードが一定以上繰り返していたらまとめたほうが良いというのは、特にif err != nil
とは関係なく行なうべきことでしょう。Goだと関数内の内部で更に関数を定義できるので、関数の一部の処理を気軽にまとめることができます、すばらしいことです。ただ// ...
の処理に共通点があまりなく綺麗にまとめることができない場合は、if err != nil
の繰り返しは我慢する以外によい解決策はないようです。
return nil, err → このerrorどこで発生したの?
Goではエラーハンドリングをきちんと書かなくてはなりません。とはいえ、きちんと書くと言ってもエラーが発生したら単に処理を中断してエラーを呼び出し元に返すことでエラー処理を呼び出し元に丸投げしてしまうことが多いでしょう。
if err != nil {
return nil, err
}
そして最後に一番外側の処理でエラーを出力します
func main() {
// ...
result, err != doSomething()
if err != nil {
log.Fatal(err)
}
// ...
}
プログラムを走らせたらエラーが発生してエラーが出力されました。
Invalid Argument
あれ…このエラーどこで発生したの…
Goのエラーはただの値なのでJavaやPythonの例外などと違ってスタックトレースを含みません。 Goのエラーには、そのエラーがどこで発生したかというコンテキストが自動的には含まれないのです。 そのため、エラーハンドリングを呼び出し元に任せるからといってerrorを呼び出し元に何も考えずに返していると最終的にそのエラーがどこで発生したのかが分からなくなってしまいます。 なので、errorを呼び出しに返すときは手動でエラーのコンテキストを残してあげましょう。
if err != nil {
return nil, fmt.Errorf("Some context: %v", err)
}
これでエラーがどこで発生したのかが分かるようになります。ただこういうことしてると実は例外で良かったんじゃないかという気分にもなりますが。
関数より狭いスコープで defer
C#のusing, Pythonのwithのように他の人気のある言語ではあるスコープから処理が抜ける際に、リソースの解放処理を確実に実行するための機能がサポートされています。Javaでも1.7からtry-with-resourcesがサポートされています。
Goではそういった解放処理はdefer
を使って行います。
ただC#のusing, Pythonのwith, Javaのtry-with-resourcesと違ってGoのdefer
は一定のスコープを抜けた時ではなく、関数が終了する際に確実に指定した処理を実行する仕組みです。
そのため、次のようなコードを書いてもr.Close()
が実行されるのは if condition
のif文のブロックが終了した時ではなくmyFunc
全体が終了した時になってしまいます。
func myFunc() err {
// ...
r, err := os.Open(filename)
if err != nil {
return err
}
defer r.Close()
data, err := readDataFromReader(r) // 実際にはもう少し複雑な処理
if err != nil {
return err
}
// この時点でr.Close()を本当は呼びたいが、myFuncの終了まで呼び出されない。
}
structとC++/Javaのクラスとの違い
これもはまりどころというよりは、Goでコードを書く上で理解しておきたいポイントの整理ですが。 GoのstructとC++/Javaのクラスの、コードを書く上での理解しておくべき大きな違いは、struct (class) の初期化の方法の違い(コンストラクタがない)から来ます。
コンストラクタがない
Goにはコンストラクタがありません。GoではC++、Javaのコンストラクタに相当する関数を単なる関数として定義します。ただこれは定義方法がC++, Javaと違うというだけで、実際にコードを書くときには大した違いはないように思います。
Goではstructは通常
var mydata MyData
mydata := &MyData {
X: 10,
Y: 20,
}
のようにStructType{name: value,…}で初期化します。{name: value}が省略された場合はゼロ初期化されます。Goにはコンストラクタは存在しません。Goであるstructの初期化関数を用意したい場合には、パッケージに生成用の関数を用意します。通常、関数の名前はNew+struct名(+付加情報)のようになります。例えば、bytes.Bufferには []byteからbytes.Bufferを生成する bytes.NewBuffer と string からbytes.Bufferを生成するbytes.NewBufferStringが用意されています。
ゼロ初期化が避けられない
Goでは、structのメンバー変数をパッケージ外に非公開にすることができます。非公開になっているメンバーは他のパッケージから直接編集することはできません。ただし、structがパッケージ外に公開されている場合、例え全ての変数が非公開だったとしても
var mydata MyData
のように書かれてしまうと、MyDataの中身は全てゼロ値で初期化されてしまいます。C++/やJavaではコンストラクタに書かれているようにしか非公開のメンバーは変更できないので、メンバーがどのように初期化されるかは明示することができます。しかしGoではそのようなことはできません。structが外部に公開されるのならばstructは全てがゼロ初期化された場合にも正しく動くように常に設計しなくてはならないのです。
コピーされるのが避けられない
GoではC++のコピーコンストラクタのような仕組みはないので、structのコピーを防止することは不可能です。公開されているstructは他のパッケージのコードで自由にコピーができてしまいます。実はGoははじめのころは非公開のメンバーがあるstructはパッケージ外部ではコピーすることはできませんでした。しかし、2011年に仕様が変更されて非公開のメンバーが存在してもコピー可能なようになりました。
そのためパッケージ外部に公開されているstructはコピーされても不都合が(あまり)起こらないようにすべきです。コピーされると非常に不都合なstructはinterfaceだけを公開して実際の実装であるstructを隠すか、あるいはコピーされたくないフィールドを別のstructに分離して公開するstructではそのstructへのポインタを保持するようにします。
例えば標準ライブラリのos.Fileは、ソースコードを見るとファイルディスクリプタなどを管理する private な os.file struct
へのポインタとなっています。これはファイルの実体に対応する構造体os.file
がコピーされて同じファイルが2回閉じられたりするようなことが起こらないように配慮された結果です。
ちなみにos.Fileは*os.file
を1つ持つだけのstruct
ですが、これをtype File *file
としては意味がありません。なぜならtype File *file
としてしまうと、os.File
はポインタなので例えos.file
が非公開だとしても*
演算でポインタの実体が参照できてしまい、*file0 = *file1
のように書くことでos.file
のコピーがパッケージ外部でもできてしまうからです。
型が後置
これは慣れです。C, Java系をメインで使っている多くのプログラマには最初は違和感がありますが、まあ割とどちらでもいいなという気分になります。多分。
1.0 が浮動小数点型にならない(時がある)
https://play.golang.org/p/JjB2WDohT3
名前が…
何でこんな検索しにくい名前なのだろうな…