3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

一年目Rubyエンジニアが一から学ぶGo言語 ポインタ編

Posted at

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
  • 値の先頭に参照演算子&を付けることで、その値のメモリアドレスを取得可能
  • ポインタは関数内で引数やレシーバの値を更新する時に使用する、パフォーマンスが考慮材料となることも

次回はインターフェースについて整理できたらいいなと思ってます。

3
0
1

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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?