今日は Go 言語でのポインターの扱いについて整理してみた。久しくポインターを扱う言語に触れていなかったので、ふわっと理解しているつもりだったが、色々理解できていなかったので、まとめてみる。
基本的なポインタ
Go での基本的なポインタの扱いは、C とそんなに変わらない。唯一違うのは、ポインタ演算ができないところだ。(プログラムを破壊する可能性があるからだろうか)
package main
import "fmt"
func main() {
str := "ushio"
p := &str
fmt.Println(str, " // str")
fmt.Println(&str, " // &str")
fmt.Println(p, " // p")
fmt.Println(*p, " // *p")
}
はっきり言って文法も C と変わらない。実行結果はきっとあなたの予想通り
ushio // str
0xc42006c1a0 // &str
0xc42006c1a0 // p
ushio // *p
理解したいターゲット
実はこれを書いているのも、オープンソースへの貢献のための一歩で、次のコードをちゃんと理解したいと思ったから。
// DataFactory is the DataFactory Response
type DataFactory struct {
autorest.Response `json:"-"`
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
Location *string `json:"location,omitempty"`
Tags *map[string]*string `json:"tags,omitempty"`
*DataFactoryProperties `json:"properties,omitempty"`
}
// DataFactoryProperties is the Property of DataFactory
type DataFactoryProperties struct {
DataFactoryID *string `json:"datafactoryId,omitempty"`
ProvisioningState ProvisioningState `json:"provisioningState,omitempty"`
Error *string `json:"error,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
}
何となくでよければ正直動くものはかけるが、結局それは遅さを招くので、理解しておきたい。
構造体の参照渡しと、値渡し
関数やメソッドに対してのパラメータは、参照渡しという方法と、値渡しという方法がある。GO で普通に書くと「値渡し」になる。つまり、変数の値をコピーして渡す(値渡し)と、参照渡し、つまりポインタを渡す方法だ。コピーする内容が多ければ、値渡しだと時間がかかってしまう。参照渡しだとパラメータが大きな構造体出会っても、アドレスだけなので、一瞬で終わる。
さて、構造体をまるでクラスのように書くテクニックがよく使われるが、大抵は上記のターゲットもそうであるように、ポインタ渡しになっている。それには理由がある。次の例を見てみよう。
こんな構造体を作る。
// Person is a person with name and age
type Person struct {
Name string // Person's first name
Age int // Person's age
}
そして、これに対応するメソッドを用意する。一つは値渡し、一つは、ポインタになっている。内容は、Ageの内容をインクリメントすることだ。
// Increment increments person's age
func (p Person) Increment() {
p.Age++
}
// IncrementWithReference increment person's age
func (p *Person) IncrementWithReference() {
p.Age++
}
これらのメソッドを次のように使ってみると、実行結果が異なる。
func main() {
ushio := Person{
Name: "tsuyoshi",
Age: 46,
}
ushio.Increment()
fmt.Println("Value: ", ushio.Age, " // 47と思いきや違う")
ushio.IncrementWithReference()
fmt.Println("Pointer: ", ushio.Age, " // 実際に47になった")
実行結果
Value: 46 // 47と思いきや違う
Pointer: 47 // 実際に47になった
値渡しの方は、Age をインクリメントしているにもかかわらず、構造体の値が変化していない。これらの関数は次のように展開される
func Increment(p Person) {
p.Age++
}
fun IncrementWithReference(p *Person) {
p.Age++
}
つまり第一引数として、構造体の引数が渡っているのと同じである。だから、値渡しにしてしまうと、コピーが渡されるので、更新がうまくいかない。だから、値渡しにしていいケースは、Immutable なケースなどが考えられるが、それ以外には、基本的にはポインタ渡しの方がイメージにあう。
ポインタと値渡しパラメータに対するシンタックスシュガー
Go はポインタの操作ができない。また、しょっちゅうポインタの値を操作することになる。面倒なので、シンタックスシュガーで、本来ポインターであっても、p.Age++
のように、値渡しのようなイメージで書くことが可能になっている。(だから、たまに、どっちだかわからなくなっていた。なるほど。)
先ほどのゴールに対してまだ理解できていないポイントがある。それは、構造体の フィールド がポインタになっていることである。何故わざわざこうなっているのだろう?
構造体の フィールド が ポインタである理由
ゴールの構造体と比べて違うところは何かというと、構造体を初期化した時の、初期化の挙動による。構造体を初期化すると、値を与えないと、デフォルト値が採用される。
// Person is a person with name and age
type Person struct {
Name string // Person's first name
Age int // Person's age
Status *string // Person's status
}
先ほどの構造体に、ポインタのフィードを足してみる。そして、次のようなコードを書いて、初期化のデフォルト値の違いを見てみる。
aPerson := new(Person) // No parameter
fmt.Println("Name :", aPerson.Name)
fmt.Println("Age :", aPerson.Age)
fmt.Println("Status :", aPerson.Status)
結果は
Name :
Age : 0
Status : <nil>
初期化したら、型ごとのデフォルト値で埋められる。ちなみに、元々のゴールの構造体の使い道は、REST API から帰ってきた値をを保存するためのものである。JSON から変換されてくる。この構造体の必須チェックを行うときに、値渡しだと、から文字が、初期値によるものなのか、から文字が渡されたのかがわからなくなってしまう。ちなみに、参照渡しにすると、値の参照が多少面倒になる。例えば、*string
のフィールドがあるときに、&"tsuyoshi"
などと書いても、参照渡しはできない。たぶんこんなメソッドを書くなり、一旦変数に代入して、アドレスをもらうなりしないといけない。面倒だ。
func toAddress(str string) *string {
return &str
}
ゴールの構造体で参照渡しになっているのは、JSONからパースするので、nil か から文字かを明確に区別したかったのだと思う。そして、構造体のフィールドの更新はない(REST-API から受け取った値を格納する責務なので)だから、今回はポインタを選択しているのだと思う。
ネストした構造体
では最後のテクニック
// Person is a person with name and age
type Person struct {
Name string // Person's first name
Age int // Person's age
Status *string // Person's status
}
を使って
// Employee is an employee
type Employee struct {
Person
ID int // Employee's employee_id
}
こんな感じで書くと、継承っぽいイメージで使えるようになる。具体例を見てみよう。初期化時はPersonを意識しているが、使うところは、まるで継承したようなイメージになっている。
emp := Employee{
Person: Person{Name: "tsuyoshi",
Age: 46,
Status: toAddress("Happy"),
},
ID: 2014,
}
fmt.Println("Name: ", emp.Name)
fmt.Println("Age: ", emp.Age)
fmt.Println("Status: ", *emp.Status, "(", emp.Status, ")")
fmt.Println("Id: ", emp.ID)
実行結果
Name: tsuyoshi
Age: 46
Status: Happy ( 0xc42000e350 )
Id: 2014
うん。しっかりまるで継承しているかのようだ。これで大体ポインターの部分はつかめたきがする。他にも押さえておくべき文法がありましたら、ぜひコメントお願いいたします。
リソース
今回はたくさん参照しましたので、リストを書いておきます。