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"

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

文献

メソッド内でレシーバ(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としてパース出来ないのでエラーが発生しますが、コンパイル時にはエラーが無視されていることは検出されません。これに関しては気をつける以外に解決策はないと思います。

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

例えば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を出力します。ただこのやり方には

という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のキャストは

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

型が後置

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

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

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

名前が…

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

このエントリーをはてなブックマークに追加
Home