Edited at

GOの初心者が絶対に一度はハマる知っておいた方がいいことのまとめ

More than 1 year has passed since last update.


はじめに

Go言語にはGo言語初心者だからこそハマるポイントがあるようです。

自分なりにハマる箇所と解決策を調べてまとめてみました。

どんな問題が潜んでいるか知っておくだけでも全然違うと思います。

以下のFrequently Asked Questions (FAQ)を参考にしています。

※ どの質問に上がっていたのかはURLを一緒に貼るので原本として参考にしてください。

https://golang.org/doc/faq#assertions


1.nilだけどnilではない

まずはコードから見てもらった方が速そうなので見てください。

答えを予想しながらリーディングしてください。

// main.go

type Err struct {
N string
}

func (err *Err) Error() string {
return fmt.Sprintf("Error:", err.N)
}

func Hoge(test string) error {
var err *Err
if test == "test" {
err = &Err{"fuga"}
}
return err
}

// main_test.go
func Test(t *testing.T) {
err := Hoge("")

if err != nil {
t.Error("err:", err)
}
}

Hogeの引数に何も渡していないので、errは初期値であるnilを返すと思いますよね?

でもerr == nil にはなりません。

仕様書に「interfaces are implemented as two elements, a type and a value.」とあります。

なるほど、interfacesは型と値を持っているようです。

中身を見てみるとこのような結果に・・・

fmt.Println(reflect.ValueOf(err))

// <nil>

fmt.Println(reflect.TypeOf(err))
// *main.Err

型も値もnilでないと err == nil がtrueにはならないようです。


解決策

解決策としてnilならnilを明示的に返却すれば問題なさそうです。

func Hoge(test string) error {

if test == "test" {
return &Err{"fuga"}
}
return nil
}

他の策としてはこんなチェックの方法もできます。

e == nil || reflect.ValueOf(e).IsNil()

以下の記事を参考にしています。

https://golang.org/doc/faq#nil_error


2.Receiverはnilに対しても使えてしまう

例にならってコードから

type Test struct {

N string
}

func (t *Test) Fuga() string {
if t == nil {
return fmt.Sprintf("nilだよーん")
}
// errを使った処理をしているが、errがnillだとpanic
return fmt.Sprintf("Error:", t.N)
}

func Hoge() {
var err *Test = nil
err.Fuga()
}

他の言語ではnilに対してメソッドを読んだ時、その時点でエラーがおきますが、Goではおきないようです。

理由はnil.receiver()としたときは、参照先をさしていないからです。

Receiver内のt.Nでは参照の参照先をさすためnilによるエラーがおきます。


解決策

Receiver側、つまり呼び出される側でnilチェックをするべきか、呼び出す側でエラーチェックをするか検討する必要がある。

例えば、標準ライブラリのfile_unix.goを見て見ましょう。

func (f *File) Close() error {

if f == nil {
return ErrInvalid
}
return f.file.close()
}

NewFile関数がnilを返す可能性があるため、fileを閉じる際は、nilチェックをしています。

NewFile関数でnilが帰ってくることはerrorではありません、なのでerrorを返さず、nilチェック。

標準ライブラリのfile.goでは?

func (f *File) Read(b []byte) (n int, err error) {

if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
return n, f.wrapErr("read", e)
}

nilチェックをしていません。

理由は、nilに対してファイル 読み込みを行うことはerrorであるから。

だから呼び出す側でerrチェックをする必要があります。

@najeira さんご指摘ありがとうございました。


3.deferでerrorが起こったら?

このソースにはバグがあります。

func CopyFile(dstName, srcName string) (written int64, err error) {

src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()

return io.Copy(src)
}

src.Close() の中身を見てみると、返り値にerrorとあります。

つまり、エラーを返す可能性があります。

func (f *File) Close() error {

if f == nil {
return ErrInvalid
}
return f.file.close()
}

ですが、defer src.Close() のようにerrorの処理を忘れて遅延関数を呼び出しています。


解決策

解決策としては、以下のような感じでRecoverするべし

※ めんどかったので実装して確かめてはいないです。来る時が来たらします。

func f() {

defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}

以下の記事を参考にしています。

https://blog.golang.org/defer-panic-and-recover


4.スライスのappendには注意しろ

ちょっとGoのおさらい、配列は固定長で値渡し、スライスは可変長で参照渡しですよね?

ではコードを見て見ましょう。

func main() {

hoge := []int{1,2,3}
add(hoge)
fmt.Println("結果:",hoge)
}
func add (hoge []int) {
// 値を書き換えてる
hoge[0] = 11
// appendしている
hoge = append(hoge,4)
}
/*
結果: [11 2 3]
*/

hogeに対しての代入は反映されていますが、appendは反映されていないですね。

これが「appendには注意しろ!」の実態です。

詳しくは、他の記事を参照して欲しいですが、簡単にいうと、

スライスは内部で固定長のデータを持っていて、appendする際に新しくメモリ領域を確保するため、参照先が変わっているそうです。


解決策

こんな感じでポインタ渡しにすれば、appendしてもちゃんと元の値も反映される。

func add(hoge *[]int) {

// 受け取ったポインタにappend
*hoge = append(*hoge,5)
}
func main() {
hoge := []int{1, 2, 3, 4}
add(&hoge)
fmt.Println("結果:",hoge)
}

/*
結果: [1 2 3 4 5]
*/

内部構造などに踏み入れて詳しく理解したい方は以下の記事が参考になるかと思います。

http://jxck.hatenablog.com/entry/golang-slice-internals

http://jxck.hatenablog.com/entry/golang-slice-internals2


5.ループ内のdeferには注意せよ

まずはコードから!どんな結果になるか想像して見ましょう。

func hoge() {

for i := 0; i < 5; i++ {
defer func() {fmt.Println("third",i)}()
fmt.Println("first")
}
fmt.Println("second")
}
func main() {
hoge()
}

/*
first
first
first
first
first
second
third 5
third 5
third 5
third 5
third 5
*/

なんでこんな結果になったかというと、

遅延された関数はそれを含んでいる関数の最後まで実行されないからです。

なのでdeferはhoge関数が終わるときに呼び出される仕様になっています。

また、おまけとして、全てthird 5になっていますが、deferは値を保持するわけではなく、

呼ばれたタイミングでの値を使います。

以下の記事が参考になります。

https://blog.golang.org/defer-panic-and-recover


まとめ

Goのハマりそうなところを調べて、実際にハマって、解決してみました。

やっぱり言語が作られた背景だったり、思想を理解する必要がありそうです。

他にもハマるポイントはあると思います。

「こんなのもあるよ!」といったのがありましたら、解決策も含めて教えていただけると幸いです。