簡単なプログラムでGo言語のポインタの基本を理解しよう!

Go言語
この記事は約14分で読めます。
Go初学者
Go初学者

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

もりぴ
もりぴ

私もポインタでGo言語を一度挫折しました…でも、今は大丈夫ですよ!

この記事ではポインタの基本について理解できます!
  • 変数宣言した時のメモリの状態を理解できます
  • 値渡しと参照渡しについて(超重要ポイント!)
  • ポインタをプログラムで確認する

今回はGo言語のポインタ入門です。

私自身、Go言語学習での最初の挫折がポインタでした。

でも、Go言語で使用されるポインタは、この記事でお伝えする内容を理解しておけば応用が効きますので、しっかりとマスターしていきましょう。

もりぴ
この記事を書いた人

XHTML1.0時代にHTML&CSSを勉強した経験あり。無趣味だった私が2020年5月からプログラミング学習を開始し現在も挫折せずに趣味で学習を楽しんでいる51歳。プログラミングの楽しさをブログを通してお伝えしていきます。

もりぴをフォローする

Go言語のポインタとは?

Go言語でのポインタを専門用語で説明すると難しくなるので例をあげて説明してみます。

例えば、変数宣言した変数を格納する場所があります。

その場所はメモリと呼ばれるところに、住所(メモリアドレス)が割り振られたところに格納されます。

ポインタは、その住所(メモリアドレス)と型の情報を持っているものです。

もりぴ
もりぴ

わかりずらいですね…プログラムと図解で説明していきましょう。

メモリの中身を確認してみよう!

メモリの中身を確認していきます。

ちょっとくどい内容になるかもしれませんが、より具体的にわかるようにお伝えしていきます。

変数宣言した時のメモリの中身

では、変数宣言した時のメモリの中身はどうなっているのでしょうか?

プログラムで確認しつつ説明していきます。

// 変数宣言する
package main

import "fmt"

func main()  {
    var a int = 1000
    fmt.Println(a)
}
// 出力結果
$ go run main.go 
1000
もりぴ
もりぴ

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

int型の変数aがメモリに格納された状態

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

メモリアドレスを出力してみる

fmt.Println(a) で変数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進数 で表示されます。

このメモリアドレスを、変数の値同様にメモリへ格納することができます。

もりぴ
もりぴ

それが、ポインタ型なのです!

ポインタ型を理解しよう!

いよいよポインタ型の説明に入ります。

ゆっくりと説明していきますのでご安心を。

ポインタ型を宣言する

ポインタ型を宣言してみましょう。

ポインタ型の宣言方法は…

var ポインタ名 *型 = &変数名

POINT
  1. 型の前には * を付ける
  2. アドレスを渡したい変数名の前に & を付ける

それでは、プログラムで確認してみましょう。

// ポインタ型を宣言する
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 のメモリアドレスが格納されていることが確認できます。(メモリアドレスが一緒)

この時のメモリの状態を図で説明してみます。

ポインタ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() 関数を使って初期化してメモリの領域を確保する必要があります。

var ポインタ名 *型 = 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  // 関数実行後
Go初学者
Go初学者

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

もりぴ
もりぴ

そう、これが値渡しなのです。

値渡しを簡単に説明します

値渡しは、関数に引数を渡す時に値をコピーして渡します。

コピーして引数を渡すので、元の変数の値は変化しないのです。

先程のプログラムの例だと、変数b の値 100 をコピーして渡しています。

もりぴ
もりぴ

コピーされた値なんで、足し算しようが割り算しようが何をしようが元の変数の値には影響がないんですね。

それでは、最初の目的である「関数を実行して変数の値を初期値0にする」プログラムの要件をみたしていません。

要件を満たすために必要なのが、値渡しではなく参照渡しなのです。

参照渡しを簡単に説明します

参照渡しのプログラムを確認する前に、参照渡しの説明を簡単にいたします。

参照渡しは、関数に引数を渡す時にメモリアドレスを渡します。

Go初学者
Go初学者

メモリアドレスを渡す?

メモリアドレスの値といえば、ここでポインタの出番です。

ポインタを利用した参照渡しの参考プログラム

変数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  // 関数実行後
Go初学者
Go初学者

おっ!さっきと違い変数bの値が変わっていますね!

ここでのポイント!
  • 関数の引数にアドレスを受け取る (x *int)
  • アドレス先の値を変更するために *x = 0 にして、実体の値を指定する
  • ポインタ型を宣言して変数bのアドレスを代入する var p2 *int = &b
  • 関数を実行する引数にポインタを指定する pTest(p2)

ここでは関数の引数にポインタ 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言語のポインタの基本を理解する

今回はプログラミングの挫折の原因にもなりかねないポインタの基本をお伝えしてきました。

まずは、まとめに入ります。

Go言語でのポインタの基本として押さえておくこと!
  • ポインタはメモリアドレスと型の情報を持っているもの
  • メモリアドレスの確認には変数名などの前に & を付ける
  • ポインタ型を宣言は var ポインタ名 *型 = &変数名
  • ポインタから変数の値を取得するには変数名などの前に * を付ける
  • 値渡しは、関数に引数を渡す時に値をコピーして渡す
  • 参照渡しは、関数に引数を渡す時にメモリアドレスを渡す
  • スライスとmapは最初から、参照渡しの機能を持っている

以上です。

もりぴ
もりぴ

覚えること多すぎですね…

でも、ポインタに関しては時間をかけて理解していくか、ポインタを使用しているプログラムを見て実行して確認することで理解が深まります。

私は、ポインタが本当に理解できずにC言語を学習してからGo言語の学習を再開しました。

もし、今回の記事内容が理解できれば、とりあえずC言語を学習しなくても大丈夫です。

C言語のポインタは、ポインタだけで一冊の専門書があるくらい深い内容ですので、興味があればC言語の学習もしてみるといいですよ。

まずは今回の参考プログラムをいろいろ変えてみて、実行結果などを確認してみてください。

きっとポインタの基本については大丈夫なはずです。

ポインタを理解するおすすめ動画

私はポインタを理解できずにC言語の基礎を学習しましたが、その時に視聴したYoutube動画がとてもわかりやすかったので共有いたします。

YouTube
作成した動画を友だち、家族、世界中の人たちと共有
YouTube
作成した動画を友だち、家族、世界中の人たちと共有

動画は全2本でポインタの基礎を理解できる内容です。

C言語での説明ですが、概念はGo言語と共通していますのでおすすめです。

comment

タイトルとURLをコピーしました