Arrays and slices

You can find all the code for this chapter here

Arrays allow you to store multiple elements of the same type in a variable ina particular order.

When you have an array, it is very common to have to iterate over them. So let’suse our new-found knowledge of for to make a Sum function. Sum willtake an array of numbers and return the total.

Let’s use our TDD skills

Write the test first

In sum_test.go

  1. package main
  2. import "testing"
  3. func TestSum(t *testing.T) {
  4. numbers := [5]int{1, 2, 3, 4, 5}
  5. got := Sum(numbers)
  6. want := 15
  7. if want != got {
  8. t.Errorf("got %d want %d given, %v", got, want, numbers)
  9. }
  10. }

Arrays have a fixed capacity which you define when you declare the variable.We can initialize an array in two ways:

  • [N]type{value1, value2, …, valueN} e.g. numbers := [5]int{1, 2, 3, 4, 5}
  • […]type{value1, value2, …, valueN} e.g. numbers := [...]int{1, 2, 3, 4, 5}

It is sometimes useful to also print the inputs to the function in the errormessage and we are using the %v placeholder which is the “default” format,which works well for arrays.

Read more about the format strings

Try to run the test

By running go test the compiler will fail with ./sum_test.go:10:15: undefined: Sum

Write the minimal amount of code for the test to run and check the failing test output

In sum.go

  1. package main
  2. func Sum(numbers [5]int) int {
  3. return 0
  4. }

Your test should now fail with a clear error message

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

Write enough code to make it pass

  1. func Sum(numbers [5]int) int {
  2. sum := 0
  3. for i := 0; i < 5; i++ {
  4. sum += numbers[i]
  5. }
  6. return sum
  7. }

To get the value out of an array at a particular index, just use array[index]syntax. In this case, we are using for to iterate 5 times to work through thearray and add each item onto sum.

Refactor

Let’s introduce range to help clean up our code

  1. func Sum(numbers [5]int) int {
  2. sum := 0
  3. for _, number := range numbers {
  4. sum += number
  5. }
  6. return sum
  7. }

range lets you iterate over an array. Every time it is called it returns twovalues, the index and the value. We are choosing to ignore the index value byusing _ blank identifier.

Arrays and their type

An interesting property of arrays is that the size is encoded in its type. If you tryto pass an [4]int into a function that expects [5]int, it won’t compile.They are different types so it’s just the same as trying to pass a string intoa function that wants an int.

You may be thinking it’s quite cumbersome that arrays have a fixed length, and mostof the time you probably won’t be using them!

Go has slices which do not encode the size of the collection and instead canhave any size.

The next requirement will be to sum collections of varying sizes.

Write the test first

We will now use the slice type which allows us to have collections ofany size. The syntax is very similar to arrays, you just omit the size whendeclaring them

mySlice := []int{1,2,3} rather than myArray := [3]int{1,2,3}

  1. func TestSum(t *testing.T) {
  2. t.Run("collection of 5 numbers", func(t *testing.T) {
  3. numbers := [5]int{1, 2, 3, 4, 5}
  4. got := Sum(numbers)
  5. want := 15
  6. if got != want {
  7. t.Errorf("got %d want %d given, %v", got, want, numbers)
  8. }
  9. })
  10. t.Run("collection of any size", func(t *testing.T) {
  11. numbers := []int{1, 2, 3}
  12. got := Sum(numbers)
  13. want := 6
  14. if got != want {
  15. t.Errorf("got %d want %d given, %v", got, want, numbers)
  16. }
  17. })
  18. }

Try and run the test

This does not compile

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

Write the minimal amount of code for the test to run and check the failing test output

The problem here is we can either

  • Break the existing API by changing the argument to Sum to be a slice ratherthan an array. When we do this we will know we have potentially ruinedsomeone’s day because our other test will not compile!
  • Create a new function

In our case, no-one else is using our function so rather than having two functions to maintain let’s just have one.

  1. func Sum(numbers []int) int {
  2. sum := 0
  3. for _, number := range numbers {
  4. sum += number
  5. }
  6. return sum
  7. }

If you try to run the tests they will still not compile, you will have to change the first test to pass in a slice rather than an array.

Write enough code to make it pass

It turns out that fixing the compiler problems were all we need to do here and the tests pass!

Refactor

We had already refactored Sum and all we’ve done is changing from arrays to slices, so there’s not a lot to do here. Remember that we must not neglect our test code in the refactoring stage and we have some to do here.

  1. func TestSum(t *testing.T) {
  2. t.Run("collection of 5 numbers", func(t *testing.T) {
  3. numbers := []int{1, 2, 3, 4, 5}
  4. got := Sum(numbers)
  5. want := 15
  6. if got != want {
  7. t.Errorf("got %d want %d given, %v", got, want, numbers)
  8. }
  9. })
  10. t.Run("collection of any size", func(t *testing.T) {
  11. numbers := []int{1, 2, 3}
  12. got := Sum(numbers)
  13. want := 6
  14. if got != want {
  15. t.Errorf("got %d want %d given, %v", got, want, numbers)
  16. }
  17. })
  18. }

It is important to question the value of your tests. It should not be a goal tohave as many tests as possible, but rather to have as much confidence aspossible in your code base. Having too many tests can turn in to a real problemand it just adds more overhead in maintenance. Every test has a cost.

In our case, you can see that having two tests for this function is redundant.If it works for a slice of one size it’s very likely it’ll work for a slice ofany size (within reason).

Go’s built-in testing toolkit features a coveragetool, which can help identify areas of your codeyou have not covered. I do want to stress that having 100% coverage should notbe your goal, it’s just a tool to give you an idea of your coverage. If you havebeen strict with TDD, it’s quite likely you’ll have close to 100% coverageanyway.

Try running

go test -cover

You should see

  1. PASS
  2. coverage: 100.0% of statements

Now delete one of the tests and check the coverage again.

Now that we are happy we have a well-tested function you should commit yourgreat work before taking on the next challenge.

We need a new function called SumAll which will take a varying number ofslices, returning a new slice containing the totals for each slice passed in.

For example

SumAll([]int{1,2}, []int{0,9}) would return []int{3, 9}

or

SumAll([]int{1,1,1}) would return []int{3}

Write the test first

  1. func TestSumAll(t *testing.T) {
  2. got := SumAll([]int{1,2}, []int{0,9})
  3. want := []int{3, 9}
  4. if got != want {
  5. t.Errorf("got %v want %v", got, want)
  6. }
  7. }

Try and run the test

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

Write the minimal amount of code for the test to run and check the failing test output

We need to define SumAll according to what our test wants.

Go can let you write variadic functions that can take a variable number of arguments.

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

Try to compile but our tests still don’t compile!

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

Go does not let you use equality operators with slices. You could writea function to iterate over each got and want slice and check their valuesbut for convenience sake, we can use reflect.DeepEqual which isuseful for seeing if any two variables are the same.

  1. func TestSumAll(t *testing.T) {
  2. got := SumAll([]int{1,2}, []int{0,9})
  3. want := []int{3, 9}
  4. if !reflect.DeepEqual(got, want) {
  5. t.Errorf("got %v want %v", got, want)
  6. }
  7. }

(make sure you import reflect in the top of your file to have access to DeepEqual)

It’s important to note that reflect.DeepEqual is not “type safe”, the codewill compile even if you did something a bit silly. To see this in action,temporarily change the test to:

  1. func TestSumAll(t *testing.T) {
  2. got := SumAll([]int{1,2}, []int{0,9})
  3. want := "bob"
  4. if !reflect.DeepEqual(got, want) {
  5. t.Errorf("got %v want %v", got, want)
  6. }
  7. }

What we have done here is try to compare a slice with a string. Which makesno sense, but the test compiles! So while using reflect.DeepEqual isa convenient way of comparing slices (and other things) you must be carefulwhen using it.

Change the test back again and run it, you should have test output looking like this

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

Write enough code to make it pass

What we need to do is iterate over the varargs, calculate the sum using ourSum function from before and then add it to the slice we will return

  1. func SumAll(numbersToSum ...[]int) []int {
  2. lengthOfNumbers := len(numbersToSum)
  3. sums := make([]int, lengthOfNumbers)
  4. for i, numbers := range numbersToSum {
  5. sums[i] = Sum(numbers)
  6. }
  7. return sums
  8. }

Lots of new things to learn!

There’s a new way to create a slice. make allows you to create a slice witha starting capacity of the len of the numbersToSum we need to work through.

You can index slices like arrays with mySlice[N] to get the value out orassign it a new value with =

The tests should now pass

Refactor

As mentioned, slices have a capacity. If you have a slice with a capacity of2 and try to do mySlice[10] = 1 you will get a runtime error.

However, you can use the append function which takes a slice and a new value,returning a new slice with all the items in it.

  1. func SumAll(numbersToSum ...[]int) []int {
  2. var sums []int
  3. for _, numbers := range numbersToSum {
  4. sums = append(sums, Sum(numbers))
  5. }
  6. return sums
  7. }

In this implementation, we are worrying less about capacity. We start with anempty slice sums and append to it the result of Sum as we work through the varargs.

Our next requirement is to change SumAll to SumAllTails, where it nowcalculates the totals of the “tails” of each slice. The tail of a collection isall the items apart from the first one (the “head”)

Write the test first

  1. func TestSumAllTails(t *testing.T) {
  2. got := SumAllTails([]int{1,2}, []int{0,9})
  3. want := []int{2, 9}
  4. if !reflect.DeepEqual(got, want) {
  5. t.Errorf("got %v want %v", got, want)
  6. }
  7. }

Try and run the test

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

Write the minimal amount of code for the test to run and check the failing test output

Rename the function to SumAllTails and re-run the test

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

Write enough code to make it pass

  1. func SumAllTails(numbersToSum ...[]int) []int {
  2. var sums []int
  3. for _, numbers := range numbersToSum {
  4. tail := numbers[1:]
  5. sums = append(sums, Sum(tail))
  6. }
  7. return sums
  8. }

Slices can be sliced! The syntax is slice[low:high] If you omit the value onone of the sides of the : it captures everything to the side of it. In ourcase, we are saying “take from 1 to the end” with numbers[1:]. You might want toinvest some time in writing other tests around slices and experimenting with theslice operator so you can be familiar with it.

Refactor

Not a lot to refactor this time.

What do you think would happen if you passed in an empty slice into ourfunction? What is the “tail” of an empty slice? What happens when you tell Go tocapture all elements from myEmptySlice[1:]?

Write the test first

  1. func TestSumAllTails(t *testing.T) {
  2. t.Run("make the sums of some slices", func(t *testing.T) {
  3. got := SumAllTails([]int{1,2}, []int{0,9})
  4. want := []int{2, 9}
  5. if !reflect.DeepEqual(got, want) {
  6. t.Errorf("got %v want %v", got, want)
  7. }
  8. })
  9. t.Run("safely sum empty slices", func(t *testing.T) {
  10. got := SumAllTails([]int{}, []int{3, 4, 5})
  11. want := []int{0, 9}
  12. if !reflect.DeepEqual(got, want) {
  13. t.Errorf("got %v want %v", got, want)
  14. }
  15. })
  16. }

Try and run the test

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

Oh no! It’s important to note the test has compiled, it is a runtime error.Compile time errors are our friend because they help us write software thatworks, runtime errors are our enemies because they affect our users.

Write enough code to make it pass

  1. func SumAllTails(numbersToSum ...[]int) []int {
  2. var sums []int
  3. for _, numbers := range numbersToSum {
  4. if len(numbers) == 0 {
  5. sums = append(sums, 0)
  6. } else {
  7. tail := numbers[1:]
  8. sums = append(sums, Sum(tail))
  9. }
  10. }
  11. return sums
  12. }

Refactor

Our tests have some repeated code around assertion again, let’s extract that into a function

  1. func TestSumAllTails(t *testing.T) {
  2. checkSums := func(t *testing.T, got, want []int) {
  3. t.Helper()
  4. if !reflect.DeepEqual(got, want) {
  5. t.Errorf("got %v want %v", got, want)
  6. }
  7. }
  8. t.Run("make the sums of tails of", func(t *testing.T) {
  9. got := SumAllTails([]int{1, 2}, []int{0, 9})
  10. want := []int{2, 9}
  11. checkSums(t, got, want)
  12. })
  13. t.Run("safely sum empty slices", func(t *testing.T) {
  14. got := SumAllTails([]int{}, []int{3, 4, 5})
  15. want := []int{0, 9}
  16. checkSums(t, got, want)
  17. })
  18. }

A handy side-effect of this is this adds a little type-safety to our code. Ifa silly developer adds a new test with checkSums(t, got, "dave") the compilerwill stop them in their tracks.

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

Wrapping up

We have covered

  • Arrays
  • Slices
    • The various ways to make them
    • How they have a fixed capacity but you can create new slices from old onesusing append
    • How to slice, slices!
  • len to get the length of an array or slice
  • Test coverage tool
  • reflect.DeepEqual and why it’s useful but can reduce the type-safety of your code

We’ve used slices and arrays with integers but they work with any other typetoo, including arrays/slices themselves. So you can declare a variable of[][]string if you need to.

Check out the Go blog post on slices for an in-depth look intoslices. Try writing more tests to demonstrate what you learn from reading it.

Another handy way to experiment with Go other than writing tests is the Goplayground. You can try most things out and you can easily share your code ifyou need to ask questions. I have made a go playground with a slice in it for you to experiment with.

Here is an example of slicing an arrayand how changing the slice affects the original array; but a “copy” of the slicewill not affect the original array.Another example of why it’s a good ideato make a copy of a slice after slicing a very large slice.