はじめに
GOアドベントカレンダーの11日目です。
昨日の記事は@tutuzさんのGo言語を使ったTCPクライアントの作り方でした。
初日の記事「なんでGo?」を読んでとても共感を覚えましたが、Goの印象はシンプル、素朴な印象を受ける言語です。
モダンさの追求と、敢えてレガシーな要素を受け継ぐ匙加減の妙、のようなものを感じます。(Googleの看板があるから凄く見える感が無くもない。)
さてそんなGoですが、レガシーな印象を与える特徴として、ポインタが登場するという特徴があります。
なんだかんだ雑な理解をしているので、記事としてアウトプットしつつしっかり勉強しようと思います。
ポインタへの雑な理解
変数にはその値が格納されたアドレス(メモリの番地)があるよ。
変数のアドレスを参照するためにポインタという機能があるよ。
アドレスが参照できることで嬉しい時があるよ。
変数の前に*
や&
をつける事でポインタを使うことができるよ。
よく分からないままでも、ライブラリでポインタを使う時に(json.Unmarshal等のユースケースが想像できますね)、とりあえず*
を変数名の前に付与して、コンパイルエラーが起きたら&
に変えてみる、のような雑な仕事でも最低限生きていくことはできます。
概念レベルでそもそもの理解を深めたい、という内容はwikipediaに任せようと思います。
ポインタとは
謎の*
や&
は構文としては演算子に相当します。
+
,-
,/
みたいなものです。
&
は**「アドレス演算子」(address operator)**と呼ばれます。
変数の前に記述することで、任意の型の変数のポインタ型を生成することができます。
var i int
p := &i
fmt.Printf("%T\n", p) // => *int
*
は**間接演算子 (indirection operator)と呼ばれます。
ポインタ型が保持するアドレス情報を間接演算子を用いて参照し、データ本体にアクセスすることを「間接参照」(デリファレンス)**と呼びます。
C言語の時代から間接演算子は*
で、アドレス演算子は&
です。
var i int
p := &i
i = 10
fmt.Println(p) // => 0xc000100010
fmt.Println(*p) // => 10
イメージとしては、変数を収納している箱の場所をメモして持ち歩くことが&
演算子であり、メモを元に箱を開けに行くことが*
演算子の機能です。
変数に格納していない値そのものに対して&
演算子や*
演算子を使うとコンパイルエラーが発生します。
ポインタ型ではない変数に対して*
を使うとコンパイルエラーが発生します。
package main
import (
"fmt"
)
func main() {
i := 1
fmt.Println(i) // => 1
fmt.Println(&i) // => 0xc000100010
p := &i
fmt.Println(p) // => 0xc000100010
fmt.Println(*p) // => 1
fmt.Println(&p) // => 0xc0000ca018 (俗にいうポインタのポインタなので違うアドレスが表示される)
/*
コンパイルエラーが発生するもの
invalid indirect of 1 (type untyped int)
i := *1
cannot take the address of 1
i := &1
invalid indirect of i (type int)
p = *i
*/
}
ポインタ型とは
変数の値を格納したアドレスを管理するための型です。
C言語にも存在する由緒ある概念です。
int
のポインタ型は*int
, string
のポインタ型は*string
です。
オリジナルの構造体MyType
を作成した場合も、MyType
のポインタ型である*MyType
を使えるようになります。
ポインタのポインタ型である**int
等も型としては存在します。(ほぼ使うことは無いと思いますが)
役に立つかはともかく、int型のポインタのポインタのポインタ型のような無茶ができます。
package main
import (
"fmt"
)
func main() {
var ppp ***int // int型のポインタのポインタのポインタ型
i := 1 // int
p := &i // *int
pp := & p // **int
ppp = &pp // ***int
fmt.Println(ppp) // => 0xc00000e030
fmt.Println(*ppp) // => 0xc00000e028
fmt.Println(pp) // => 0xc00000e028
fmt.Println(*pp) // => 0xc00002c008
fmt.Println(p) // => 0xc00002c008
fmt.Println(*p) // => 1
}
ポインタがあると結局何が嬉しいのか
ポインタの学習障壁の高さは仕組みから利点を想像することが難しい事にあると思います。
ポインタを駆使して具体的に何ができるのかを説明していきます。
値渡しと参照渡しの使い分け
i := 1
i2 := i
p := &i
上記のようなコードではi
, i2
ともに値は1ですが、i2
はi
の値を基に作られた別の変数であり、それぞれの変数が参照しているアドレスは異なります。
つまりi2
の値を書き換えてもi
の値は変化がありません。
一方ポインタ型の変数pの内容を書き換えるとi
の内容も変わります。
この違いを値渡し、参照渡しと呼ぶのはどの言語でも共通です。
ポインタ型を意識する必要のない言語では、参照渡しだと思ってコードを書いたら値渡しだったため、変わると思っていた値が変わらなかった、という事件が起きることがあります。
例えばRubyの場合、メソッドは原則値渡しで、引数に与えた変数が実際に書き換わるメソッド(破壊的変更を行うメソッドと呼ばれます)はメソッド名に!
をつける習慣があります。
配列をソートした配列を新しく作り、ソート前の状態を維持したい場合は
ary1 = [ "d", "a", "e", "c", "b" ]
ary2 = ary1.sort
配列の変数の内容をそのまま書き換えたい場合は
ary1 = [ "d", "a", "e", "c", "b" ]
ary1.sort!
のような書き方になります。
参考: class Array(Ruby 2.7.0 リファレンスマニュアル)
優劣というよりは言語仕様とその言語が産まれた背景にある文化の違いかなと思います。
別の言語の話に逸れましたが、上記の話をコードにすると、このようになります。
package main
import (
"fmt"
)
func main() {
i := 1
i2 := i
p := &i
fmt.Println(i) // => 1
fmt.Println(i2) // => 1
fmt.Println(*p) // => 1
i2 = 99
fmt.Println(i) // => 1
fmt.Println(i2) // => 99
fmt.Println(*p) // => 1
*p = 99
fmt.Println(i) // => 99
fmt.Println(i2) // => 99
fmt.Println(*p) // => 99
}
参照渡しを駆使することで、変数の内容を更新するような処理で記述量を少し減らす事ができます。
json.Unmarshal等が参考になります。
参考: Package json
package main
import (
"encoding/json"
"fmt"
)
func main() {
var jsonBlob = []byte(`[
{"Name": "Platypus", "Order": "Monotremata"},
{"Name": "Quoll", "Order": "Dasyuromorphia"}
]`)
type Animal struct {
Name string
Order string
}
var animals []Animal
err := json.Unmarshal(jsonBlob, &animals)
if err != nil {
fmt.Println("error:", err)
}
fmt.Printf("%+v", animals)
}
nilを明確に区別する
引数を元に値を設定した構造体を返すメソッドを作っていて、引数の内容によっては処理の途中でエラーが発生する可能性があるとします。
関数の戻り値を構造体にする場合、エラーが発生した場合でも何かの値を返す必要があり、苦し紛れに作りかけの構造体か初期値を持った構造体を返すことになります。
(HTTPリクエストのリクエストパラメータを処理するサーバーサイドの処理など、諸事情により潔くpanic
で死ねないものとします。)
お作法としては良くないですし、エラーで作られたゴミデータなのか、実際に意味のあるデータなのか、線引きができなくなってしまいます。
そこで関数の戻り値をポインタ型にすることで、nilを返すことが可能になります。
package main
import (
"fmt"
"strconv"
)
type item struct {
value int
}
func main() {
s := "1"
i, _ := makeItem(s)
fmt.Println(i) // => {1}
p, _ := makeItemPointer(s)
if p != nil {
fmt.Println(*p) // => {1}
}
s2 := "not a number"
i2, _ := makeItem(s2)
fmt.Println(i2) // => {0}
p2, _ := makeItemPointer(s2)
if p2 != nil {
fmt.Println(*p2) // => 実行されない
}
}
func makeItem(s string) (item, error) {
i, err := strconv.Atoi(s)
if err != nil {
// 特に意味のない構造体を生成して返す
return item{}, err
}
return item{value: i}, nil
}
func makeItemPointer(s string) (*item, error) {
i, err := strconv.Atoi(s)
if err != nil {
// nilを返す
return nil, err
}
return &item{value: i}, nil
}
引数の文字列をintにしつつ、他にフィールドのない簡素な構造体に詰める謎の処理を書きました。
このサンプルだとエラーを握りつぶして無駄にnilチェックをする無駄なコードですが...
このような書き方が役立つユースケースとしては、データベースからレコードを取得して構造体にマッピングするような処理で、レコードが一件もない場合も後続処理で何かを生成して返してあげたいような場合が具体例として挙げられます。
なんにせよゼロ値とnilを明確に区別させる際にポインタ型が役に立ちます。
冗長な説明になりましたが実際ややこしいユースケースになる事が多いです。
stringとポインタ
Goには生成した文字列はイミュータブル(変更不能)である、という特徴があります。
普段から意識する必要は案外少ないですが、ポインタとして扱った場合影響が出る場合があります。
package main
import (
"fmt"
)
func main() {
s := "hoge"
p := &s
fmt.Println(s) // => hoge
fmt.Println(*p) // => hoge
fmt.Println(s[0:1]) // => h
// fmt.Println(*p[0:1]) => cannot slice p (type *string)
}
デリファレンスで値を取得したものは一見完全にstring型と同じように扱えそうですが、string型に関しては文字列の一部を参照することができなくなります。
これは生成した文字列が変更不能であるという特徴を守るため敢えて参照不能にしているようです。
package main
import (
"fmt"
)
func main() {
s := "hoge"
p := &s
*p = "fuga"
fmt.Println(s) // => fuga
fmt.Println(*p) // => fuga
}
一方このようなコードは動きました。
これは厳密にはGo言語におけるstring型は「不変(immutable)なバイト型のスライス」という仕様であるという理由のようです。
中々混乱する内容ですのでredditの質問のリンクを紹介して補足とさせていただきます。
あくまでも文字列の部分的な変更がNGであり、文字列全体の参照、変更はOKという事になります。
文字列生成時のメモリ割り当てを効率化させ、パフォーマンスを向上させる意図が背景にあるようです。
パフォーマンスと使い勝手のバランスを攻めるGoらしさを感じるポイントです。
追記: 記述に間違いがありコメントで指摘いただきました、ありがとうございます。
このエラーが出ているのはstringがimmutableなのは関係なく、エラーメッセージのとおり *string 型の値のスライスを取ろうとしているからです。
演算子結合順位の関係で、 *(p[0:1]) と解釈されているわけですね。
従って、カッコをつけて次のようにすれば動きますよ:
fmt.Println((*p)[0:1]) // => h
配列とポインタ
package main
import (
"fmt"
)
func main() {
s := [3]string { "1", "2", "3" }
p := &s
fmt.Println((*p)[1]) // => 2
fmt.Println(p[1]) // => 2
// fmt.Println(*p[1]) => invalid indirect of p[1] (type string)
}
配列のポインタ型であるp
に対してデリファレンスを行う場合は(*p)[1]
のような癖のある書き方になります(C言語由来の仕様らしいです)
Goではp[1]
のように添字を書いても暗黙的にデリファレンスが行われ、配列の中身を取得することができます。
また、配列のポインタ型に対してfor分を使用した場合も暗黙的にデリファレンスが行われます。
package main
import (
"fmt"
)
func main() {
s := [3]string{"1", "2", "3"}
p := &s
for _, v := range p {
fmt.Println(v)
}
}
一方スライスに対しては暗黙的なデリファレンスは行われません。
package main
import (
"fmt"
)
func main() {
s := []string{"1", "2", "3"}
p := &s
fmt.Println((*p)[1]) // => 2
// fmt.Println(p[1]) = > invalid operation: p[1] (type *[]string does not support indexing)
for _, v := range *p {
fmt.Println(v)
}
/*
for _, v := range p {
fmt.Println(v)
}
// => cannot range over p (type *[]string)
*/
}
structとポインタ
structに限った話ではありませんが、ポインタを用いずに引数でデータをやり取りすると値渡しとなるためオブジェクトがコピーされます。
structの場合は変数一つに内包されるデータがそれなりに大きくなる場合があるため、メモリの節約という観点でも参照渡しを積極的に使っていきたいです。
package main
import (
"fmt"
)
type MyObject struct {
Value *string
}
func printAddress(o MyObject) {
fmt.Printf("printAddress: %p\n", &o)
}
func printAddress2(o *MyObject) {
fmt.Printf("printAddress2: %p\n", o)
}
func main() {
o := MyObject{}
fmt.Printf("Address: %p\n", &o)
printAddress(o)
printAddress2(&o)
}
同様の理屈でミスが起こりうる部分ですが、Goは定義したstructにメソッドを実装することができます。
その際ポインタの挙動を把握しておかないと期待した挙動を実現するにあたり苦戦したり、メモリを必要以上に消費する実装になってしまいます。
下記のサンプルコードは生成したstructのデータを初期化しつつ生成するNewMyObject関数を実装したサンプルコードですが、正常に動きません。
package main
import (
"fmt"
)
type MyObject struct {
Value *string
}
func (o MyObject) Set() {
s := "Hello"
o.Value = &s
}
func (o MyObject) Get() {
fmt.Println(*o.Value)
}
func NewMyObject() MyObject {
var ret MyObject
ret.Set()
return ret
}
func main() {
o := NewMyObject()
o.Get()
}
一見Set関数でValueに値が代入されそうですが、main関数で初期化した変数oのValueの値はnilになっており、エラーが発生します。
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4990a3]
goroutine 1 [running]:
main.MyObject.Get(...)
/tmp/sandbox919124516/prog.go:17
main.main()
/tmp/sandbox919124516/prog.go:28 +0x23
些細な違いですが、下記のように修正することで正常に動作します。
package main
import (
"fmt"
)
type MyObject struct {
Value *string
}
func (o *MyObject) Set() {
s := "Hello"
o.Value = &s
}
func (o *MyObject) Get() {
fmt.Println(*o.Value)
}
func NewMyObject() MyObject {
var ret MyObject
ret.Set()
return ret
}
func main() {
o := NewMyObject()
o.Get()
}
これは、structをポインタで渡さないとデータのコピーが生成される値渡しの挙動を取るためです。
レシーバをポインタ型にしなければ、Set()
を実行してもコピーされたstructに値が代入されるだけで、大本のstructのデータに影響を与えることができません。
結局アドレスそのものが表示できる意味ある?
正直自分は恩恵に預かったことがありません。
メモリの節約を常に意識するようなハードウェアを作る場合や、究極的にパフォーマンスチューニングが要求されるような場合を除き、ほぼ無いといっても過言ではないと思います。
言語によっては配列の先頭のオブジェクトのアドレスをやり取りすると、アドレスをインクリメントすれば次のオブジェクトが取れるので、巨大な配列の先頭アドレス情報だけをやり取りして高速化するような裏技があるようです。(多分Goでは不可能だと思うのですが断言すると刺されそうな予感)
あとは並列実行処理で各変数のアドレスをデバッグ出力することである程度プロセスの動きを追えるとかなんとか...
アドレスをプリント出力したり取得したりする事でハックするような事例があればぜひコメントいただければと思います。