📑

GoのSliceについて

2021/06/28に公開

はじめに

配列はプログラミングでは重要な要素です。Go言語でもそれは同様ですが、他の言語から移ってきたときには少し注意する必要もあります。そんな事例を紹介したいと思います。

Sliceとは

Go言語の Slice とは、その他の言語の配列とは少し違います。

A Tour of Goには以下の説明が記載されています。

An array has a fixed size. A slice, on the other hand, is a dynamically-sized, flexible view into the elements of an array. In practice, slices are much more common than arrays.
( Go言語では配列のサイズは固定されています。一方、スライスは、配列の要素に対する動的なサイズの柔軟なビューです。Go言語ではスライスは配列よりもはるかに一般的です。)

とあります。
「なるほど、データ長が固定しているのが配列を利用して、可変長データを取り扱う場合はSliceを利用すればよいのか」

https://golang.org/src/runtime/slice.go

ループから抜け出すことができない?

かなり初期の段階で出会いそうなエピソードです。

最初は、10回ループを繰り返してループを抜けるコードです。

func main() {
    num := 0
    for {
        if num == 10 {
            fmt.Println("ループを抜けます!")
            break
        }
        num++
    }
    fmt.Println("終了")
}

// [output]
// ループを抜けます!
// 終了

ふむふむって感じですよね。

下の場合どうなりますか?

func main() {
    num := 0
    for {
        switch num {
            case 10:
            fmt.Println("ループを抜けます!")
            break
            default:
            fmt.Println("何もしない")
        }
        num++
    }
    fmt.Println("終了")
}

実行しないでください。無限ループになります!😱

swtich caseの場合ブロックは通常breakは抜けれますが、外側のループは抜けられません。
ドキュメントでも言及されてます。Break statements

A "break" statement terminates execution of the innermost "for", "switch", or "select" statement within the same function.

「break」ステートメントは、同じ関数内の最も内側の「for」、「switch」、または「select」ステートメントの実行を終了します。

解決方法

ラベル付きbreak(Labeled break)を利用することで回避することができます。

If there is a label, it must be that of an enclosing "for", "switch", or "select" statement, and that is the one whose execution terminates.

ドキュメントからの抜粋です。ネストされたforループからbreakするには、ループにラベル(Outer)をつけておき、breakにラベルを指定します。

Outer:
    for i = 0; i < n; i++ {
        for j = 0; j < m; j++ {
            switch a[i][j] {
            case nil:
                state = Error
                break Outer
            case item:
                state = Found
                break Outer
            }
        }
    }
}

大元のループにラベル名を設定しておき、ネストしているループから脱出する際には、ラベル付きbreakでループ全体を抜けます。

スライスのデータが更新されない?

このケースもよくありがちかもしれません。

例) スライスの各要素を2倍にする。


slc := []int{1,2,3,4,5,6,7,8,9,}

for _, ele := range slc {
    // あえて冗長な記述をしてみます
    ele = ele * 2
}
// どうなるかというと
fmt.Println(slc)

// こうなります
// [output]
// [1 2 3 4 5 6 7 8 9]

何もかわりませんでした。

解決方法

これはスライスが range loop は元スライスのコピーで要素はそのローカル変数となるため、要素を更新しても元スライスには何も影響がされません。

// for i :=0; i<len(slc); i++ { .... } としたほうがいいですが、あえて
for index := range slc {
    // rangeで取り出したとしても元スライスを指定することで値の更新はできます。
    slc[index] *= 2
}
// こうなります
// [output]
// [2 4 6 8 10 12 14 16 18]

スライスは返り値に nil を指定できる

goのsliceは配列(array)のポインタなのでnil指定ができます。(*Slice internal)

A slice is a descriptor of an array segment. It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment).

スライスのソート

実際の開発ではソートを必要とする場面はしばしばありそうです。いろいろなソートにも対応したメソッドが用意されています。

ソート:サンプル をカスタマイズして試してみます。

// Person 
type Person struct {
    Name string
    Age  int
    Height int
    Weight int
}

func (p Person) String() string {
    return fmt.Sprintf("%s: H: %d,W: %d,Y: %d\n", p.Name, p.Height, p.Weight, p.Age)
}

func sortByAge() {
    people := []Person{
        {"Bob", 31, 178, 67},
        {"John", 42, 189, 79},
        {"Michael", 17, 169, 56},
        {"Jenny", 26, 180, 78},
    }

    // 年齢の降順でソートする
    sort.Slice(people, func(i, j int) bool {
        return people[i].Age > people[j].Age
    })
    fmt.Println(people)

}

// [output]
//  年齢降順の結果
// [John: H: 189,W: 79,Y: 42 
//  Bob: H: 178,W: 67,Y: 31 
//  Jenny: H: 180,W: 78,Y: 26 
//  Michael: H: 169,W: 56,Y: 17]


// 複数の条件でソートする。
// 優先度: 身長→体重→年齢 でソートする
func sortByHeightWeightAge() {
    people := []Person{
        {"Michael", 17, 169, 56},
        {"Jenny", 26, 180, 56},
        {"Bob", 31, 189, 70},
        {"John", 33, 189, 70},
    }

    sort.Slice(people, func(i, j int) bool {
        if people[i].Height > people[j].Height {
            return true
        } else if people[i].Height == people[j].Height {
            if people[i].Weight > people[j].Weight {
                return true
            }
        } else if people[i].Weight == people[j].Weight {
            if people[i].Age > people[j].Age {
                return true
            }
        }
        return false
    })
    fmt.Println(people)
}

// [output]
//  年齢降順の結果
// [Bob: H: 189,W: 70,Y: 31 
//  John: H: 189,W: 70,Y: 33
//  Jenny: H: 180,W: 56,Y: 26
//  Michael: H: 169,W: 56,Y: 17]

Go Playground で確認する

まとめ

Sliceは配列(array)をラップして、連続したデータに対して強力なインターフェイスを提供しているようです。
Go言語では配列プログラミングは単純な配列ではなくスライスを使用して行われています。
そのようなポイントで他の言語のリストや配列と違っています。
今回は開発の中でスライスを扱う時に詰まりそれで調べてみたことなど紹介しました。

Discussion