Webエンジニアとして実務でプロダクト開発に携わること約1年弱、バックエンドの技術としてはRuby(とRails)一本でやってきた私ですが、エンジニアとしてやっていくからには静的型付け言語の習得も必要だろうということでGoの学習を始めました。
そんな中でRubyには無かった機能や概念のうち、個人的に理解に時間がかかったものを備忘録も兼ねて整理していきたいと思います。
とりあえず第一弾ということで、今回はポインタについてです。
ポインタ事始め
最初に躓くと定番?のポインタです。Rubyでもポインタの概念が無いわけではないようですが、少なくとも私は今まで意識したことがありませんでした。
そもそもメモリやアドレスが何か?については、様々な記事でイラスト付きで分かりやすく図解されているため、ここでは割愛します。
私は下記の記事で理解を深めました。
var num int = 10
var ptr *int = &num
fmt.Println(num) // 10
fmt.Println(ptr) // 0xc00001c0e8
fmt.Println(*ptr) // 10
num = 15
fmt.Println(*ptr) // 15
*ptr = 20
fmt.Println(*ptr) // 20
fmt.Println(ptr) // 0xc00001c0e8
上の例では、ptr
がポインタ変数を表し、int型変数のnum
が格納されているメモリの場所(アドレス)を表しています。
ptr
の中身を見ると0x00001c0e8
のように、16進数でメモリアドレスが格納されていますね。
ポインタ変数は型名の前に*
を置くことで宣言できます。*int
*string
など。
変数名の前にアドレス演算子&
を置くと、それはその変数のメモリアドレスを表しています。
メモリアドレスが指し示す中身(今回で言うとnum
)を見たい場合、*ptr
のように、ポインタ変数の前に間接参照演算子*
をつけることで参照可能です。
num
を更新するとポインタ変数ptr
が指し示す先の値、つまり*ptr
の値も変わります。同様に*ptr
の値を更新するとnum
も変わりますが、メモリアドレス、つまりptr
は変化しません。
なぜポインタ変数の宣言に型を付ける必要があるか?
var num int = 5
var ptrNum *int = &num
var str string = "a"
var ptrStr *string = &str
fmt.Println(ptrNum) // 0xc0000b0008
fmt.Println(ptrStr) // 0xc000096210
ポインタ変数を宣言する際は、*int
*string
のように*
+ポインタ変数が指し示す先の変数の型で宣言しますが、上の例でも分かるように、*int
だろうが*string
だろうが同じように16進数のメモリアドレスが格納されます。
そうすると*
だけでなく型も宣言する必要性があるのか?と思いますが、これは型を指定しないと間接参照で変数の値を読み取る際、何バイトまで辿れば良いか分からないためです。
var num int = 5
var str string = "a"
var flg bool = true
fmt.Println(unsafe.Sizeof(num)) // 8
fmt.Println(unsafe.Sizeof(str)) // 16
fmt.Println(unsafe.Sizeof(flg)) // 1
unsafe.Sizeof
メソッドで各変数のメモリ上のサイズ(バイト数)見てみると、それぞれ大きさが異なることが分かります。
ポインタ変数が表すメモリアドレスは、あくまで指し示す変数が格納されているアドレスの先頭のみを表しています。そのため型を指定しないと先頭から何バイト取得すれば良いか?が分からない、ということですね。
ポインタはどんな場面で必要になるのか?
ポインタの概念は理解できましたが、これって具体的にどんな場面で使うんでしょうか?
ポインタが無いと困る場面を以下で示してみたいと思います。
関数内で引数やレシーバの値を更新する時
func setPoliteName(name string) {
name = "Mr." + name
}
func main() {
name := "Jotaro"
setPoliteName(name)
fmt.Println(name) // Jotaro
}
上記の例はsetPoliteName
という関数を定義しています。こちらは引数name
の頭にMr.を付与するだけの単純な関数ですが、main関数でこちらの関数をコール後name
を出力すると、値が変わっていないことが分かります。
これは、関数の引数として値を渡した場合、関数が実行される際に値がコピーされ、呼び出し元とは別の変数として扱われる、つまり参照先のアドレスが変わるためです。
これを避けるためにはどうすれば良いか?そう、値ではなくポインタを渡してあげれば良いのです。
func setPoliteName(name *string) {
*name = "Mr." + *name // ポインタ変数を渡しているので、間接参照演算子を付けるのを忘れずに
}
func main() {
name := "Jotaro"
setPoliteName(&name)
fmt.Println(name) // Mr.Jotaro
}
ポインタ変数を渡すことでポインタが指し示す先の値を更新することができたので、呼び出し元のname
を更新できました。
小見出しで「引数やレシーバ」と書きましたが、レシーバについては構造体のフィールド値を更新する場合が当てはまります。
type User struct {
name string
age int
}
func (u *User) addAge() { // レシーバをポインタ型で指定
(*u).age += 1
}
func main() {
user := User{ "Jotaro", 18 }
user.addAge()
fmt.Println(user) // {Jotaro 19}
}
余談ですが、上記でaddAge
のレシーバをポインタ型にしたのにかかわらず、呼び出し元ではuser.addAge
のままです。これで問題無く動くのは何故でしょうか?
これはGoの言語仕様で、レシーバがポイント型の場合は自動でポインタとして解釈してくれるためです。もちろん、律儀に(&user).addAge
で呼び出した場合も問題無く動作します。
Goはこういうところで厳密性を求めるイメージがあったのですが、意外と融通が効くヤツなんですね。
パフォーマンスを考慮する場合
基本的には先述のパターンでポインタを使うことが多いと思いますが、パフォーマンス面を考慮して、値ではなくポインタをメソッドに受け渡す、という場合があるようです。
詳細は開発元から提供されているCodeReview Commentsという記事で解説されており、日本語訳もあります。
以下の記事でも解説されていますが、まだあまり理解できていません…。
まとめ
- ポインタはその変数の(先頭の)メモリアドレスを表す
- ポインタは型名に
*
を付けることで定義可能 例:*int
- 値の先頭に参照演算子
&
を付けることで、その値のメモリアドレスを取得可能 - ポインタは関数内で引数やレシーバの値を更新する時に使用する、パフォーマンスが考慮材料となることも
次回はインターフェースについて整理できたらいいなと思ってます。