LoginSignup
25
19

More than 5 years have passed since last update.

Go でのポインターの扱い

Posted at

今日は 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

うん。しっかりまるで継承しているかのようだ。これで大体ポインターの部分はつかめたきがする。他にも押さえておくべき文法がありましたら、ぜひコメントお願いいたします。

リソース

今回はたくさん参照しましたので、リストを書いておきます。

25
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
19