Arrays and slices in Go – for interviews

Having gained some knowledge at first, we begin to use arrays and slices in Go quite confidently and usually don’t think too much about various unpleasant details (if we write carefully enough). However, when it comes to interviews, it turns out that our intuitive ideas can easily fail – something was forgotten somewhere, and some nuances may not have been thought about.

Here are a few basic questions that were encountered during the last job search session 🙂 some may be trivial – but it’s difficult to guess who may have a gap on what question. And maybe it will help those who are just delving into the language. In some places, details from the manuals have been added. We will avoid too detailed answers so as not to get bored – they are not difficult to find.

It is advisable not to use this list as interview questions. Some of them may give the candidate a strange impression of you 🙂

To begin with: what is an array and what is a slice?

And how can you tell them apart? Let us remember that in Go an array is just that: an array – a sequence of elements of the same type of a given (and unchangeable) length – but often we operate not with arrays, but with slices from them.

A slice is a lightweight thing – just a pointer to the array “placed” under it, and not necessarily from the beginning – and the length. The array under the slice can be “implicit” – that is, it does not have “its own” variable. At the same time, several slices, including intersecting ones, can grow like mushrooms on one array.

In addition to the length, the slice also has “capacity” (capacity) – it relates specifically to the slice, although it depends on the underlying array. It’s better to look at examples later.

Function len(…)

Everyone knows about len(…) – it returns the length of a string, array, slice or map size. What can it be applied to besides arrays, strings, slices or maps?

To a pointer to an array and to a channel (!). But you can’t access a pointer to a slice or map (this can be explained, but it may not be easy to remember).

Also len(…) normally swallows nils if they are one of the above types.

And the function cap(…)

You can live for years and not know about it. It returns the capacity of the slice. In addition to the slice, you can call it on an array, although there is no point in this. When is it needed? I don’t think you can easily come up with good cases 🙂

For self-control, check what the slice capacities are in the code below:

a := []int{2, 3, 5, 7, 9}
println(cap(a))
b := a[1:4]
println(cap(b))

About the make(…) function

Probably one of the first questions is what can be done with its help (slice, map or channel). In this case, the second argument is the size – mandatory for a slice, optional for a map and channel. For the map, it determines the initial number of “buckets”, but since it grows automatically, this is not often remembered. For slices, you can also specify a third argument – capacity (i.e., the size of the unnamed array under this slice).

	a := make([]int, 3, 5)
	fmt.Printf("%v %d %d\n", a, len(a), cap(a))

I think we often create slices just like a := []int{} for the purpose of further appendage. What capacity will it have? No need to guess 🙂

What happens if you go beyond the slice?

Yes, there will be panic – it seems like an obvious answer, we've probably encountered it 🙂

	a := []int{2, 3, 5, 7, 9}
	b := a[1:4]
	println(b[3])      // паника, т.к. длина этого слайса всего 3

however…

What's outside the slice if you really want it?

Don't panic! A slice can be rolled to a slice of greater length

	a := []int{2, 3, 5, 7, 9}
	b := a[1:4]
	println(b[:4][3])      // печатает 9 из массива под слайсом

however…

It’s still impossible to get out of the capacity, b[:6] in this example will cause panic

Sloppy append(…)

Probably a classic – append can modify the underlying array if there is capacity. You don't come across this very often, but especially when passing it to a function, you can:

func sum(a []int) int {
    // somewhat artistic way to do this simple task
	a = append(a, 0)
	s := 0
	for a[0] > 0 {
		s += a[0]
		a = a[1:]
	}
	return s
}

func main() {
	primes := []int{2, 3, 5, 7, 11, 13}
	println(sum(primes[0:3]))
	println(sum(primes[2:5]))
}

Inside the function, we append a zero to the slice – it seems like nothing, because the variable “a” is local, it itself can be changed as much as you like. Of course, we are unlikely to write such sophisticated code for a sum – but in other situations the temptation to add something to the end to simplify the processing of “boundary conditions” can be great.

How does a slice grow?

That is, if append(…) still gets out of capacity. And a new array (implicit) is allocated for the slice, returned as the result of the append. And elements are copied into it. What size is this new array (and slice capacity)? It's easy to check:

	a := make([]int, 7)
	a = append(a, 13)
	fmt.Printf("%d %d\n", len(a), cap(a)) // печатает 8 и 14

so, the size doubles – this behavior can be found in similar cases in other languages, but you should not talk about this as an immutable truth – of course, this is an implementation detail, it is not specified. A separate nuance is if the slice initially had capacity=0. But in general, we are only interested in this insofar as a slice of 100 million elements when adding just one number can require 300 million of memory (since during copying it is necessary to have both the old and new array in memory).

How to append a whole slice and not a single element?

You just need to remember that append allows you to add an arbitrary number of elements separated by commas (variable arguments) – and there is a magic syntax with an “ellipsis” that expands the slice into the following sequence of arguments:

b := []int{1, 2, 3, 5, 8}
a := append(a, b...)

Is there a limit to the length of the slice added this way? After all, it would seem that function arguments, like local variables, are allocated on the stack. But firstly, the goroutine stack increases as needed – secondly, variadic arguments are actually passed using a slice – that is, this is “syntactic sugar”, and not a hardcore implementation as in C.

function clear(…)

Another feature that you might not know about. And it may even be better not to know. It cleans maps and slices. But it cannot be applied to an array (logic?)

Moreover, the map simply becomes empty, and “zero” values ​​of this type are entered in the slice, which may be slightly unobvious.

function copy(…)

From the same opera: a function that copies slice to slice. Why not make a function that “clones” a slice is unclear. One of the nuances is that it can copy a string into a slice byte. It is also separately emphasized that slices can intersect, but it does not say what result we expect. Apparently the authors have reminiscences about strcpy(…) from the C language, which, if used carelessly in this case, could lead to the erasure of the end of line sign, followed by a segfault or damage to other variables.

	a := []int{2, 3, 5, 7, 11}
	copy(a[0:2], a[2:4])
	fmt.Printf("%v\n", a)

You can try to guess which array is the result (what if you swap the function arguments?) – although if I understand correctly, since it is not specified, the result may even depend on the version.

Can a function return an array (not a slice)

Of course it can – although in practice you rarely see this (maybe that’s why they’re trying to “catch” it?) Cases when you need to return data of a fixed size occur, for example, when calculating hashes. As an example – in crypto/md5:

const Size = 16
...
func Sum(data []byte) [Size]byte

How to cast an array into a slice

For example, to use it in clear(…) or append(…) – or use the result of the hashing function (above) where a slice is needed.

In general, this is obvious – just take a slice from it the size of the entire array. By asking this question, they are either trying to catch him by surprise, or hoping that the candidate will be clever with the syntax:

arr := md5.Sum([]byte("I'm a fine string"))
slice1 := arr[0:len(arr)] // если мы забыли что начало и конец можно не указывать
slice2 := arr[:] // вот так норм

This question leads to another, ideological and abstract one.

Was it possible to leave Go without arrays at all?

What this means is to use only slices under which implicit arrays are allocated. I chuckled at this question and answered in the affirmative, since I don’t remember (and can’t think of a case) when a strictly array is needed or when a slice would get in the way. The interviewer also chuckled and said something vague like “uh-huh” – he probably couldn’t remember or come up with it either. Why did you ask? purely “to see how the interlocutor reasons”?

Let's stop there – good luck to everyone! Feel free to add, correct, criticize!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *