豪鬼メモ

一瞬千撃

Go言語におけるスライスや文字列のアドレス検査と特異値

Go言語でスライスや文字列をよく使うが、コード上ではアドレスが巧みに隠蔽されて、安全なプログラミングができるようになっている。しかし、たまに、アドレスを使ってユニーク性を調べたいときがある。その方法についてメモする。


C++でcharの配列を作った場合、その配列は確保した領域のアドレスとして評価される。つまり、そのアドレスはその配列オブジェクトのIDとして扱える。内容がたまたま同じ別の配列オブジェクトがあったとしても、アドレスを比較すれば別のオブジェクトであるとわかる。

// 内容が "John Doe" であるC文字列を作る
char* john = new char[256];
strcpy(john, "John Doe");

// 同じオブジェクトを指すエイリアスを作る
char* john_alias = john;

// 内容が同じC文字列を作る
char* john_jr = new char[256];
strcpy(john_jr, "John Doe");

// アドレスを比較する = 同一比較
printf("%d\n", john == john_alias);  // -> 1
printf("%d\n", john == john_jr);  // -> 0

// 中身を比較すれば、両者は同じものだ = 同値比較
printf("%d\n", strcmp(john, john_alias));  // -> 0
printf("%d\n", strcmp(john, john_jr));  // -> 0

Goでは、普通の方法では、スライスの参照先のアドレスの比較ができないようになっている。ポインタの同値比較をしてしまうと、そのスライスを参照する変数のアドレスを比較することになるので、意味がない。

// 内容が "John Doe" であるスライスを作る
john := []byte("John Doe")

// 同じオブジェクトを指すエイリアスを作る
john_alias := john

// 内容が "John Doe" であるスライスを作る
john_jr := []byte("John Doe")

// アドレスを比較する = 同一比較
fmt.Printf("%v\n", &john == &john_alias)  // -> false
fmt.Printf("%v\n", &john == &john_jr)     // -> false

// 中身を比較すれば、両者は同じものだ = 同値比較
fmt.Printf("%v\n", reflect.DeepEqual(john, john_alias))  // -> true
fmt.Printf("%v\n", reflect.DeepEqual(john, john_jr))     // -> true

変数johnと変数john_aliasが指すスライスが同じオブジェクト(=確保されたメモリ領域)であることを知るには、以下のようにリフレクションを使うことになる。

func CheckSameBytes(a, b []byte) {
  return ((*reflect.SliceHeader)(unsafe.Pointer(&a)).Data ==
    *reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
}

同じことが文字列でもできる。Goでは文字列とバイトスライスは本質的に同じものだ。

func CheckSameString(a, b string) {
  return ((*reflect.StringHeader)(unsafe.Pointer(&a)).Data ==
    *reflect.StringHeader)(unsafe.Pointer(&b)).Data)
}

これができると何が嬉しいかというと、nil以外の任意の特異値をバイトスライスに導入でき、またnil相当の特異値をstringに導入できることだ。例えば、任意の値の存在を示すAnyBytesという特異値を作るには、適当な値を持ったスライスを作ればよい。

var AnyBytes = []byte("_ANYBYTES_")

AnyBytesはユニークなアドレスを持つので、アドレスを比較すれば、特異値かどうかがわかる。

func IsAnyBytes(data []byte) {
  return ((*reflect.SliceHeader)(unsafe.Pointer(&data)).Data ==
    *reflect.SliceHeader)(unsafe.Pointer(&AnyBytes)).Data)
}

重要なのは、たまたま "_ANYBYTES_" という内容を持つ別のスライスが渡されても、ちゃんと偽を返すことだ。なので、AnyBytesの中身は別に空文字列でもよい。デバッグ出力で便利なように読みやすい文字列を入れているだけだ。

文字列の特異値を作る場合には、以下のようにする。なぜ一旦バイトスライスにするかというと、文字列リテラルをそのまま指定すると、同じリテラルが別の場所で使われた場合にユニーク性が保証できないからだ。

var AnyString = string([]byte("_ANYSTRING_"))

func IsAnyString(data []byte) {
  return ((*reflect.StringHeader)(unsafe.Pointer(&data)).Data ==
    *reflect.StringHeader)(unsafe.Pointer(&AnyBytes)).Data)
}

インターフェイスが絡んでもこの特異値判定ができる。

func IsAnyData(data interface{}) bool {
  switch data := data.(type) {
  case []byte:
    return ((*reflect.SliceHeader)(unsafe.Pointer(&data)).Data ==
      (*reflect.SliceHeader)(unsafe.Pointer(&AnyBytes)).Data)
  case string:
    return ((*reflect.StringHeader)(unsafe.Pointer(&data)).Data ==
      (*reflect.StringHeader)(unsafe.Pointer(&AnyString)).Data)
  default:
    return false
  }
}

ちょっと無理やりな嫌いはあるが、既存のAPIを変えずにどうしても特異的な挙動を導入したい場合には、この技法は便利だ。具体的に言えば、TkrzwのComapreExchangeの事前条件パラメータに、ワイルドカードとしてこのAnyBytesとAnyStringを加えたいという話だ。特異値なんてハックはダサいとも思えるが、nilっていう特異値をいつも使っているじゃないか。nilがいいなら、anyがあったっていいじゃないか。CompareExchangeは引数にバイト列でも文字列でも受け付けるので、こんなふうに柔軟な書き方ができる。

// rec1というキーのレコードの値がfooの時にそれをbarにする
dbm.CompareExchange("rec1", "foo", "bar")
dbm.CompareExchange("rec1", bytes[]("foo"), "bar")

// rec1というキーのレコード存在しないなら、値がbarのレコードを格納する
dbm.CompareExchange("rec1", NilString, "bar")
dbm.CompareExchange("rec1", nil, "bar")

// rec1というキーのレコードが値は何であれ存在するなら、その値をbarに変える
dbm.CompareExchange("rec1", AnyString, "bar")
dbm.CompareExchange("rec1", AnyBytes, "bar")

繰り返しになるが、オブジェクトの同一性はあくまで参照先のアドレスで判定され、バイト列の内容や変数のアドレスからは独立なので、別の変数に代入しても同一性は失われないし、たまたま内容がAnyBytesやAnyStringのものと同じレコードを操作する場合にも誤って同一と判定されることがない。

一点注意が必要なのは、空文字列や空バイト列を特異値の内容には指定すべきではないということだ。なぜなら、それらを生成すると、最適化が働いて、常に同じアドレスの領域を指すからだ。なので、"a" でも "\x00" でも何でも良いので、空ではない内容を指定されたい。