いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-22 (配列, Arrays, Tour of Go)

概要

Tour of Go の - Arrays についてのサンプル。

tour.golang.org

Goの配列は [要素数]型名 という形で宣言します。

var (
    arr [2]int
)

上だと、要素数が2のintの配列を定義しています。

特殊なのが、Goでは、配列の長さ(要素数)は「型の一部」であるということです。

つまり、[2]int[3]intは全く別の型になります。

最初は不便って思えるんですが、Goにはスライスという概念があるので実際にはほぼ気になりません。

その他は別に使い方として気になる点は無いのですが、別の言語経験者からすると引っかかる癖みたいなのがあります。

Goの配列のクセ

それが、Goの配列は値であるということ。

つまり、Goの配列は関数に引数としてそのまま渡した場合、配列は値なのでそれ自身がコピーされて渡ります。

なので、関数内でその配列に対して値操作を行ったとしても、呼び元の配列には影響しません。

CやC#やJava経験者からすると、この点が最初ちょっとミスしてしまうかもしれませんね。

これらの言語で、メソッドに配列を渡すと参照(Cの場合はポインタ)が値コピーされて渡っているので

メソッド内で値を操作しても呼び元の値は変化してくれています。Goで同じようにすると呼び元の配列の値は変わりません。

例でいうと

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void p(int *ints, int size, char *message) {
    for (int i = 0; i < size; i++) {
        printf("[%s][%d] = %d\n", message, i, ints[i]);
    }
}

void update_array(int *ints) {
    ints[0] = 999;
    ints[1] = 998;
}

int main() {
    const int arr_size = 2;
    int arr[arr_size];

    memset(arr, 0x00, sizeof(int) * arr_size);

    p(arr, arr_size, "before");
    update_array(arr);
    p(arr, arr_size, "after ");

    return 0;
}

これは実行すると以下と出ます。

[before][0] = 0
[before][1] = 0
[after ][0] = 999
[after ][1] = 998

同様にC#で

void Main()
{
    var arr = new int[2];
    arr.Dump();
    
    UpdateArray(arr);
    arr.Dump();
}

// Define other methods and classes here
void UpdateArray(int[] ints)
{
    ints[0] = 999;
    ints[1] = 998;
}

実行すると

0 
0 

999 
998 

となります。

同じようにGoで

package main

import (
    "fmt"
)

func main() {
    var (
        arr [2]int
    )

    fmt.Printf("[updateArray前] %v\n", arr)
    updateArray(arr)
    fmt.Printf("[updateArray後] %v\n", arr)
}

func updateArray(ints [2]int) {
    ints[0] = 999
    ints[1] = 998

    fmt.Printf("[updateArray中] %v\n", ints)
}

を実行すると

[updateArray前] [0 0]
[updateArray中] [999 998]
[updateArray後] [0 0]

という風に値が変わっていません。

同じようにするには、配列をポインタで渡す必要があります。

func updateArray2(ints *[2]int) {
    // 本来、ポインタでパラメータが渡されているので
    // 値にアクセスするには、まずデリファレンスが必要となる
    // (*ints)[0] = 999
    // しかし、Goのランタイム側がこれを吸収してくれるので
    // 普通にデリファレンス無しで配列操作のように書くことができる
    ints[0] = 999
    ints[1] = 998

    fmt.Printf("[updateArray2中] %v\n", ints)
}

こうすると、ちゃんと呼び元も変わります。

他の言語に慣れていると、よく引っかかりますのでご注意を。

サンプル

package tutorial

import "fmt"

// Array は、 Tour of Go - Arrays (https://tour.golang.org/moretypes/6) の サンプルです。
func Array() error {
    // ------------------------------------------------------------
    // Go言語の配列
    // Go言語の配列は、 [要素数]型名 という形で宣言する.
    // 特殊なのが、配列の長さ(要素数)は「型の一部」であるということ。
    // なので、 例えば 同じ int の配列であっても
    //   - [2]int
    //   - [3]int
    // は異なる型となる。これは不便だと最初思えるが、Go言語にはスライスという
    // 概念があるので、実際には気にならない.
    //
    // スライスは「可変長」で、配列は「固定長」である.
    //
    // C言語経験者だと、引っかかってしまう点が一つあり
    // Goの配列は、値であるので関数に引数として配列を渡した場合
    // 関数内で引数の配列の要素に対して値更新をしても、呼び出し元の
    // 配列には影響しないという点がある。同じようにするには配列のポインタを
    // 渡すようにする。
    //
    // C#などの経験者も、配列を関数の引数で渡す場合、頭では参照が渡っていると
    // 認識してしまうので注意が必要。同じような動きをさせる場合はポインタで渡すこと。
    // ------------------------------------------------------------
    var (
        arr [2]int
    )

    // 初期値は設定していない場合、その要素型のゼロ値となる
    fmt.Println(arr)

    // 配列の長さは 組み込み関数 len で取れる
    fmt.Println(len(arr))

    // Cの様に配列をループする場合は以下のようにする
    for i := 0; i < len(arr); i++ {
        fmt.Printf("%v\t", arr[i])
    }

    fmt.Println("")

    // foreach ループする場合は以下のようにする
    for _, v := range arr {
        fmt.Printf("%v\t", v)
    }

    fmt.Println("")

    // 値の設定も他の言語と同じ
    arr[0] = 100
    arr[1] = 200
    for _, v := range arr {
        fmt.Printf("%v\t", v)
    }

    fmt.Println("")

    // Goの配列は値なので、そのまま渡すと配列自体がコピーされて渡る。
    // そのため、関数内で配列の値を編集しても呼び元には影響しない。
    fmt.Printf("[updateArray前] %v\n", arr)
    updateArray(arr)
    fmt.Printf("[updateArray後] %v\n", arr)

    fmt.Println("")

    // ポインタで渡すと望んだ動きとなる
    fmt.Printf("[updateArray2前] %v\n", arr)
    updateArray2(&arr)
    fmt.Printf("[updateArray2後] %v\n", arr)

    return nil
}

func updateArray2(ints *[2]int) {
    // 本来、ポインタでパラメータが渡されているので
    // 値にアクセスするには、まずデリファレンスが必要となる
    // (*ints)[0] = 999
    // しかし、Goのランタイム側がこれを吸収してくれるので
    // 普通にデリファレンス無しで配列操作のように書くことができる
    ints[0] = 999
    ints[1] = 998

    fmt.Printf("[updateArray2中] %v\n", ints)
}

func updateArray(ints [2]int) {
    ints[0] = 999
    ints[1] = 998

    fmt.Printf("[updateArray中] %v\n", ints)
}

try-golang/tutorial_gotour_15_array.go at master · devlights/try-golang · GitHub

実行すると以下な感じ。

[Name] "tutorial_gotour_array"
[0 0]
2
0   0   
0   0   
100 200 
[updateArray前] [100 200]
[updateArray中] [999 998]
[updateArray後] [100 200]

[updateArray2前] [100 200]
[updateArray2中] &[999 998]
[updateArray2後] [999 998]

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com