Go言語を学んでいると、途中で * や & が出てきます。
ポインタが使われたGoのコード
本記事では、Google Colab上でGoのコードを動かしながら、
- 値渡し
- ポインタ渡し
-
&と* - nilポインタ
- structとポインタ
- rangeでやりがちなミス
を、解説していきます。
Google Colabを使いながら、よくある失敗例も多めに紹介するので、参考にしてください。
対象読者
この記事は、次のような人を対象にしています。
- Google Colabを使ったことがある人
- Goの基本文法を少し触ったことがある人
-
func main()、関数、struct、sliceがなんとなく読める人 -
*や&が出てくると急に読めなくなる人 - Goのポインタで値が思った通りに変わらない理由を知りたい人
本格的なメモリ管理の話ではなく、まずはGoのコードを読むためのポインタ入門です。
学習を進めるために
なお、「Google ColabでGoのプログラムを動かすなんてできるの?」で引っかかってる読者は、まず下記記事で予習してください。
また、先に、Go言語の基本文法が知りたいという人は、下記記事を見てください。
すぐに使えるチートシートはこちら Google Colab版
すぐに実行して、試せるコードレシピはこちら。
ポインタとは何か
ポインタは「値のメモリアドレスを保持するもの」であり、Goでは、次の2つの記号が重要です。
& : 変数のアドレスを取得する
* : ポインタの指す先の値を読む、または変更する
まずは小さな例を見ます。
%%writefile pointer_basic.go
package main
import "fmt"
func main() {
x := 10
p := &x
fmt.Println("xの値:", x)
fmt.Println("xのアドレス:", &x)
fmt.Println("pの値:", p)
fmt.Println("pが指す先の値:", *p)
}
xの値: 10
xのアドレス: 0xc0000120c0
pの値: 0xc0000120c0
pが指す先の値: 10
&x は、x のアドレスです。
p := &x
pはxの場所を覚えている
という意味で、*pと書くと、p が指している先の値を取り出せます。
ここでは、p は x を指しているため、*p は 10 になります。
ポインタ経由で値を変更する
ポインタを使うと、指している先の値を変更できます。
%%writefile pointer_update.go
package main
import "fmt"
func main() {
x := 10
p := &x
*p = 20
fmt.Println("x:", x)
fmt.Println("*p:", *p)
}
実行結果は、こうなります。
x: 20
*p: 20
*p = 20 と書くことで、p が指している先の値を変更しています。
p は x を指しているため、結果として x の値も20になります。
つまり、
*p = 20は、pが指している元データを書き換えるという意味です。
失敗例1: 関数に渡したのに値が変わらない
Goでは、関数に値を渡すと、基本的に値はコピーされます。
まず、値が変わらない例を見ます。
%%writefile value_copy.go
package main
import "fmt"
func addOne(n int) {
n = n + 1
fmt.Println("関数の中:", n)
}
func main() {
x := 10
addOne(x)
fmt.Println("mainの中:", x)
}
実行結果です。
関数の中: 11
mainの中: 10
関数の中では 11 になっています。
しかし、main の中の x は 10 のままです。
これは、addOne(x) で渡された x がコピーされているからです。
mainのx = 10
↓
addOneにコピーを渡す
↓
関数の中のnだけが11になる
↓
mainのxは10のまま
ここでやりがちな誤解は、
関数の中でnを変更したから、元のxも変わるはず
というものです。
しかし、普通に値を渡しただけでは、元の変数は変わりません。
正解: ポインタを渡す
元の値を変更したい場合は、ポインタを渡します。
%%writefile pointer_func.go
package main
import "fmt"
func addOne(n *int) {
*n = *n + 1
}
func main() {
x := 10
addOne(&x)
fmt.Println("mainの中:", x)
}
mainの中: 11
ここで重要なのは、関数の引数です。
func addOne(n *int)
*int は、「int型の値を指すポインタ」という意味です。
呼び出す側では、&x と書いています。
addOne(&x)
これは、x のアドレスを渡しています。
関数の中では、*n でポインタの指す先を変更します。
*n = *n + 1
つまり、
xそのものを渡すのではなく、xの場所を渡す
その場所にある値を書き換える
という流れです。
失敗例2: structを変更したつもりなのに変わらない
次に、structでよくある失敗です。
%%writefile struct_value_copy.go
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u User) {
u.Age = u.Age + 1
fmt.Println("関数の中:", u)
}
func main() {
user := User{Name: "Alice", Age: 30}
birthday(user)
fmt.Println("mainの中:", user)
}
関数の中: {Alice 31}
mainの中: {Alice 30}
関数の中では Age が31になっています。
しかし、main の中では30のままです。
これも、structがコピーされているからです。
userを関数に渡す
↓
Userのコピーが作られる
↓
関数の中ではコピーのAgeを変更する
↓
元のuserは変わらない
正解: structのポインタを渡す
元のstructを変更したい場合は、structのポインタを渡します。
%%writefile struct_pointer.go
package main
import "fmt"
type User struct {
Name string
Age int
}
func birthday(u *User) {
u.Age = u.Age + 1
}
func main() {
user := User{Name: "Alice", Age: 30}
birthday(&user)
fmt.Println("mainの中:", user)
}
実行結果です。
mainの中: {Alice 31}
ここでは、関数の引数が *User になっています。
func birthday(u *User)
呼び出し側では、&user と書いています。
birthday(&user)
つまり、user の場所を渡しています。
なお、structのポインタを使うとき、本来は次のように書けます。
(*u).Age = (*u).Age + 1
ただし、この書き方は面倒です。
Goでは、structのポインタに対しても、次のように書けます。
u.Age = u.Age + 1
Go公式Tourでも、structポインタのフィールドには p.X のようにアクセスできると説明されています。
失敗例3: nilポインタを読んでpanicする
ポインタのゼロ値は nil です。
nil は、どこも指していない状態です。
%%writefile nil_pointer.go
package main
import "fmt"
func main() {
var p *int
fmt.Println(p)
fmt.Println(*p)
}
これはpanicとなります。
実行結果の例です。
panic: runtime error: invalid memory address or nil pointer dereference
p はどこも指していません。
その状態で、*p
と書くと、「どこも指していないのに、その先の値を読もうとする」ことになり、panicとなります。
nilの可能性がある場合は、先に確認します。
%%writefile nil_check.go
package main
import "fmt"
func main() {
var p *int
if p == nil {
fmt.Println("pはnilです")
return
}
fmt.Println(*p)
}
実行結果です。
pはnilです
失敗例4: sliceの要素を変更したつもりなのに変わらない
次に、sliceとstructの組み合わせでよくある失敗です。
%%writefile range_value_copy.go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
users := []User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Carol", Age: 40},
}
for _, user := range users {
user.Age = user.Age + 1
}
fmt.Println(users)
}
[{Alice 30} {Bob 25} {Carol 40}]
年齢を1つずつ増やしたつもりなのに、変わっていません。
原因はここです。
for _, user := range users {
user.Age = user.Age + 1
}
range で取り出した user は、sliceの要素そのものではなく、要素のコピーです。
そのため、user.Age を変更しても、元の users の中身は変わりません。
users[0] のコピーを user に入れる
user.Age を変更する
でも users[0] は変わらない
正解: indexで元の要素を変更する
sliceの元の要素を変更したい場合は、indexでアクセスします。
%%writefile range_index_update.go
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
users := []User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Carol", Age: 40},
}
for i := range users {
users[i].Age = users[i].Age + 1
}
fmt.Println(users)
}
実行結果です。
[{Alice 31} {Bob 26} {Carol 41}]
今度はコピーではなく、sliceの中の要素を直接変更できました。
ここまでの整理
ポインタで混乱したときは、まず次のように考えるとよいです。
普通の変数:
値そのものを持っている
ポインタ:
値の場所を持っている
&x:
xの場所を取る
*p:
pが指している場所の値を読む、または変更する
そして、よくある失敗は次のように整理できます。
関数に値を渡しただけ
→ コピーなので元の値は変わらない
structを関数に渡しただけ
→ コピーなので元のstructは変わらない
rangeで取り出した値を変更する
→ コピーなので元のsliceは変わらない
range変数のアドレスを保存する
→ 元のslice要素を指しているとは限らない
nilポインタを読む
→ panicする
ポインタを使うべき場面
入門段階では、ポインタは次のような場面で使うと理解しやすいです。
- 関数の中で元の値を変更したい
- structが大きいのでコピーを避けたい
- メソッドで元のstructを変更したい
- sync.WaitGroupのように、共有して使う状態を渡したい
逆に、何でもポインタにすればよいわけではありません。
ポインタを使うと、複数の場所から同じ値を変更できるようになります。しかし、どこで値が変わったのかわかりにくくなることもあります。
そのため、まずは、
* 元の値を変更する必要があるか
* コピーでよいか
を考えるとよいと思います。
まとめ
本記事では、Google Colab上でGoのポインタを学びました。
Google ColabやJupyter Notebookで学ぶと、こうした失敗例を小さなセルごとに試せます。
完成した正解コードを暗記するよりも、
* 変更したつもりなのに変わらない
* 全部同じ値になってしまう
* panicになる
といった失敗を実際に見てみるのがおすすめです。また、小さなコードで「作って・壊して」をやってみるのも効果的です。
弊社について
本記事を書いている 合同会社インクルーシブソリューションズ は、データ基盤構築・分析基盤設計・システム改善支援を中心に活動している小規模IT法人です。
主な領域は、
- データマート設計・データパイプライン構築
- SQL / Python を用いたデータ処理設計
- BI導入支援・分析基盤の整備
- 既存システムの運用改善・可視化支援
といった、「データを使える状態にする」ための活動です。
弊社の企業活動に興味がある方は、ぜひ公式サイトも覗いてみてください。