LoginSignup
2
1

Goにおけるinterface{}のnilの挙動

Last updated at Posted at 2023-12-21

Go Advent Calendar 2023の22日目の記事です。

はじめに

あるコードをリファクタリングしている際に得たnilの挙動についての記事となります。

コード

突然ですが、以下のコードがどのような挙動をすると思いますか?
(実際のコードを簡略化したものとなります)

func main() {
	_, err := hoge()
	if err != nil {
		fmt.Println(err)
	}
}

func hoge() (ret *struct{}, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = errors.New("hoge error")
		}
	}()
    // なんか処理してエラーがあったらpanic
	if true {
		panic(ret)
	}
	return
}

正解は呼び出し元にてhoge errorが出力されます。

これはこれで、プロダクション環境において、正しく動いており、特に問題はなかったのですが、例えばpanicにエラーではなく、構造体を入れていたりと、リファクタリングをする余地があったため、実施しようと思ったのですが、ふと疑問に思いました。

「retは構造体のポインタでnilになっているはずなのになぜrecoverの分岐に入るのだろうか」

Goにおけるnilの扱い

ドキュメントに解説がありました。

An interface value is nil only if the V and T are both unset, (T=nil, V is not set), In particular, a nil interface will always hold a nil type. If we store a nil pointer of type *int inside an interface value, the inner type will be *int regardless of the value of the pointer: (T=*int, V=nil). Such an interface value will therefore be non-nil even when the pointer value V inside is nil.

interface{}においてのnilとは型(T)と値(V)の両者がnilで、nilが成立するようです。
つまり、上記のr := recover()の部分において、rinterface{}をとるため、TVnilにならなかったということになります。

実際に確認してみました。

func hoge() (ret *struct{}, err error) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println(r == nil) //false
			fmt.Println(r.(*struct{}) == nil) //true
			fmt.Println("T:", reflect.TypeOf(r)) //T: *struct {}
			fmt.Println("V:", reflect.ValueOf(r)) //V: <nil>
			err = errors.New("hoge error")
		}
	}()

	if true {
		panic(ret)
	}
	return
}

結果の通り、Vnilとなりましたが、T*struct{}であるため、interface{}としての比較r == nilfalseとなりました。一方で、*struct{}にキャストした状態での比較r.(*struct{}) == niltrueとなっています。

また上記の挙動についての日本語の記事はNoboNoboさんのこちらの記事がわかりやすかったです。

linterによる検知

今回はバグが発生することはありませんでしたが、時として、一見して分かりづらいこの挙動はバグの発生となる可能性もあります。しかし、すでにlinterによるgo-staticcheckがあるため、通常であれば、事前に防ぐことができます。
今回は少し特殊?なパターンだったため、検知されませんでしたが、、

例えば上記のドキュメントにあるようなerrorを例に取るとlinterによる検知が可能になりました。

func main() {
	err := fuga(false)
	if err != nil {
		fmt.Println(err)
	}
}

type MyErr struct{}

func (MyErr) Error() string {
	return "MyErr"
}

func fuga(isErr bool) error {
	var my *MyErr = nil
	if isErr {
		my = &MyErr{}
		return my
	}
	return my
}

fugaの戻り値であるerrorインターフェースは実際にMyErr型であるため、err != niltrueになることはありませんが、VSCode以下の様に警告が出てきました。
image.png

終わりに

今回はnilの挙動についての一部を紹介させていただきました。
普段はあまり意識することなく、コーディングできるかと思いますが、こういった挙動を知っているだけでも思わぬバグを踏まずに済むかもしれません。

では残り少ないアドベントカレンダーを楽しんでいきましょう!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1