Go 言語(Golang) はまりどころと解決策

Go の言語仕様はシンプルで他の言語に比べてはまりどころが少なくて学習コストが小さめな言語のように思います。しかし、それでもはまるところがないわけではないので、自分がはまって時間を無駄にしてしまったことを書き留めておきます。

念の為誤解のないように追記しておくと、この文書の目的は Go を批判することではなく Go が Go であるがゆえに C++/Java/Python など利用者が Go を使い始めるときに困惑あるいは誤解するであろうポイントをまとめておくことで初めて Go を触る人がスムーズに Go を使い始められるようにすることです。私個人は Go はバランスがとれた良い言語でだと思いますし、気に入っています

目次

interface と nil (Go の interface は単なる参照ではない)

Java や C#あるいは C++の経験があり、ある程度クラスの内部構造への理解がある人は Go の interface の実体もデータへの参照だと考えると思います。 しかし実はそれは正しくありません。Go の interface の実体は参照(ポインタ)ではありません。Go の interface の実体は参照と型情報のペアです

さてこの内部構造の違いが Go にどういった影響をもたらすのでしょうか。実はこの内部構造の違い、意外と言語の挙動にはあまり大きな影響を与えません。そのためこの点を理解していなくても Go でプログラムを書けてしまいます。ただし nil を扱う場合には Go は予想外の挙動をします。

package main

import "fmt"
import "reflect"

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

	fmt.Println("---- err ----")
	fmt.Println("is nil:", err == nil)
	fmt.Println("Type:", reflect.TypeOf(err))
	fmt.Println("Value:", reflect.ValueOf(err))

	var trueNil error
	fmt.Println("---- trueNil ----")
	fmt.Println("is nil:", trueNil == nil)
	fmt.Println("Type:", reflect.TypeOf(trueNil))
	fmt.Println("Value:", reflect.ValueOf(trueNil))
}

上のコード(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 くらいは目を通しておいたほうがよいですね。

文献

メソッド内でレシーバ(this, self)が nil でないことをチェックすることに意味がある

これははまりどころというより、Go だとnilに大してメソッド呼び出しを行った場合の挙動が他の人気のある言語と少し異なるので、メソッド側の書き方次第では呼び出し側のnilチェックをすこし緩和できるよという話ですが。あるいはこの点を理解していないと、呼び出し側でnilチェックしなくてなぜ大丈夫なのか困惑するという話です。

C++, Java, Python, JavaScript などの他の人気のある言語では

person.sayHello()

のようにメソッド呼び出しをする場合は(C++の場合は.でなく->)、personnullでないことを確認しなくてはなりません(C++でsayHelloが non-virtual の場合も挙動は未定義)。 しかし Go の場合はpersonstructのポインタである場合にはpersonnilでも関数の呼び出しは問題なく行えます。 また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 としてパース出来ないのでエラーが発生しますが、コンパイル時にはエラーが無視されていることは検出されません。これに関しては気をつける以外に解決策はないと思います。

defer の中で発生した error を処理し忘れる

Go (golang) ではファイルを閉じたりセッションを閉じたりといった、リソースの解放処理はdeferで行います。 defer x.Close()みたいなコードを書くことが多くあると思います。 Go Blog の defer, panic and recoverにも似たようなコードがあります。

func WriteSomeData() error {
	w := OpenData()
	defer w.Close()
	if err := w.Write("...."); err != nil {
		return err
	}
	// Write more to w...
}

このコード問題ないようにみえます。そもそも Go Blog ほぼ同じものが書かれているのだから問題がないはずです。

しかしこのコードでは重大なことが忘れ去られてしまっています。Closeの定義はClose() errorでありエラーを返す可能性があるのです。現実のコードではClose()でエラーが発生するような場合はClose()よりも前の段階の処理でエラーが発生しがちなのでClose()のエラーを無視してしまっていても、あまり問題にならないのかもしれません。しかしClose() errorがエラーを返す以上、ライブラリや状況によっては重要なエラーがClose()の段階になって初めて発生する可能性も十分にありえます。その場合にはエラーは完全に消失してしまうので実行結果がおかしいけれどエラーはどこにも出ないという非常に厄介な状態になります。そのため、deferで関数を呼び出す場合はその関数がerrorを返すことがないか慎重にチェックすべきです。Go Blog の記事ではチェックされていませんが。

問題はどのように関数の終了し返り値も決定された後に実行されるdeferの内部で生じたerrorを処理するかです。結論としてはdefer内部で発生したerrorを処理するのには名前付き返り値を利用します。

func WriteSomeData() (err error) {
	w := OpenData()
	defer func() {
		cerr := w.Close()
		if cerr == nil {
			return
		}
		err = fmt.Errorf("Failed to close: %v, the original error was %v", cerr, err)
	}()
	if err := w.Write("...."); err != nil {
		return err
	}
	// Write more to w...
}

このようにdeferの中で発生したerrorを呼び出し元に返すには

  • 返り値のerrordeferから上書きできるように名前(err)をつけておく
  • deferで直接w.Close()を呼ぶのではなく、w.Close()の返り値をチェックしてエラーが発生したらerrを上書きするような関数を呼び出す
  • errの元の値にも重要なエラー情報が入っている可能性があるので、間違って err = w.Close() などとして上書きして消してしまわないように注意が必要。

うーん随分冗長ですね。これは間違えやすいように思いますし、もう少し賢くとりあつかえるように言語仕様が変わるとうれしいですね。 errorを明示的に扱わなければ行けない一方、うっかりerrorを処理し損なうとerrorが完全にどこかに消えてしまうのはエラー処理に例外を使用しない Go の割りと致命的な欠陥のように思うので、将来うまく扱う方法が現れるとよいです。

基本型がメソッドを持たない

例えば 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 には継承はありませんそもそも継承はプログラミング言語にあまり必要ない機能だと思います。 継承が本当に有益なこともありますが、経験上大半のケースでは設計を手抜きするために継承が使われていて、結果長い目で見た際の 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のコピーがパッケージ外部でもできてしまうからです。

キャストという概念がない

Go (golang) にはキャストがありません。そもそも C++や Java のキャストは

  • プリミティブ型のデータ変換(int→float)
  • 親クラス、インターフェイスへの参照をサブクラス参照へと型を変更(Object→String)

という 2 つの全く異なる処理が「キャスト」という概念に統合されてしまっています。 そのためC++では旧来のキャストに加えて 4 つのキャスト演算がサポートされたりしています。 Go ではこの2つの処理は全く異なるシンタックスで扱われます。そもそも全く異なる処理なのだから、Go のやり方のほうが正しいです。

  • プリミティブ型のデータ変換

    • intfloatなどのプリミティブ型間のデータ変換には newtype(val) を使います。
    • int64(f), float64(i) など
    • []bytestringの変換も非常によく使います。[]byte(str), string(binary)
  • interface から実体を取り出す

    • obj.(subType)という少し風変わりなシンタックスで処理します。
    • Java で親クラスからサブクラスに、インターフェイスからサブインターフェイスに変換するのに相当する処理です。 interface からその実体の struct や struct のポインタ、あるいはより詳細な interface に変換します。
    • このシンタックスは 3 通りの使い方があります。
    • s := obj.(*myStruct)
    • interface obj*myStructとして扱えるかをチェックし、可能な場合は*myStructを取り出します。 そうでない場合はpanicします。
    • s, ok := obj.(*myStruct)
    • 上記とほぼ同じですが、panic する代わりにチェック結果をbool値のokに格納します。okであってerrorでないので注意。
    • 失敗した場合は、sにはnilなどが入ります。
    • type switch
    • s, ok := obj.(subType)を複数の型に対して行って条件分岐するのを簡潔に記述するための特殊な構文が用意されています。
    • An example on Go Playground

型が後置

これは慣れです。C, Java 系をメインで使っている多くのプログラマには最初は違和感があります。でも使っていれば割とどちらでもいいなという気分になります。 とはいえ後置の言語と前置の言語両方書いてるとたまに混乱します。

1.0 が浮動小数点型にならない(時がある)

https://play.golang.org/p/JjB2WDohT3

名前が...

何でこんな検索しにくい名前なのだろうな...結果 golang で検索する羽目になるなら最初から golang という名前だったらまだ良かったのにと思います。

最終更新: 2017/4/14