新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Go言語でハマったことメモ(値渡し・ポインター渡し)

Golangを始めました。

Golangはポインターを使います。

Golangは歴史的に新しい言語なので、ポインターを意識しないで、何となくコードを書けば良しなに動いてくれるのかなと思ったのですが、甘かったです…。

ポインター自体はよくあるもので、とりあえずポインターにして渡せば大元を操作するので困ることはないのですが、 値で渡した時は、何が渡ってどうなるかを知っていないと困ることがあります。

そしてそのあたり、他の言語での先入観や勝手な思い込みで判断してしまって、ハマることがありました。

ここでは、個人的にハマったことを中心に、備忘録としてメモしておこうと思います。

おさらい

関数に引数を渡し、戻り値を受ける時、何が行われているかをおさらいします。

例えば、「整数を渡すと100倍にする計算式を文字列にして返す」関数を見てみます。

func test(x int) string {
    y := x * 100
    s := fmt.Sprintf("%d x 100 = %d", x, y)
    return s
}

func main(){
    a := 1
    b := test(1)
    fmt.Println(b)  // 「1 x 100 = 100」
}

これを、中で何が行われているかを、関数なしで表現してみます。

func main(){
    a := 1

    // 関数開始
    x := a  // 引数を取り込む

    y := x * 100
    s := fmt.Sprintf("%d x 100 = %d", x)

    b := s  // 戻り値を返す
    // 関数終了

    fmt.Println(b)
}

ポイントは、関数に引数を渡す時と、関数から戻り値を受ける時に「=」代入が行われてるということです。

つまり、「=」代入で何が行われるかを知れば、関数に何が渡り、何が戻ってくるかを知ることができます。

「=」と「==」の挙動

intの場合

intなどの基本型の代入はコピーが行われます。

比較は実体ではなく値を見て行います。

a := 1
b := a
fmt.Println(a)  // 1
fmt.Println(b)  // 1
fmt.Println(a == b)  // true

b = 2
fmt.Println(a)  // 1
fmt.Println(b)  // 2
fmt.Println(a == b)  // false

配列の場合

Golangの配列の代入は、C言語と違い、コピーが行われます。

比較は実体ではなく値を見て行います。

a := [2]int{1, 1}
b := a
fmt.Println(a)  // [1 1]
fmt.Println(b)  // [1 1]
fmt.Println(a == b)  // true

b[0] = 2
fmt.Println(a)  // [1 1]
fmt.Println(b)  // [0 1]
fmt.Println(a == b)  // false

構造体の場合

Golangの構造体の代入は、C言語と同様、コピーが行われます。

比較は実体ではなく値を見て行います。

type myStruct struct {
    x int
}

a := myStruct{x: 1}
b := a
fmt.Println(a)  // {1}
fmt.Println(b)  // {1}
fmt.Println(a == b)  // true

b.x = 2
fmt.Println(a)  // {1}
fmt.Println(b)  // {2}
fmt.Println(a == b)  // false

スライス・マップ・ストリング(string)の場合

ここでは結論だけ言うと

  • スライス・ストリングは構造体と同じ
  • マップはポインターと同じ

で考えます。

しかし、そもそも「スライス・マップ・ストリング」は何なのか?の説明が必要なのですが、長くなるので別記事にまとめました。

www.kwbtblog.com

「=」と「==」の挙動の注意点

まとめると、Golangの代入・比較には下記の特徴があります。

  • 配列の代入はコピーで値もコピーされる
  • 配列の比較は配列の中の値を比較する
  • 構造体の代入はコピーで値もコピーされる
  • 構造体の比較は構造体の中の値を比較する
  • 変数は必ず初期値で初期化される

C言語と比べた時に、ちょっとした違いのように見えますが、この仕様のおかげで、コーディングがめちゃくちゃ楽になります!

大きい配列・構造体はポインターで受け渡しする

前述の通り、配列・構造体の代入はコピーなので、大きい配列・構造体を関数でやり取りする時は、ポインターで渡した方がいいです。

type myStruct struct {
    x [100000]int
}

// 値渡し
func testObject(v [100000]int) myStruct {
    // vを作成時に配列のコピーが行われる
    o := myStruct{
        x: v, // ここの配列のコピーは致し方ない
    }
    return o  // 値を戻す時に、構造体のコピー(要素の配列のコピー)が行われる
}

// ポインター渡し
func testPointer(v *[100000]int) *myStruct {
    o := new(myStruct)
    o.x = (*v)  // ここの配列のコピーは致し方ない
    return o
}

new()

GolangにもC++言語のようなnew()関数があります。

挙動も想像通りで、指定した型のメモリ領域を作成し、ポインターを返します。値は初期化もしてくれます。

a := new(int)
fmt.Println(*a)  // 0
*a = 123
fmt.Println(*a)  // 123

しかし、Golangには、「ローカル変数をnew()代わりに使える」という、超便利な仕様があって、そちらを使うことも多いかと思います。

例えば、前述のtestPointer()は、Golangでは下記のように書けます。

func testPointer(v *[100000]int) *myStruct {
    o := myStruct{
        x: *v,
    }
    return &o  // ローカル変数のアドレスを渡しても問題ない
}

感想など

代入を押さえておけば、関数で何がやり取りされるかが分かるので、関数に値を渡した方がいいか、ポインターを渡した方がいいかの検討がしやすくなります。

下記記事にも書きましたが、Golangでは、マップ・ストリングをポインターにすることはほとんどありません。

www.kwbtblog.com

また、配列を使いたい時は代わりにスライスを使い、配列そのものを直接扱う場面はあまりありません。

となると、値渡しかポインター渡しかを考える箇所って、主に構造体またはスライスをやり取りする所になりますね。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com