Slice gotchas

Station 2.3 14 min Interchange: copy-on-write arrays & array_slice() → shared backing arrays

PHP arrays never surprise you. Assign one, pass it to a function, slice it with array_slice: you always get your own copy, and nothing you do to it reaches the original. (PHP is clever about this behind the scenes: it only really copies when someone writes.) Go arrays work the same way, as 2.1 showed.

Slices don't. A slice is a small header pointing at a backing array (2.2), and copying the header doesn't copy the array. So two slices can quietly share the same elements:

%%{init: {"flowchart": {"wrappingWidth": 400}}}%%
flowchart TB
  accTitle: Two slices sharing one backing array
  accDescr: stops is a slice of length 4 over a backing array holding Bank, Moorgate, Old Street and Angel. first, made with stops[:2], is a second slice of length 2 and capacity 4 that points at the same array. Writing through either slice changes the elements both of them see.
  stops["stops<br/>len 4, cap 4"] --> arr["Bank | Moorgate | Old Street | Angel"]
  first["first := stops[:2]<br/>len 2, cap 4"] --> arr

That sharing is why slices are cheap to pass around. It's also the source of every bug in this lesson.

Assigning and slicing share

same := stops    // a second header, same array
same[0] = "Oval" // stops[0] is "Oval" now too

first := stops[:2]
first[1] = "Euston" // and stops[1] is "Euston"

Neither same nor first is a copy. Each is another view of the same four elements.

Functions can change your elements

Passing a slice to a function passes the header. The function can't change your length or capacity, because those were copied, but it can change the elements, because those are shared:

func shout(stops []string) {
	for i := range stops {
		stops[i] = strings.ToUpper(stops[i])
	}
}

Call shout(route) and route is in capitals afterwards. A PHP function doing the same to an array parameter would only change its own copy. Watch out for standard library functions that work in place: slices.Sort and slices.Reverse rearrange the slice you give them, and don't return a new one.

The flip side: an append inside the function doesn't show up in the caller's slice. The caller's length never changed, so the new element sits beyond its view (or in a different array altogether). That's one more reason Go functions return the slice they appended to.

append can overwrite a neighbour

This is the one that really hurts. A sub-slice has the original's spare capacity, so appending to it writes into the original:

09:00first := stops[:2]
09:01first = append(first, "Euston")
09:02stops[2] is now "Euston"
09:03"But I never touched stops!"
Arrays copy. PHP arrays copy. Slices share.

The same thing happens with any slice that has spare capacity, and slices grown with append usually do. Append twice to the same base slice and both results can share the same last slot, so the second append overwrites the first's value. The exercise is exactly that bug.

Asking for a real copy

When the result must be independent, make a copy explicitly:

  • slices.Clone(s) returns a new slice with its own backing array. It's the everyday answer.
  • copy(dst, src) copies elements into a slice you've already made, and returns how many it copied: the shorter of the two lengths.
  • s[low:high:max], the full slice expression, caps the new slice's capacity at max. With stops[:2:2] there's no spare room, so the next append has to allocate a fresh array instead of writing into stops.

Mind the gap

"Does this change it, or give me a new one?" In PHP the answer is nearly always "a new one", because arrays are copied. In Go you have to know. A function that takes a slice can change its elements, and append can write into memory that another slice can see. When a function builds a new slice from one it was given, clone first. When it deliberately changes the slice in place, say so in its doc comment.

Laravel corner

You've met this question before, with Collections. They're objects, so $b = $a shares one Collection, and their methods split into ones that return a new Collection (map, filter, sortBy) and ones that change it in place (transform, push, pop). Go's slices have the same split, minus the safety net of a class that documents which is which.

Recap

  • Assigning, slicing and passing a slice all share the backing array. Only arrays (and PHP arrays) copy.
  • A function can change the elements of a slice you pass it. slices.Sort and slices.Reverse work in place.
  • append on a slice with spare capacity writes into shared memory, which can overwrite another slice's elements.
  • slices.Clone makes an independent copy, copy fills an existing slice, and s[a:b:b] limits capacity.

1Assigning

PHPassign.php
<?php
$stops = ['Bank', 'Moorgate', 'Angel'];
$copy = $stops;
$copy[0] = 'Oval';

echo $stops[0]; // "Bank": a separate copy
GOassign.go
stops := []string{"Bank", "Moorgate", "Angel"}
same := stops // same backing array
same[0] = "Oval"

fmt.Println(stops[0]) // "Oval"

Mind the gap

PHP arrays copy on assignment, and so do Go arrays (2.1). Go slices don't: same := stops copies the slice header, not the elements, so both names share one backing array.

2Passing to a function

PHPfunc.php
<?php
function sorted(array $stops): array
{
    sort($stops); // sorts this function's copy
    return $stops;
}

$route = ['Moorgate', 'Angel', 'Bank'];
sorted($route);
// $route is unchanged
GOfunc.go
func sorted(stops []string) []string {
	slices.Sort(stops) // sorts the caller's elements
	return stops
}

route := []string{"Moorgate", "Angel", "Bank"}
sorted(route)
// route is now [Angel Bank Moorgate]

Mind the gap

A PHP function gets its own copy of an array. A Go function gets a slice that shares the caller's backing array, so changing an element changes it for the caller too.

3Making a real copy

PHPclone.php
<?php
$copy = $stops;                       // already a copy

$firstTwo = array_slice($stops, 0, 2); // a copy too
GOclone.go
copied := slices.Clone(stops) // a real copy

dst := make([]string, 2)
copy(dst, stops) // copies min(len(dst), len(stops))

Mind the gap

In PHP you never ask for a copy, because you always get one. In Go you ask: slices.Clone for a whole slice, or the built-in copy to fill one you've already made.