17
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NRI OpenStandiaAdvent Calendar 2024

Day 8

Go言語のaddressableについて説明する

Last updated at Posted at 2024-12-07

はじめに

any型(interface{})に入っている構造体のポインタを取得しようとしてreflectパッケージのfunc (v Value) Addr() Valueメソッドを使ったところ、以下のエラーが発生しました。

panic: reflect.Value.Addr of unaddressable value
原因を調べたところaddressableという概念に出くわしました。

「アドレスでアクセス可能」であることを意味する単語ですが、正直これだけではよくわかりません。そこで、

  • 「アドレスでアクセス可能」とは具体的にどういうことか
  • addressableでない具体例2つ

を説明します。またこれらを踏まえてaddressableの定義を説明し、any型からポインタを取得するコードを紹介します。

アドレスでアクセス可能とは

addressableに関係するGo言語の一番重要な仕様は、代入だと思います。
addressableであることはGo言語における変数への代入の前提になっています。

var int x
x = 10

この例のようにx10を代入できるのは、xがaddressableだからです。
実際、

fmt.Println(&x) //0xc00009a0b0

のようにアドレスを取得でき、xは「アドレスでアクセス可能」です。変数は言語仕様でaddressableと定義されていて、&をつけてアドレスを取得できます。

これは使用しない値がメモリ上に残ることを避けるための仕様です。ポインタで参照できない場所に値を代入してもメリットはなく、またGC(Garbage Collection)の負担になってしまいます。

代入の他に構造体フィールドへのアクセスやインクリメント等、addressableを必要とする操作は多数あります。

では、addressableでないものとは、具体的にどういったケースでしょうか。

addressableでない例① 関数の返り値

package main

import "fmt"

type animal struct{}

func newAnimal() animal {
	return animal{}
}

func main() {
    fmt.Printf("%p\n", &newAnimal()) //invalid operation: cannot take address of newAnimal() (value of type animal)
}

animal構造体を作成する関数の戻り値に対してアドレスを取得しています。
関数の戻り値はaddressableでないのでビルドエラーが発生します。

func main() {
    a := newAnimal()
    fmt.Printf("%p\n", &a) //0xc00009c050
}

一度変数に代入することでエラーを回避できます。

addressableでない例② mapのインデックス指定

package main

type animal struct {}

func main() {
	m := make(map[string]animal)
	m["animal"] = animal{}

	fmt.Printf("%p\n", &m["animal"]) //invalid operation: cannot take address of m["animal"] (map index expression of type animal)
}

sliceやarrayの要素がaddressableなのに対して、mapの要素はaddressableではありません。mapの要素に対してアドレスを取得するとビルドエラーが発生します。

mapの要素はaddressableではありませんが、例外的に
m["animal"] = animal{}のように代入することができます。

func main() {
	m := make(map[string]animal)
    m["animal"] = animal{}
    a := m["animal"] 
	fmt.Printf("%p\n", &a) //0xc000106050
}

こちらも、一度変数に代入することでエラーを回避できます。

addressableの定義

ここで、addressableなものを整理します。
言語仕様書のAddress_operatorsの章に、addressableの定義は以下のいずれかに該当すること、とあります。

  • 変数(例:var x)
  • 間接演算子(例:*x)
  • スライスのインデックス指定(例:s[10])
  • addressableな構造体のfield selector(例:s.name)
  • addressableなarrayのインデックス指定(例:a[10])

これまでの例で説明した「関数の呼び出し」や「マップのインデックス指定」は、これらに該当しないのでaddressableではありません。

Addr()メソッドがパニックになる理由

冒頭の話題にもどりまして、Addr()メソッドを呼び出すにはreflect.Valueが、addressableである必要があります。
reflectパッケージの言うaddressableはreflectパッケージがアドレスを取得できるかどうかが基準です。
例えば、以下はaddressableではありません。

func main() {
	num := 10
	r := reflect.ValueOf(num)
	fmt.Println(r.CanAddr()) //false
}

CanAddr()はreflect.Valueがaddressableかどうかを判定するメソッドです。
num変数は&numのようにアドレスを取得可能のはずですが、CanAddr()の返り値はfalseになってしまっています。
これは、reflectパッケージがnumのコピーの値を保持するだけで、アドレスに関する情報は抜け落ちてしまうからです。
このnumをaddressableにするにはポインタでreflectパッケージに渡し、その後Elem()メソッドを呼び出してポインタが指す値を取得する必要があります。

func main() {
	num := 10
	r := reflect.ValueOf(&num).Elem()
	fmt.Println(r.CanAddr()) //true
}

そうすることでreflect.Valueの値が元のポインタと紐づき、addressableなものとして扱うことができます。

any型に入っている構造体のポインタを取得する方法

func getStructPointer(v any) reflect.Value {
    r := reflect.ValueOf(v)

    if r.CanAddr() {
    	return r.Addr()
    }

    ptrType := reflect.PointerTo(r.Type())

    ptr := reflect.New(ptrType.Elem())

    ptr.Elem().Set(r)

    return ptr
}

any型の値から構造体のポインタを取得する関数です。

if r.CanAddr() {
	return r.Addr()
}

reflect.Valueがaddressableだった場合、直接Addr()関数を使ってポインタを取得することができます。

ptrType := reflect.PointerTo(r.Type())

構造体のポインタ型を取得します。この処理によって、構造体の型がわからなくても動的にポインタを取得できます。

ptr := reflect.New(ptrType.Elem())
ptr.Elem().Set(r)

ポインタのインスタンスを作成し、引数の値をセットします。
複雑になりましたが、以上の処理でany型の変数に入っている構造体のポインタを取得できます。

あとがき

addressableについて説明しているサイトが少なくて苦労したので作成しました。一見シンプルな概念ですが、定義が複雑だったり、何に使うのかがわかりずらかったりと、地味にひっかかるポイントが多いと思います。この記事が、addressable理解の一助になれば幸いです。

17
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
17
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?