My External Storage

Jul 7, 2019 - 5 minute read - Comments - go

[Go]Sliceを含んだ構造体が等値演算子(==)でpanicを引き起こすのを回避する #golang

Goにはcomparableが定義されておらず、比較できない型としてSlice, Mapなどがある。
interfaceがそのような型(フィールドにそのような型を持った構造体)を値に持っていたときに==を利用するとpanicが発生する可能性がある。
行儀の悪い構造体を定義しないテクニックがあったのでメモしておく。

TL;DR

  • Goの値はすべてがcomparableではない
    • Slice、Map、関数の値はcomparableではない
  • comparableではない値を指すインターフェースに等価演算子を使うとruntime errorが発生する
  • 構造体にSliceを含ませながら等価演算子を使いたいならばSliceのポインタで内包すれば良い
    • ただし当然ポインタの指し示す値の比較で等価であるか決まるようになる

Goの値はすべてがcomparableではない

プログラミング言語によってはすべてのオブジェクトがequalityを持ち、comparableであることもある。 が、Goの場合そうではない。Goは仕様にcomparebleでない型があることを明記している。

Slice, map, and function values are not comparable. However, as a special case, a slice, map, or function value may be compared to the predeclared identifier nil. Comparison of pointer, channel, and interface values to nil is also allowed and follows from the general rules above.

上記の通り、not comparableな値とはSliceやMapfunctionへの値のことを指す。

comparableではない値を指すインターフェースに等価演算子を使うとruntime errorが発生する

このcomparable性については構造体にも影響する。 構造体がnot comparableな型のフィールドを持つとき、その構造体もnot comparableになる。
明示的にnot comparableな構造体を比較しているときはビルド時にエラーになるので気づく。

 a1 := struct {
 	Slice []string
 }{
 	[]string{
 		"hoge",
 	},
 }
 
 // invalid operation: a1 == a1 (struct containing []string cannot be compared)
 if a1 == a1 {
 	fmt.Println("Same article")
 }

問題はinterfaceでそのような構造体を指していたときだ。
not comparableな値を指した状態のinterface値を等価演算子で比較すると、runtime errorが発生する旨が仕様に記載されている(記載場所は上記リンクと同じ)。

A comparison of two interface values with identical dynamic types causes a run-time panic if values of that type are not comparable. This behavior applies not only to direct interface value comparisons but also when comparing arrays of interface values or structs with interface-valued fields.

下記のサンプルコードはinterfaceを介して[]stringを内包する構造体を比較してしまう。 そのため、ビルドには成功するが、実行時にruntime errorが発生する。

// https://play.golang.org/p/FO_vwjsT7TY
package main

import (
	"fmt"
)

type Article struct {
	Title string
	Tag   []string
}

func main() {
	var a1 interface{} = Article{
		Title: "Content Title",
		Tag: []string{
			"go",
			"slice",
		},
	}

	var a2 interface{} = Article{
		Title: "Content Title",
		Tag: []string{
			"go",
			"slice",
		},
	}
	// panic: runtime error: comparing uncomparable type main.Article
	if a1 == a2 {
		fmt.Println("Same article")
	}
}

サンプルコードではnot comparableな値が入っているのは自明だが、関数やメソッドの引数をinterfaceとしていたとき、渡ってきた具象型がcomparableかどうかわざわざ検証しないだろう。 errorインターフェースなど、具象型を意識しないでで取り扱う構造体はcomparableな構造体にしておきたい。

構造体をcomparableにしてruntime errorを回避するにはポインタを使ってフィールドを定義する

では具体的にどのように回避すればよいかというと、ポインタを利用することでSliceを内部で持っていたままcomparableな構造体にできる(Sliceの場合は配列にしてもよい)。 サンプルコードは以下。

// https://play.golang.org/p/6RGzUoPFrjW
package main

import (
	"fmt"
)

type Tags *[]string

type Article struct {
	Title string
	Tag   Tags
}

func main() {
	var a1 interface{} = Article{
		Title: "Content Title",
		Tag: &[]string{
			"go",
			"slice",
		},
	}

	var a2 interface{} = Article{
		Title: "Content Title",
		Tag: &[]string{
			"go",
			"slice",
		},
	}
	if a1 == a2 {
		fmt.Println("Same article")
	}
}

[]stringではなく、*[]stringにした構造体を宣言するとruntime errorは解消された。
ただ、当然ポインタで比較されるようになるのでそこは注意する。 なので、サンプルコードのa1a2は直感的には「等価」だが、ポインタの指すSliceは別物なので「等値」ではなく!=になる。 あくまで何も知らないでinterfaceを経由して利用される場合のruntime error回避のためのテクニックなので、構造体の中身の詳細を知っているコードではreflect.DeepEqualなどを利用する必要がある。

終わりに

業務で実際にruntime errorが発生する可能性があったので、どうしたら回避できるのか調べたのが今回の記事になる。
「そういえばpkg/errorsはSliceでStackFrame持っているはずだけど同じエラーが発生しないな?」と調べたらポインタを使っていた。
著名なOSSのコードはやはり勉強になるので使うだけでなくちゃんと読んでみないとなと改めて思った。

// https://github.com/pkg/errors/blob/27936f6d90f9c8e1145f11ed52ffffbfdb9e0af7/errors.go#L119-L123
// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
	msg string
	*stack // type stack []uintptr
}

追記

比較する2つのオブジェクトが違う型ならばruntime errorが発生するnot comparableなフィールドを触る前に比較が終わるのでruntime errorは発生しない。 なので、比較可能な具象型をどちらかに置いて比較演算子を書けばruntime errorが出ないことは保証される(interface == interfaceと比較するケースはあまりないだろう)。

// https://play.golang.org/p/Ygi0f5GQIV3
package main

import (
	"fmt"
)

type NotCompareble []string

func main() {
	var a interface{} = []string{
		"go",
		"slice",
	}

	var b interface{} = NotCompareble{
		"go",
		"slice",
	}
	// not runtime error, even if each type are not comparable, because a, b are different type.
	if a != b {
		fmt.Println("different object")
	}
}

関連記事