slices & maps packages

Station 2.6 15 min Interchange: in_array() & usort() & <=> & array_map() → slices.Contains

PHP has around eighty array_* functions, plus sort, usort, in_array and friends. Go keeps its equivalents in two small packages, slices and maps. You've already used a few: slices.Sort in 0.4, slices.Clone in 2.3, slices.Sorted and maps.Keys in 2.5. Here's the rest of the toolkit.

The quick tour

PHP Go
in_array($x, $xs, true) slices.Contains(xs, x)
array_search($x, $xs, true) slices.Index(xs, x)
sort($xs) slices.Sort(xs)
usort($xs, $fn) slices.SortFunc(xs, fn)
array_reverse($xs) slices.Reverse(xs)
max($xs) slices.Max(xs)
$a == $b slices.Equal(a, b)
array_keys($m) maps.Keys(m)
$copy = $m maps.Clone(m)

Everything compares strictly, so there's no loose in_array to remember a true for. slices.Reverse works in place rather than returning a copy, and maps.Keys returns an iterator (2.5), so wrap it in slices.Collect when you want a slice. For PHP's array_unique, sort first and then call slices.Compact, which removes duplicates that sit next to each other. Functions that answer "where is it?" return -1 for "not found", because a Go function can't return false instead of an int. And max(a, b) and min(a, b), for two or more separate values, are built into the language.

Functions as values (just enough)

usort takes a closure, and so does slices.SortFunc. A Go function without a name, written inline, looks like this:

func(a, b Entry) int {
	return cmp.Compare(a.Taps, b.Taps)
}

It's PHP's fn($a, $b) => $a['taps'] <=> $b['taps'], with types and braces. Like PHP's fn, it can use variables from around it without a use (...) clause. You'll see these anonymous functions everywhere the standard library wants "a bit of your logic": sorting, searching with slices.IndexFunc, and later HTTP handlers.

Sorting with a comparison

slices.Sort only works on things Go already knows how to order: numbers and strings. Try it on a slice of structs and you get a long compile error that starts Entry does not satisfy cmp.Ordered. For anything else, slices.SortFunc takes a comparison function. It returns a negative number if a comes first, a positive one if b does, and 0 if they tie, which is exactly what PHP's spaceship operator does.

  • cmp.Compare(x, y) is <=>. It returns -1, 0 or +1.
  • Swap the arguments to reverse the order. cmp.Compare(b.Taps, a.Taps) puts the busiest first, like $b <=> $a.
  • cmp.Or(...) returns the first of its arguments that isn't zero. It's PHP's ?: chain, and it's how you write a tie-break:
slices.SortFunc(board, func(a, b Entry) int {
	return cmp.Or(
		cmp.Compare(b.Taps, a.Taps),
		cmp.Compare(a.Station, b.Station),
	)
})
flowchart TB
  accTitle: How cmp.Or breaks a tie
  accDescr: To compare two leaderboard entries, cmp.Or first compares their tap counts, busiest first. If the counts differ, that result decides the order. If they are equal, the comparison is zero, so cmp.Or moves on and compares the station names alphabetically, and that result decides instead.
  a["Compare taps<br/>busiest first"] --> q["A tie?"]
  q -->|no| d1["Taps decide"]
  q -->|yes| b["Compare names<br/>A to Z"]
  b --> d2["Names decide"]

SortFunc isn't stable: items that compare as equal can come out in any order. So either give it a tie-break, as above, or use slices.SortStableFunc, which keeps equal items in their original order.

Laravel corner

$stations->sortBy([['taps', 'desc'], ['station', 'asc']]) is the Collection version of this sort. In Go the same rule is a comparison function: one cmp.Compare per column, wrapped in cmp.Or, with the arguments swapped for desc.

The array_map that isn't there

The slices package has no Map, no Filter and no Reduce. That's deliberate: a for loop does the job, and it can filter and map in a single pass:

var busy []string
for _, e := range board {
	if e.Taps > 100 {
		busy = append(busy, e.Station)
	}
}

If you really want Map and Filter helpers, lesson 3.9 shows how to write them with generics in a few lines. Most Go code just writes the loop.

Mind the gap

Some functions change the slice, others return a new one. slices.Sort, slices.SortFunc, slices.Reverse and slices.Compact work in place on the slice you pass in, which, after 2.3, you know means anyone sharing that backing array sees the change too. slices.Sorted, slices.Clone and slices.Collect return a new slice. In PHP, sort changes its argument but array_reverse returns a copy, so you've had to know this before. Go's docs say "in place" when it matters.

Recap

  • slices and maps hold Go's array_* functions. Searching is strict, and "not found" is -1.
  • An anonymous function is func(a, b T) int { … }, Go's version of PHP's fn.
  • slices.SortFunc takes a comparison. cmp.Compare is <=>, swapping its arguments reverses the order, and cmp.Or chains tie-breaks.
  • There's no Map or Filter. A for loop does both.
  • Sort, Reverse and Compact work in place. Sorted, Clone and Collect return new slices.

1Searching

PHPsearch.php
<?php
$line = ['Bank', 'Moorgate', 'Angel'];

in_array('Angel', $line, true);     // true
array_search('Angel', $line, true); // 2, or false
max([3, 9, 2]);                     // 9
GOsearch.go
line := []string{"Bank", "Moorgate", "Angel"}

slices.Contains(line, "Angel") // true
slices.Index(line, "Angel")    // 2, or -1
slices.Max([]int{3, 9, 2})     // 9

Mind the gap

slices.Index returns -1 when there's no match, rather than false. Always strict: there's no loose in_array to trip over.

2Sorting with a comparison

PHPsort.php
<?php
usort($board, fn($a, $b) =>
    $b['taps'] <=> $a['taps']           // busiest first
    ?: $a['station'] <=> $b['station']  // then A to Z
);
GOsort.go
slices.SortFunc(board, func(a, b Entry) int {
	return cmp.Or(
		cmp.Compare(b.Taps, a.Taps),       // busiest first
		cmp.Compare(a.Station, b.Station), // then A to Z
	)
})

Mind the gap

cmp.Compare is the spaceship operator, <=>, and cmp.Or is the ?: chain: it returns the first comparison that isn't zero.

3No array_map or array_filter

PHPmapfilter.php
<?php
$busy = array_filter($board, fn($e) => $e['taps'] > 100);
$names = array_map(fn($e) => $e['station'], $busy);
GOmapfilter.go
var names []string
for _, e := range board {
	if e.Taps > 100 {
		names = append(names, e.Station)
	}
}

Mind the gap

The standard library has no Map or Filter for slices. A loop does both jobs in one pass, and lesson 3.9 shows how to write generic helpers if you want them.

4Keys and values

PHPkeys.php
<?php
$stations = array_keys($taps);
$counts = array_values($taps);
GOkeys.go
stations := slices.Collect(maps.Keys(taps))
counts := slices.Collect(maps.Values(taps))
// both in no particular order (2.5)

Mind the gap

maps.Keys and maps.Values hand out iterators. slices.Collect gathers one into a slice, and slices.Sorted gathers and sorts.