配列とスライス

Arrays and slices

この章のすべてのコードはここにあります

配列を使うと、同じ型の複数の要素を特定の順番で変数に格納することができます。

配列を持っていると、それらの要素を繰り返し処理しなければならないことがよくあります。そこで、新しく知ったforの知識 を使ってSum関数を作ってみましょう。Sumは数値の配列を受け取り、その合計を返します。

テスト駆動開発(TDD)の技術を使ってみましょう

最初にテストを書く

In sum_test.go

package main

import "testing"

func TestSum(t *testing.T) {

    numbers := [5]int{1, 2, 3, 4, 5}

    got := Sum(numbers)
    want := 15

    if got != want {
        t.Errorf("got %d want %d given, %v", got, want, numbers)
    }
}

配列には固定容量があり、これは変数を宣言する際に定義します。配列を初期化するには、以下の2つの方法があります。

  • numbers := [5]int{1, 2, 3, 4, 5}

  • numbers := [...]int{1, 2, 3, 4, 5}

エラーメッセージに関数への入力を表示するのも便利な場合がありますが、ここでは %v プレースホルダを使用しています。

文字列の書式についての詳細はこちら

テストを実行してみてください

go testを実行すると、コンパイラは失敗します。 ./sum_test.go:10:15: undefined: Sum

テストを実行するための最小限のコードを書き、失敗したテストの出力をチェックする

In sum.go

package main

func Sum(numbers [5]int) int {
    return 0
}

これで、テストは明確なエラーメッセージが表示されて失敗するはずです。

sum_test.go:13: got 0 want 15 given, [1 2 3 4 5]

テストをパスするのに十分なコードを書く

func Sum(numbers [5]int) int {
    sum := 0
    for i := 0; i < 5; i++ {
        sum += numbers[i]
    }
    return sum
}

特定のインデックスの配列から値を取り出すには、array[index]構文を使えば大丈夫です。この例ではforを使って配列を5回繰り返し、各項目をsumに加算しています。

リファクタリング♪

コードをきれいにするためにrangeを導入してみましょう。

func Sum(numbers [5]int) int {
    sum := 0
    for _, number := range numbers {
        sum += number
    }
    return sum
}

rangeは配列の反復処理を行うことができる。呼び出されるたびにインデックスと値の2つの値を返します。ここではインデックスの値を無視して _ 空(スペース)の識別子を使用しています。

配列とその型

配列の興味深い特性として、サイズが型でエンコードされていることが挙げられます。 もし [5]int を期待する関数に [4]int を渡そうとしてもコンパイルできません。 これらは異なる型なので、int を求める関数に string を渡そうとするのと同じです。

配列の長さが固定されているのは非常に面倒だと思うかもしれませんし、ほとんどの場合、配列を使うことはないでしょう。

Goにはslicesがありますが、これはコレクションのサイズをエンコードするものではなく、任意のサイズを持つことができます。

次の要件は、さまざまなサイズのコレクションを合計することです。

最初にテストを書く

ここでは、任意のサイズのコレクションを持つことができる slice type を使用します。構文は配列と非常に似ていますが、宣言時にサイズを省略するだけです。

myArray := [3]int{1,2,3}ではなく、mySlice := []int{1,2,3}こちらです。

func TestSum(t *testing.T) {

    t.Run("collection of 5 numbers", func(t *testing.T) {
        numbers := [5]int{1, 2, 3, 4, 5}

        got := Sum(numbers)
        want := 15

        if got != want {
            t.Errorf("got %d want %d given, %v", got, want, numbers)
        }
    })

    t.Run("collection of any size", func(t *testing.T) {
        numbers := []int{1, 2, 3}

        got := Sum(numbers)
        want := 6

        if got != want {
            t.Errorf("got %d want %d given, %v", got, want, numbers)
        }
    })

}

テストを実行してみてください

これはコンパイルされません

./sum_test.go:22:13: cannot use numbers (type []int) as type [5]int in argument to Sum

テストを実行するための最小限のコードを書き、失敗したテストの出力をチェックする

ここで問題なのは

  • 引数Sumをスライスに変更することで、既存のAPIを壊します。

    配列に対して、このようなことをすると、破壊された可能性を潜在的発見するので他のテストがコンパイルされません!

  • 新しい関数を作成しましょう

私たちの場合は誰もこの関数を使っていないので、二つの関数を持つよりも一つの関数を持つことにしましょう。

func Sum(numbers []int) int {
    sum := 0
    for _, number := range numbers {
        sum += number
    }
    return sum
}

テストを実行しようとしてもコンパイルされないので、最初のテストを配列ではなくスライスで渡すように変更しなければなりません。

テストをパスさせるのに十分なコードを書いてください

コンパイラの問題を修正するだけで、次のことができることがわかりました。

リファクタリング♪

すでにSumをリファクタリングしており、配列からスライスに変更しただけなので、ここでやることはそれほど多くありません。リファクタリングの段階でテストコードをおろそかにしてはいけないことを覚えておいてください。

func TestSum(t *testing.T) {

    t.Run("collection of 5 numbers", func(t *testing.T) {
        numbers := []int{1, 2, 3, 4, 5}

        got := Sum(numbers)
        want := 15

        if got != want {
            t.Errorf("got %d want %d given, %v", got, want, numbers)
        }
    })

    t.Run("collection of any size", func(t *testing.T) {
        numbers := []int{1, 2, 3}

        got := Sum(numbers)
        want := 6

        if got != want {
            t.Errorf("got %d want %d given, %v", got, want, numbers)
        }
    })

}

テストの価値を問うことは重要です。可能な限り多くのテストを行うことが目標ではなく、むしろコードベースに対して可能な限り多くの信頼を持つことが目標です。あまりにも多くのテストを持っていると、実際に問題になることがありますし、メンテナンスのオーバーヘッドを増やすだけです。 すべてのテストにはコストがある.

私たちのケースでは、この関数のために2つのテストを持つことが冗長であることがわかります。あるサイズのスライスで動作するなら、どのサイズのスライスでも動作する可能性が高いです。

Goの組み込みテストツールキットには、 カバレッジツールがあり、あなたがカバーしていないコードの領域を特定するのに役立ちます。私が強調したいのは、100%のカバレッジを持つことがゴールではないということです。TDD を厳格に行っていれば、100% に近いカバレッジが得られる可能性が高いでしょう。

実行してみてください。

go test -cover

見てください。

PASS
coverage: 100.0% of statements

テストの一つを削除して、カバレッジをもう一度確認してください。

これで、十分にテストされた関数を手に入れたことに満足しているので、次の課題に挑戦する前に、あなたの素晴らしい作品をコミットしてください。

次に様々なスライスの数を受け取り、渡された各スライスの SumAll を含む新しいスライスを返します。

例えば、以下のようになります。

SumAll([]int{1,2}, []int{0,9})[]int{3, 9}を返します。

もしくは

SumAll([]int{1,1,1})[]int{3}を返します。

最初にテストを書く

func TestSumAll(t *testing.T) {

    got := SumAll([]int{1, 2}, []int{0, 9})
    want := []int{3, 9}

    if got != want {
        t.Errorf("got %v want %v", got, want)
    }
}

テストを実行してみてください

./sum_test.go:23:9: undefined: SumAll

テストを実行するための最小限のコードを書き、失敗したテストの出力をチェックする

テストの目的に応じてSumAllを定義する必要があります。

Goを使えば、可変数の引数を取ることができる 可変関数 を書くことができます。

func SumAll(numbersToSum ...[]int) (sums []int) {
    return
}

コンパイルしようとしても、テストはまだコンパイルされません。

./sum_test.go:26:9: invalid operation: got != want (slice can only be compared to nil)

Goでは、スライスで等号演算子を使うことはできません。gotwant の各スライスを繰り返し処理して値を確認する関数を書くこともできますが、便利のために reflect.DeepEqual を使うと、2つの変数が同じであるかどうかを確認するのに便利です。

func TestSumAll(t *testing.T) {

    got := SumAll([]int{1, 2}, []int{0, 9})
    want := []int{3, 9}

    if !reflect.DeepEqual(got, want) {
        t.Errorf("got %v want %v", got, want)
    }
}

DeepEqualにアクセスできるようにするには、ファイルの先頭でimport reflectを作成します)

重要なのは、reflect.DeepEqualは「型安全」ではないことに注意することです。これを確認するには、テストを一時的に変更してください。

func TestSumAll(t *testing.T) {

    got := SumAll([]int{1, 2}, []int{0, 9})
    want := "bob"

    if !reflect.DeepEqual(got, want) {
        t.Errorf("got %v want %v", got, want)
    }
}

ここで行ったことは、slicestring を比較しようとしていることです。これでは意味がありませんが、テストはコンパイルされます。だから、reflect.DeepEqualを使うのはスライスを比較するのに便利な方法ですが、使うときは注意が必要です。

テストを再度変更して実行すると、以下のようなテスト出力が得られるはずです。

sum_test.go:30: got [] want [3 9]

テストをパスするのに十分なコードを書く

必要なのは可変長引数を繰り返し処理して、前に使ったSum関数を使って合計を計算し、それを返すスライスに追加することです。

func SumAll(numbersToSum ...[]int) []int {
    lengthOfNumbers := len(numbersToSum)
    sums := make([]int, lengthOfNumbers)

    for i, numbers := range numbersToSum {
        sums[i] = Sum(numbers)
    }

    return sums
}

新しい学びがたくさんあります!

スライスを作成する新しい方法があります。make を使うと、numbersToSumの開始容量が lenであるスライスを作成できるようになります。

配列のように mySlice[N] でスライスをインデックス化して値を取得したり、= で新しい値を代入したりすることができます。

これでテストは合格するはずです。

リファクタリング♪

前述したように、スライスには容量があります。容量2のスライスを持っていて mySlice[10] = 1 を実行しようとすると、ランタイムエラーが発生します。

しかし、append関数を使えば、スライスと新しい値を受け取り、その中にあるすべての項目を含む新しいスライスを返すことができます。

func SumAll(numbersToSum ...[]int) []int {
    var sums []int
    for _, numbers := range numbersToSum {
        sums = append(sums, Sum(numbers))
    }

    return sums
}

この実装では、容量についてはあまり気にしていません。空のスライス sums から始め、 可変長引数を処理しながら Sum の結果をそれに追加します。

次の要件は SumAllSumAllTails に変更することです。コレクションの末尾とは、最初のものを除いたすべてのアイテムのことです。

最初にテストを書く

func TestSumAllTails(t *testing.T) {
    got := SumAllTails([]int{1, 2}, []int{0, 9})
    want := []int{2, 9}

    if !reflect.DeepEqual(got, want) {
        t.Errorf("got %v want %v", got, want)
    }
}

テストを実行してみてください

./sum_test.go:26:9: undefined: SumAllTails

テストを実行するための最小限のコードを書き、失敗したテストの出力をチェックする

関数名を SumAllTails に変更し、テストを再実行します。

sum_test.go:30: got [3 9] want [2 9]

テストがパスするのに十分なコードを書く

func SumAllTails(numbersToSum ...[]int) []int {
    var sums []int
    for _, numbers := range numbersToSum {
        tail := numbers[1:]
        sums = append(sums, Sum(tail))
    }

    return sums
}

スライスはスライスすることができます。構文は slice[low:high] で、: の片方の辺の値を省略すると、その辺までのすべての値をキャプチャします。この例では、numbers[1:] を使って「1から最後まで取る」と言っています。スライスを使った他のテストを書いたり、スライス演算子に慣れるために実験をしたりすることに時間を投資したほうがいいかもしれません。

リファクタリング♪

今回はリファクタリングすることはあまりありません。

空のスライスを関数に渡すとどうなると思いますか?

空のスライスの「末尾」とは何ですか?

Goに myEmptySlice[1:] からすべての要素をキャプチャするように指示するとどうなるか?

最初にテストを書く

func TestSumAllTails(t *testing.T) {

    t.Run("make the sums of some slices", func(t *testing.T) {
        got := SumAllTails([]int{1, 2}, []int{0, 9})
        want := []int{2, 9}

        if !reflect.DeepEqual(got, want) {
            t.Errorf("got %v want %v", got, want)
        }
    })

    t.Run("safely sum empty slices", func(t *testing.T) {
        got := SumAllTails([]int{}, []int{3, 4, 5})
        want := []int{0, 9}

        if !reflect.DeepEqual(got, want) {
            t.Errorf("got %v want %v", got, want)
        }
    })

}

テストを実行してみてください

panic: runtime error: slice bounds out of range [recovered]
panic: runtime error: slice bounds out of range

これはランタイムエラーです。 コンパイル時のエラーは、動作するソフトウェアを書くのに役立ちますが、ランタイムエラーはユーザーに影響を与えます。

テストをパスするのに十分なコードを書く

func SumAllTails(numbersToSum ...[]int) []int {
    var sums []int
    for _, numbers := range numbersToSum {
        if len(numbers) == 0 {
            sums = append(sums, 0)
        } else {
            tail := numbers[1:]
            sums = append(sums, Sum(tail))
        }
    }

    return sums
}

リファクタリング♪

このテストでは、アサーションの周りに繰り返しコードがあるので、それを関数に抽出してみましょう。

func TestSumAllTails(t *testing.T) {

    checkSums := func(t *testing.T, got, want []int) {
        t.Helper()
        if !reflect.DeepEqual(got, want) {
            t.Errorf("got %v want %v", got, want)
        }
    }

    t.Run("make the sums of tails of", func(t *testing.T) {
        got := SumAllTails([]int{1, 2}, []int{0, 9})
        want := []int{2, 9}
        checkSums(t, got, want)
    })

    t.Run("safely sum empty slices", func(t *testing.T) {
        got := SumAllTails([]int{}, []int{3, 4, 5})
        want := []int{0, 9}
        checkSums(t, got, want)
    })

}

これは便利な副作用として、コードに少しだけ型の安全性が追加されます。 アホな開発者が checkSums(t, got, "dave") で新しいテストを追加しても、コンパイラはその場で止めてくれます。

$ go test
./sum_test.go:52:21: cannot use "dave" (type string) as type []int in argument to checkSums

まとめ

ここで学んだこと

  • 配列

  • スライス

    • 作り方いろいろ

    • 固定容量を持っていますが、古いものから新しいスライスを作成することができます。

      appendを使えば追加できます

    • スライスからスライス

  • 配列やスライスの長さを取得するために len を使用します。

  • テストカバレッジツール

  • reflect.DeepEqual と、なぜそれが便利なのか、しかしコードの型安全性を低下させる可能性があるのか。

ここまでは整数のスライスや配列を使ってきましたが、配列やスライス自体を含め、他の型でも動作します。ですから、必要に応じて [][]string の変数を宣言することができます。

スライスの詳細については、スライスに関するGoブログ記事を参照してください。これを読んで学んだことを実証するために、より多くのテストを書いてみてください。

テストを書く以外にGoを使って実験するもう一つの便利な方法は、Goの遊び場です。ほとんどのことを試すことができますし、質問が必要な場合は簡単にコードを共有することができます。 Go playgroundにスライスを入れて実験できるようにしてみました。

配列をスライスした例 では、配列をスライスして、スライスを変更すると元の配列にどのように影響するかを説明していますが、スライスの「コピー」は元の配列には影響しません。

別の例 非常に大きなスライスをスライスした後にコピーを作るのが良い理由があります。

最終更新