
ポインタ…全然、理解できません…

私もポインタでGo言語を一度挫折しました…でも、今は大丈夫ですよ!
今回はGo言語のポインタ入門です。
私自身、Go言語学習での最初の挫折がポインタでした。
でも、Go言語で使用されるポインタは、この記事でお伝えする内容を理解しておけば応用が効きますので、しっかりとマスターしていきましょう。
Go言語のポインタとは?
Go言語でのポインタを専門用語で説明すると難しくなるので例をあげて説明してみます。
例えば、変数宣言した変数を格納する場所があります。
その場所はメモリと呼ばれるところに、住所(メモリアドレス)が割り振られたところに格納されます。
ポインタは、その住所(メモリアドレス)と型の情報を持っているものです。

わかりずらいですね…プログラムと図解で説明していきましょう。
メモリの中身を確認してみよう!
メモリの中身を確認していきます。
ちょっとくどい内容になるかもしれませんが、より具体的にわかるようにお伝えしていきます。
変数宣言した時のメモリの中身
では、変数宣言した時のメモリの中身はどうなっているのでしょうか?
プログラムで確認しつつ説明していきます。
// 変数宣言する
package main
import "fmt"
func main() {
var a int = 1000
fmt.Println(a)
}
// 出力結果
$ go run main.go
1000

まー当たり前の出力結果ですが、これがメモリ上ではどうなっているのでしょう。

上記の図を元に説明すると var a int = 1000
で宣言した 変数a
は、メモリアドレス 0x0004
に用意されて値の 1000
が収まっている状態です。
メモリアドレスを出力してみる
fmt.Println(a)
で変数aの値が出力できます。

できるんです!
変数名の前に &
を付けることで、メモリアドレスが出力できます。
プログラムで確認していきましょう。
// メモリアドレスを出力する
package main
import "fmt"
func main() {
var a int = 1000
fmt.Println(a)
// 変数aのメモリアドレスを確認
fmt.Println(&a)
}
// 出力結果
$ go run main.go
1000
0xc0000b6010 // メモリアドレス
メモリアドレスが出力されました。
メモリアドレスは 16進数 で表示されます。
このメモリアドレスを、変数の値同様にメモリへ格納することができます。

それが、ポインタ型なのです!
ポインタ型を理解しよう!
いよいよポインタ型の説明に入ります。
ゆっくりと説明していきますのでご安心を。
ポインタ型を宣言する
ポインタ型を宣言してみましょう。
ポインタ型の宣言方法は…
- 型の前には * を付ける
- アドレスを渡したい変数名の前に & を付ける
それでは、プログラムで確認してみましょう。
// ポインタ型を宣言する
package main
import "fmt"
func main() {
var a int = 1000
fmt.Println(a)
// 変数aのメモリアドレスを確認
fmt.Println(&a)
// 変数aのアドレスが格納される
var p *int = &a
// ポインタpを確認する
fmt.Println(p)
}
// 出力結果
$ go run main.go
1000
0xc0000b6010 // メモリアドレス
0xc0000b6010 // ポインタpの値
ポインタp
に 変数a
のメモリアドレスが格納されていることが確認できます。(メモリアドレスが一緒)
この時のメモリの状態を図で説明してみます。

このような感じです。
ポインタを使用して変数の値を確認する
このポインタを使用して、変数aの値を確認することができます。
ポインタから変数の値を取得するには、ポインタ名の前に * を付けます。
では、プログラムで確認しましょう。
// ポインタを使用して変数aの値を確認
package main
import "fmt"
func main() {
var a int = 1000
fmt.Println(a)
// 変数aのメモリアドレスを確認
fmt.Println(&a)
// 変数aのアドレスが格納される
var p *int = &a
// ポインタpを確認する
fmt.Println(p)
// ポインタpのアドレスの中身を確認する
fmt.Println(*p)
}
// 出力結果
$ go run main.go
1000
0xc0000b6010 // メモリアドレス
0xc0000b6010 // ポインタpの値
1000 // 変数aの値

この状態を図解しますね!

ポインタを宣言する時に型の前に * を付け、ポインタが持っているアドレスの値を参照するにも * を付けたりと混乱しがちですが、ポインタを使用しているプログラムに触れていくことで慣れてくるでしょう。
ポインタ宣言する時に初期化する変数が無い場合
今までのプログラムは、ポインタを宣言する時に初期化する変数がありました。
答えは new()
関数を使って初期化してメモリの領域を確保する必要があります。
プログラムで確認してみましょう。
// ポインタ宣言する時に初期化する変数が無い場合
package main
import "fmt"
func main() {
// new()関数でポインタを初期化する
var pn *int = new(int)
fmt.Printf("pmのメモリアドレスは、%pです。\n", pn)
fmt.Println(*pn)
// *pnをインクリメントする
*pn++
fmt.Println(*pn)
}
// 出力結果
$ go run main.go
pnのメモリアドレスは、0xc000014118です。
0
1
ポインタpn
のメモリも確保され、int型の初期値の 0
が *pn
で確認できます。
*pn
にインクリメントすると 1
足されていいることも確認できます。
では、new()
関数使用しない場合はどうなるのでしょう。
// ポインタ宣言する時に初期化する変数が無い場合
package main
import "fmt"
func main() {
// new()関数を使用しない場合
var pn2 *int
fmt.Printf("pn2のメモリアドレスは、%pです。\n", pn2)
fmt.Println(pn2)
}
// 出力結果
$ go run main.go
pn2のメモリアドレスは、0x0です。
<nil>
new()
関数を使用しないと、メモリアドレスが確保されずに <nil> になります。
メモリアドレスが確保されていない状態で演算(今回はインクリメント)すると…
// ポインタ宣言する時に初期化する変数が無い場合
package main
import "fmt"
func main() {
// new()関数を使用しない場合
var pn2 *int
fmt.Printf("pn2のメモリアドレスは、%pです。\n", pn2)
fmt.Println(pn2)
// *pn2をインクリメントする
*pn2++
fmt.Println(*pn2)
}
// 出力結果
$ go run main.go
pn2のメモリアドレスは、0x0です。
<nil>
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4996de]
「無効なメモリアドレスまたはポインタがnil」というpanicエラーになりますので注意が必要です。
ポインタの理解を深めるために必要な値渡しと参照渡し
ここまでポインタ型について説明してきましたが、実際にGo言語でポインタが使用される場面としては関数の引数でしょう。
ここで理解しておかなければいけないのが、値渡しと参照渡しです。
ここは、実際にプログラムで確認していきましょう。
値渡しの参考プログラム
値渡しの説明に入る前に、値渡しの参考プログラムを確認していきます。
// 値渡し参考プログラム
package main
import "fmt"
// 値渡しの関数
func test(x int) {
x = 0
}
func main() {
var b int = 100
// 関数実行前の値を出力
fmt.Println(b)
// 関数を実行
test(b)
// 関数実行後の変数の値
fmt.Println(b)
}
このプログラムは、変数b
の値を初期値 0
にしたい時に実行するためのプログラムを、とりあえず書いてみました。
関数を実行すると値はどうなるでしょうか?
// 出力結果
$ go run main.go
100 // 関数実行前
100 // 関数実行後

あれ?!関数実行しても、変数bの値は変わらないじゃないですか!

そう、これが値渡しなのです。
値渡しを簡単に説明します
コピーして引数を渡すので、元の変数の値は変化しないのです。
先程のプログラムの例だと、変数b
の値 100
をコピーして渡しています。

コピーされた値なんで、足し算しようが割り算しようが何をしようが元の変数の値には影響がないんですね。
それでは、最初の目的である「関数を実行して変数の値を初期値0にする」プログラムの要件をみたしていません。
要件を満たすために必要なのが、値渡しではなく参照渡しなのです。
参照渡しを簡単に説明します
参照渡しのプログラムを確認する前に、参照渡しの説明を簡単にいたします。

メモリアドレスを渡す?
メモリアドレスの値といえば、ここでポインタの出番です。
ポインタを利用した参照渡しの参考プログラム
変数b
の値を初期値 0
にしたい時に実行するためのプログラムをポインタを使用して確認してみます。
// ポインタを利用した参照渡しのプログラム2
package main
import "fmt"
// 参照渡しの関数
// 引数に*intを指定(アドレスを受け取る)
func pTest(x *int) {
// *xは引数として受け取ったアドレスの中身
*x = 0
}
func main() {
var b int = 100
// ポインタを宣言
var p2 *int = &b
// 関数実行前の値を出力
fmt.Println(b)
// ptest()は引数がポインタ型なのでアドレスを渡す
pTest(p2)
// 変数aの値が変更される
fmt.Println(b)
}
// 出力結果
$ go run main.go
100 // 関数実行前
0 // 関数実行後

おっ!さっきと違い変数bの値が変わっていますね!
ここでは関数の引数にポインタ p2
を指定して、メモリアドレスの値を直接操作して値を変更いたしました。
ここではポインタを宣言してから関数を実行しました。
ほかにポインタを宣言しないで関数実行時に引数として & を付けた変数で実行するプログラムもあわせて紹介いたします。
// ポインタを利用した参照渡しのプログラム
package main
import "fmt"
// 参照渡しの関数
// 引数に*intを指定(アドレスを受け取る)
func pTest(x *int) {
// *xは引数として受け取ったアドレスの中身
*x = 0
}
func main() {
var b int = 100
// 関数実行前の値を出力
fmt.Println(b)
// ptest()は引数がポインタ型なのでアドレスを渡す
pTest(&b)
// 変数aの値が変更される
fmt.Println(b)
}
// 出力結果
$ go run main.go
100 // 関数実行前
0 // 関数実行後
先程の出力結果と同じです。

ここまでの流れが理解できれば、ポインタの基本はバッチリです!
【参考】スライス・mapは最初から参照渡しの機能を持っている
スライスやmapは最初から参照渡しの機能を持っていますので、ポインタの宣言をする必要なく値を直接書き換えることができます。
プログラムで確認してみましょう。
プログラムは、スライスの値を10倍するプログラムです。
// ポインタを利用した参照渡しのプログラム2
package main
import "fmt"
// スライスの参照渡し機能を確認
// * を付けていない
func slTest(s []int) {
for i, v := range s {
s[i] = v * 10
}
}
func main() {
// スライス
var sl []int = []int{10,20,30}
// 関数実行前の値を出力
fmt.Println(sl)
// 関数を実行
slTest(sl)
// 関数実行後の値を出力
fmt.Println(sl)
}
// 出力結果
$ go run main.go
[10 20 30]
[100 200 300]
スライスは、関数の引数にアドレスを指定しなくても値が変更されているのが確認できます。

スライスとmapは参照渡しの機能を持ってます。覚えておきましょう。
【まとめ】Go言語のポインタの基本を理解する
今回はプログラミングの挫折の原因にもなりかねないポインタの基本をお伝えしてきました。
まずは、まとめに入ります。
以上です。

覚えること多すぎですね…
でも、ポインタに関しては時間をかけて理解していくか、ポインタを使用しているプログラムを見て実行して確認することで理解が深まります。
私は、ポインタが本当に理解できずにC言語を学習してからGo言語の学習を再開しました。
もし、今回の記事内容が理解できれば、とりあえずC言語を学習しなくても大丈夫です。
C言語のポインタは、ポインタだけで一冊の専門書があるくらい深い内容ですので、興味があればC言語の学習もしてみるといいですよ。
まずは今回の参考プログラムをいろいろ変えてみて、実行結果などを確認してみてください。
きっとポインタの基本については大丈夫なはずです。
ポインタを理解するおすすめ動画
私はポインタを理解できずにC言語の基礎を学習しましたが、その時に視聴したYoutube動画がとてもわかりやすかったので共有いたします。
動画は全2本でポインタの基礎を理解できる内容です。
C言語での説明ですが、概念はGo言語と共通していますのでおすすめです。
comment