この記事は CA Tech Lounge Advent Calendar 2025 の3日目の投稿となっております。
今後投稿される他の方の記事は上記のリンクからご覧ください!
CA Tech Lounge についてはこちらからご覧ください!
はじめに
どうも、りょうさんです。埼玉の田舎の方で情報系大学生をしている学部4年生です。
CA Tech Lounge ではバックエンドエンジニアとして所属しています。
私は Ruby が好きなのですが、最近のバックエンドの主流は Golang になってきているようです。自分もその波に乗り遅れないように学習したことをまとめていこうと思います。
導入
私は初めてのプログラミングはC言語からでした。プログラミング初心者がC言語を始めるとまず詰まるのが「ポインタ」の概念や使い方についてだと思います。
この記事では、明示的なポインタとしての代表例であるC言語とGo言語のポインタについて触れつつ、最終的には Go のポインタではC言語と違って何ができる・できないを記していこうと思います。
ポインタとはなんぞや
ポインタは、ご存知の方も多いとは思いますが「何かの位置を指し示すための仕組みや道具」です。
プログラムにおいて、変数や配列、構造体などは、メモリ上の特定の位置に値が格納されますが、"その位置を指し示す変数(メモリ空間上のアドレス値など)を格納した"変数を「ポインタ」、または「ポインタ変数」と呼びます。
C/C++ で実装されたものが特に有名です。C言語でポインタについて扱うには以下のようにします。
#include <stdio.h>
int main(void) {
int x = 10;
int *p = &x; // p は x のアドレスを指す
printf("x = %d\n", x); // 値を表示(x = 10)
printf("p = %p\n", p); // アドレスを表示(p = 0x16ce83008)
printf("*p = %d\n", *p); // ポインタ経由で値を表示(*p = 10)
return 0;
}
C言語で書かれていますが、同様に Go で書くと以下の通りです。
func main() {
x := 10 // xは10という値を持つ
p := &x // pは「xのアドレス」を持つ(&はアドレス演算子)
fmt.Println(x) // 値を表示
fmt.Println(p) // メモリのアドレスを表示
fmt.Println(*p) // ポインタ経由で値を表示
}
ここからは Go では何ができないのかをまとめていきます。
ポインタ演算ができない
C言語では、
#include <stdio.h>
int main(void) {
int arr[] = {10, 20, 30, 40};
int *p = arr; // arr は &arr[0] と同義
printf("%d\n", *p); // 10
p++; // 次の要素を指す
printf("%d\n", *p); // 20
int *q = p + 2; // 現在 p が指す要素からさらに2つ先へ
printf("%d\n", *q); // 40
printf("Distance: %td\n", q - arr); // 3 (要素数での差)
return 0;
}
しかし、Go ではこのようなポインタ演算は一切できません。
Go のポインタは「アドレスを保持する」という点では C と同じですが、アドレス値そのものをインクリメントしたり、任意の算術を行うことは禁止されています。
実際に Go で同じように書こうとすると、次のようなコードはコンパイルエラーになります。
func main() {
arr := []int{10, 20, 30, 40}
p := &arr[0]
fmt.Println(*p) // 10
p++ // ❌ invalid operation: p++ (non-numeric type *int)
}
このように、Go では 「ポインタを数値扱いしない」という哲学が貫かれています。
Go の設計者は、メモリ管理の安全性を高めるために、アドレスの直接計算やメモリ上の移動といった低レベル操作を意図的に排除しています。
その結果、
-
「配列名はポインタに暗黙変換される」という C のような挙動はない
-
p + 1のように「次の要素に移動する」操作も不可 -
q - pのような「ポインタ同士の差分取得」も不可
といった制限があります。
これは最初は不便に見えますが、実は Go の配列やスライスには別の強力な機能があるため、多くの場合ポインタ演算の必要がないというのがポイントです。
Go では「スライス」がポインタ演算の代わりを担う
では Go ではどうするかというと、C のポインタ演算で行っていた「次の要素を見に行く」というような操作は、ポインタではなく、スライスを使って実現します。
func main() {
arr := []int{10, 20, 30, 40}
fmt.Println(arr[0]) // 10
next := arr[1:]
fmt.Println(next[0]) // 20
fmt.Println(arr[3]) // 40
}
スライスには「配列の先頭ポインタ」、「長さ」、「キャパシティ」というメタ情報がすべて入っているため、ポインタ演算が不要になります。スライス=安全に包まれた配列ポインタと考えると理解しやすいです。
安全性とパフォーマンスのトレードオフ
C言語のポインタ演算は強力ですが、自由すぎるがゆえに危険でもあります。
- 1つ間違うと隣接メモリを破壊する
- 配列の境界外アクセスがそのまま実行される
- 言語が何も守ってくれない
Go はこれを避けるため、「アドレスを持つことは許可するが、アドレスを直接いじることは許さない」という方針を採っています。
そのため Go のポインタは、文字通り「参照したい場所を指すだけ」の存在となり、C 言語のようにアドレス値を使って自由に計算する道具ではなくなっています。
まとめ
ここまで紹介してきたように、C と Go はどちらもポインタ自体は持っていますが、使える範囲と目的がまったく異なります。
- C言語
- ポインタ=メモリを直接触るための強力な道具
- 演算できる、移動できる、何でもできる(代わりに危険)
- Go
- ポインタ=「値をコピーせず渡す」ためのシンプルな仕組み
- 演算できない、安全のために用途を絞った設計
Go の世界では、ポインタ演算のような低レイヤの操作はできませんが、その代わりに スライス・マップ・構造体・GC などが充実しており、「ポインタを危険に使う必要がない」ように言語全体が設計されています。
このあたりを理解しておくと、「C ではこうできたのに、Go ではできない理由」「Go ではどの場面でポインタを使うのか」が直感的につかめるようになります。