Go 言語(Golang) はまりどころと解決策
Go の言語仕様はシンプルで他の言語に比べてはまりどころが少なくて学習コストが小さめな言語のように思います。しかし、それでもはまるところがないわけではないので、自分がはまって時間を無駄にしてしまったことを書き留めておきます。
念の為誤解のないように追記しておくと、この文書の目的は Go を批判することではなく Go が Go であるがゆえに C++/Java/Python など利用者が Go を使い始めるときに困惑あるいは誤解するであろうポイントをまとめておくことで初めて Go を触る人がスムーズに Go を使い始められるようにすることです。私個人は Go はバランスがとれた良い言語でだと思いますし、気に入っています。
目次
- メソッド内でレシーバ(this, self)が nil でないことをチェックすることに意味がある
- error しか返り値がない関数で error を処理し忘れる
- defer の中で発生した error を処理し忘れる
- 基本型がメソッドを持たない
- string が単なるバイト列
- 継承がない
- Generics がない
- goroutine は GC されない
- goroutine は generator (yield) の実装には使えない
- 例外が(推奨され)ない
- return nil, err → この error どこで発生したの?
- 関数より狭いスコープで defer
- キャストという概念がない
- 型が後置
- 1.0 が浮動小数点型にならない(時がある)
- 名前が...
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 くらいは目を通しておいたほうがよいですね。
文献
- 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 としてパース出来ないのでエラーが発生しますが、コンパイル時にはエラーが無視されていることは検出されません。これに関しては気をつける以外に解決策はないと思います。
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
を呼び出し元に返すには
- 返り値の
error
をdefer
から上書きできるように名前(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 ではファイルなどから
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
のコピーがパッケージ外部でもできてしまうからです。
キャストという概念がない
Go (golang) にはキャストがありません。そもそも C++や Java のキャストは
- プリミティブ型のデータ変換(int→float)
- 親クラス、インターフェイスへの参照をサブクラス参照へと型を変更(Object→String)
という 2 つの全く異なる処理が「キャスト」という概念に統合されてしまっています。 そのためC++では旧来のキャストに加えて 4 つのキャスト演算がサポートされたりしています。 Go ではこの2つの処理は全く異なるシンタックスで扱われます。そもそも全く異なる処理なのだから、Go のやり方のほうが正しいです。
-
プリミティブ型のデータ変換
int
→float
などのプリミティブ型間のデータ変換にはnewtype(val)
を使います。int64(f)
,float64(i)
など[]byte
とstring
の変換も非常によく使います。[]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 という名前だったらまだ良かったのにと思います。