Go言語 ポインタ 基礎で「うーん…」と悩んでいませんか? プログラミング学習の中でも、ポインタはちょっとした壁に感じやすい部分かもしれませんね。まるでRPGのちょっと手強い中ボスみたいな存在でしょうか?
でも大丈夫!この記事では、Go言語のポインタが「そもそも何なのか?」という基本のキから、実際のコードでの「書き方」や「使い方」、そして「どんな時に役立つのか?」まで、ステップバイステップで解説していきます。
この記事で学べることは、ざっくり以下の通りです。
- ポインタって何?が分かる
- アドレスとメモリのイメージが掴める
- 基本的なポインタの書き方・使い方が身につく
- ポインタを使うと何が嬉しいか分かる
- ポインタの注意点が分かる
読み終わるころには、「なるほど、ポインタってそういうことか!」とスッキリするはず。さあ、一緒にポインタの壁を乗り越えて、Go言語マスターへの道を歩み始めましょう!
Go言語のポインタとは?メモリとアドレスの基礎知識
まず、ポインタって一体何者なんでしょう?
ものすごく簡単に言うと、ポインタは「他の変数がメモリのどこにあるか」という場所情報(住所)を覚えておくための特別な変数なんです。
ここで、「メモリ」と「アドレス」という言葉が出てきましたね。これも難しく考えないでください。
- メモリ
データを一時的にしまっておく、たくさんのロッカーが並んだ場所みたいなものです。変数を作ると、このロッカーの一つが割り当てられます。 - アドレス
そのロッカー一つ一つに付けられた、固有の番号(住所)のことです。
普通の変数は、ロッカーの中に「データそのもの」(例えば数字の100とか、文字列の"hello"とか)を入れます。
一方、ポインタ変数は、ロッカーの中に「他のロッカーの番号(アドレス)」を入れる、というわけです。住所録みたいなイメージですね。
なぜこんな仕組みがあるかというと、大きなデータを扱うときに、データそのものをあちこちコピーするより、そのデータの「住所」だけを教えた方が、ずっと効率が良い場面があるからなんですよ。
メモリとアドレスの関係を理解しよう
言葉だけだとイメージしにくいので、簡単な図で見てみましょう。
例えば、`num := 100` というコードで変数を作ったとします。これは、メモリ(ロッカー)の中に `100` という値を入れる、という意味でしたね。
メモリの中はこんな感じになっていると想像してみてください。
メモリ(たくさんのロッカー) +-------------+ | ... | ← 他のデータやプログラムが入ってる +-------------+ | ロッカーA番地 | ← ここに変数numが割り当てられた! | (値: 100) | +-------------+ | ... | +-------------+
この「ロッカーA番地」というのが、変数 `num` のアドレスです。コンピュータが内部で管理している、場所を示す情報ですね。
ポインタは、この「ロッカーA番地」のようなアドレス情報を格納するための変数、というわけです。
すべての変数(データ)は、メモリ上のどこかに場所(アドレス)を持っている、と覚えておきましょう。
【Go言語のポインタ】変数の宣言方法
では、Go言語でポインタ変数をどうやって作る(宣言する)のか見ていきましょう。書き方は意外とシンプルです。
基本形は、型名の前にアスタリスク `*` を付けるだけ。
var p *int // int型の値へのポインタを格納する変数pを宣言
上の例だと、`*int` で「int型の値が置いてある『場所(アドレス)』を入れるための変数ですよ」と宣言しています。変数 `p` には、int型のデータそのものではなく、int型データが置いてあるメモリアドレスが入る、ということですね。
もし `string` 型へのポインタなら `*string`、自分で定義した構造体 `MyStruct` 型へのポインタなら `*MyStruct` のように書きます。簡単でしょう?
【Go言語のポインタ】アドレス演算子 `&` の使い方
ポインタ変数を作ったら、次はその変数に「どの変数のアドレスを入れるか」を指定する必要があります。そのために使うのがアドレス演算子 `&` です。
変数名の前に `&` を付けると、その変数が格納されているメモリアドレスを取得できます。
var num int = 100 var p *int // int型へのポインタ変数pを宣言 p = &num // 変数numのアドレスをポインタ変数pに代入 (&はHTMLエンティティで記述)
上の例では、まず普通のint型変数 `num` を作り、100を入れました。
次に、int型へのポインタ変数 `p` を宣言。
そして、`p = &num` で、「変数 `num` が置いてあるメモリの住所(アドレス)を調べて、その住所情報を変数 `p` に入れてね」と指示しています。
`&` は「〜のアドレス」と読むと分かりやすいかもしれませんね。 `&num` で「numのアドレス」です。
【Go言語のポインタ】間接参照演算子 `*` の使い方 (デリファレンス)
さて、ポインタ変数にアドレスを入れました。でも、私たちが本当に知りたいのは、そのアドレスが指し示している「先」にあるデータの中身ですよね?
ポインタ変数が持っているアドレス情報を元に、そのアドレスに実際に格納されている値にアクセスする方法。それが間接参照演算子 `*` です。ポインタの「指す先」を参照するので、「デリファレンス」とも呼ばれます。
ポインタ変数の前に `*` を付けると、そのポインタが指しているメモリアドレスに格納されている値を取得したり、書き換えたりできます。
fmt.Println(*p) // ポインタpが指す先の値(numの値である100)を表示 *p = 200 // ポインタpが指す先の値(numの値)を200に書き換える fmt.Println(num) // numの値が200に変わっていることを確認
ここで注意!さっきポインタ変数を宣言するときも `*` を使いましたね (`var p *int`)。文脈によって意味が変わるんです。
- 宣言の時 (`*int`): 「これはint型へのポインタ型ですよ」という意味。
- 使う時 (`*p`): 「ポインタ `p` が指している先の値」という意味(デリファレンス)。
慣れるまで少し混乱するかもしれませんが、「宣言の時の `*`」と「使う時の `*`」は役割が違う、と覚えておきましょう。
【Go言語のポインタ】具体的な使い方(サンプルコード)
ポインタを使った値の参照と変更
package main import "fmt" func main() { // 1. 普通の変数を宣言して初期化 var num int = 100 fmt.Println("元のnumの値:", num) // 出力: 元のnumの値: 100 fmt.Println("numのアドレス:", &num) // 出力: numのアドレス: 0xc0000xxxxx (アドレスは実行ごとに変わります) // 2. ポインタ変数を宣言 var p *int // 3. 変数numのアドレスをポインタ変数pに代入 p = &num fmt.Println("ポインタpが持つアドレス:", p) // 出力: ポインタpが持つアドレス: 0xc0000xxxxx (numのアドレスと同じ) // 4. ポインタpを使って、指し示す先の値(numの値)を参照 fmt.Println("ポインタpが指す先の値:", *p) // 出力: ポインタpが指す先の値: 100 // 5. ポインタpを使って、指し示す先の値(numの値)を変更 fmt.Println("ポインタpを使ってnumの値を200に変更します...") *p = 200 // 6. 元の変数numの値が変わっているか確認 fmt.Println("変更後のnumの値:", num) // 出力: 変更後のnumの値: 200 fmt.Println("再度、ポインタpが指す先の値を確認:", *p) // 出力: 再度、ポインタpが指す先の値を確認: 200 }
実行結果の例 (アドレスは環境により異なります):
元のnumの値: 100 numのアドレス: 0xc000018030 ポインタpが持つアドレス: 0xc000018030 ポインタpが指す先の値: 100 ポインタpを使ってnumの値を200に変更します... 変更後のnumの値: 200 再度、ポインタpが指す先の値を確認: 200
このコードで、ポインタ `*p` を使って変数 `num` の値を直接書き換えられることが確認できましたね!これがポインタの面白いところの一つです。
関数でポインタを使ってみよう (値の書き換え)
ポインタが特に活躍する場面の一つが、関数との組み合わせです。
「関数の中で、関数の外にある変数の値を直接書き換えたい!」と思ったことはありませんか? ポインタを使えば、それが可能になります。
ここでは、よく例に出される「2つの変数の値を入れ替える関数」を作ってみましょう。
もし普通に関数の引数として変数を渡すだけ(値渡し)だと、関数の中で値を入れ替えても、元の変数の値は変わりません。なぜなら、関数が操作しているのは元の値のコピーだからです。
そこで、変数の「住所(アドレス)」をポインタとして関数に渡してみます。住所が分かれば、関数の中からその住所にある元の変数に直接アクセスして、値を書き換えられますね!
package main import "fmt" // 2つのint型変数の値を入れ替える関数 // 引数として、int型へのポインタ (*int) を2つ受け取る func swap(ptrX *int, ptrY *int) { fmt.Println("swap関数の中: 入れ替え前の値 - *ptrX:", *ptrX, ", *ptrY:", *ptrY) // ポインタが指す先の値を入れ替える temp := *ptrX // ptrXが指す先の値(元の変数aの値)を一時変数tempに保存 *ptrX = *ptrY // ptrXが指す先の値(元の変数aの値)を、ptrYが指す先の値(元の変数bの値)で上書き *ptrY = temp // ptrYが指す先の値(元の変数bの値)を、一時変数tempの値で上書き fmt.Println("swap関数の中: 入れ替え後の値 - *ptrX:", *ptrX, ", *ptrY:", *ptrY) } func main() { a := 10 b := 20 fmt.Println("swap関数を呼ぶ前: a:", a, ", b:", b) // swap関数に変数のアドレス (&a, &b) を渡す // これで、swap関数は変数aとbの場所を知ることができる swap(&a, &b) // & はHTMLエンティティで記述 fmt.Println("swap関数を呼んだ後: a:", a, ", b:", b) }
実行結果:
swap関数を呼ぶ前: a: 10 , b: 20 swap関数の中: 入れ替え前の値 - *ptrX: 10 , *ptrY: 20 swap関数の中: 入れ替え後の値 - *ptrX: 20 , *ptrY: 10 swap関数を呼んだ後: a: 20 , b: 10
見てください!`swap`関数を呼び出した後、main関数の中の変数 `a` と `b` の値がちゃんと入れ替わっていますね。
これは、`swap`関数に変数 `a` と `b` のアドレスを渡した (`swap(&a, &b)`) ことで、`swap`関数内の `ptrX` と `ptrY` がそれぞれ `a` と `b` の場所を指すようになり、`*ptrX` や `*ptrY` を使って `a` と `b` の値を直接操作できたからです。
このように、関数にポインタを渡すことで、呼び出し元の変数を直接変更できるというのは、ポインタの非常に便利な使い方の一つです。覚えておくと役立ちますよ!
なぜGo言語でポインタを使うのか?メリットを理解しよう
「アドレスとかデリファレンスとか、なんだか面倒だな…」と感じたかもしれません。では、なぜわざわざポインタなんて使うのでしょう?それにはちゃんとした理由、メリットがあるんです。
【メリット1】関数の引数としての効率性 (値渡しとの比較)
関数を呼び出すとき、通常は引数として渡した値がコピーされて関数の中で使われます(これを「値渡し」といいます)。
小さなデータなら問題ないのですが、例えば巨大なデータ構造(たくさんの情報が詰まったもの)を関数に渡す場合、そのコピーを作るだけでも時間やメモリを使ってしまいます。
ここでポインタの出番です。データそのものではなく、そのデータが置いてある場所(アドレス)をポインタとして関数に渡せば、コピーされるのはわずかなアドレス情報だけ。大きなデータのコピーを作る必要がありません。
データのコピーを作らずに済むので、処理が速くなったり、メモリを節約できたりする。これがポインタを関数の引数に使う大きなメリットの一つです。
【メリット2】関数による呼び出し元変数の変更
通常、関数の中で引数の値を変更しても、関数の外にある元の変数には影響しません。なぜなら、関数の中で扱っているのは、あくまで元の値のコピーだからです。
でも、「関数の中で、外にある変数の値を書き換えたい!」という場面はよくあります。例えば、二つの変数の値を入れ替える関数を作りたいときなどです。
こんなときにポインタが活躍します。変数のアドレスをポインタとして関数に渡せば、関数はそのアドレスを使って、元の変数に直接アクセスできます。つまり、関数の中から、関数の外にある変数の値を直接変更できるのです。
さっきのサンプルコードで `*p = 200` と書いたら元の `num` が変わったのが、まさにこの仕組みですね。
Go言語ポインタを使う上での注意点
便利なポインタですが、いくつか注意しておきたい点もあります。安全運転を心がけましょう!
一番気をつけたいのは、「どこも指していないポインタ」(`nil` ポインタ)をうっかり使ってしまうことです。また、意図しない場所の値を書き換えてしまう可能性もゼロではありません。
でも、これから説明するポイントを押さえておけば、むやみに怖がる必要はありませんよ。
`nil` ポインタに注意しよう
ポインタ変数を宣言しただけ(まだどこかのアドレスを代入していない状態)のとき、その中身は `nil` になっています。`nil` は「何もない」「どこも指していない」という特別な状態を表します。
この `nil` の状態のポインタ変数に対して、中身を見ようとしたり (`*p`)、書き換えようとしたり (`*p = ...`) すると、プログラムは「指す先がないのにアクセスしようとした!」と判断して、エラーで停止してしまいます(これを「panic」といいます)。
これを防ぐためには、ポインタを使う前に、そのポインタが `nil` でないかをチェックするのが基本です。
var p *int // この時点では p は nil // このまま *p を使うと panic する! // 使う前に nil チェックをする if p != nil { fmt.Println(*p) // nil でなければ安全に使える } else { fmt.Println("ポインタはnilです") } // アドレスを代入すれば nil ではなくなる var num int = 10 p = &num if p != nil { fmt.Println(*p) // 今度は 10 が表示される }
ポインタを使う前には、必ず `nil` でないかチェックする習慣をつけること。これがポインタを安全に使うための、とても大事なルールです。
【まとめ】Go言語ポインタの基礎をマスターしよう!
お疲れ様でした!今回はGo言語のポインタの基礎について、できるだけ分かりやすく解説してみました。
この記事で学んだ要点をまとめておきましょう。
- ポインタは「メモリアドレス(場所の情報)」を格納する変数。
- `&` で変数のアドレスを取得する。
- `*`(アスタリスク)は、宣言時はポインタ型を示し、使う時(デリファレンス)はポインタが指す先の値にアクセスする。
- ポインタを使うと、大きなデータの効率的な受け渡しや、関数外の変数の変更ができる。
- `nil` ポインタへのアクセスはエラー(panic)の原因になるので、使う前に `nil` チェックを忘れずに!
ポインタは最初は少しとっつきにくいかもしれませんが、一度理解してしまえば、Go言語でのプログラミングの幅がぐっと広がります。メモリの動きを意識できるようになると、より効率的なコードを書く助けにもなりますよ。
この記事が、あなたのGo言語学習の一助となれば嬉しいです。自信を持って、どんどんコードを書いて、Go言語をもっと楽しんでくださいね!
【関連記事】 Go言語とは?特徴・できること・将来性
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。