Maps

Station 2.4 13 min Interchange: associative arrays & isset() & array_key_exists() → map[K]V

The other big job PHP's array does is the associative array: ['Bank' => 3, 'Moorgate' => 1]. In Go that's a map, and its type spells out both halves: map[string]int has string keys and int values.

The basics

platforms := map[string]int{"Bank": 3, "Oval": 1}
platforms["Angel"] = 2         // add or replace
fmt.Println(platforms["Bank"]) // 3
fmt.Println(len(platforms))    // 3
delete(platforms, "Oval")      // PHP's unset()

Every key has the key type and every value has the value type, so there's no mixing 'Bank' => 3 with 'Bank' => 'three'. delete on a key that isn't there does nothing. range over a map gives you each key and value, though not in any order you can rely on. That's the whole of the next lesson.

Keys are typed too, which fixes an old PHP oddity. PHP quietly turns the key '1' into the integer 1, and true into 1 as well, so ['1' => 'a', true => 'c'] has just one element. In Go, a map[string]int key is a string, always. Any type that can be compared with == can be a key: strings, numbers, bools, arrays and simple structs. Slices can't, because they aren't comparable.

Missing keys

Read a key that isn't there and Go hands you the zero value of the value type, not null. platforms["Croydon"] is 0.

That's handy: counting is just counts[s]++, because a station you haven't seen yet starts at 0, with no ?? 0 needed. But it means a missing key and a key whose value is zero look the same. For that, a map lookup can return a second value:

p, ok := platforms["King's Cross"]

This is the comma-ok idiom you were promised back in 1.4 and 1.8:

flowchart TB
  accTitle: A comma-ok map lookup
  accDescr: The lookup p, ok := platforms[station] checks whether the key is in the map. If it is, p is the stored value and ok is true, even when the stored value is 0. If it isn't, p is the zero value, 0, and ok is false.
  look["p, ok := platforms[station]"] --> q["Is the key in the map?"]
  q -->|yes| yes["p = the stored value<br/>ok = true, even if p is 0"]
  q -->|no| no["p = 0, the zero value<br/>ok = false"]

Use ok whenever zero is a real value. King's Cross really does have a platform 0, so if p == 0 would announce that a major station doesn't exist. The exercise has that exact bug. You'll often see the lookup inside an if: if p, ok := platforms[s]; ok { … }.

The nil map

A map's zero value is nil, just like a slice's. A nil map is fine to read. Every lookup returns the zero value and len is 0. But writing to one panics:

Mind the gap

var counts map[string]int then counts["Bank"]++ panics with assignment to entry in nil map. A nil slice happily accepts append (2.2), so this catches people. Always make the map before writing to it: a literal, counts := map[string]int{}, or make(map[string]int). Struct fields that hold maps need making too, before anything writes to them.

Maps share, like slices

Assigning a map or passing it to a function shares it, just like a slice in 2.3. If a function adds a key, the caller sees it. When you need an independent copy, maps.Clone(m) makes one. And clear(m) empties a map in place.

Recap

  • map[K]V is Go's associative array. Keys and values are typed, and keys must be comparable.
  • A missing key reads as the zero value. v, ok := m[k] tells you whether it was really there.
  • counts[k]++ works without checking first, because missing keys start at zero.
  • A nil map can be read but not written. Make it with map[K]V{} or make first.
  • Maps are shared when assigned or passed. maps.Clone copies, and delete removes a key.

1The basics

PHPbasics.php
<?php
$platforms = ['Bank' => 3, 'Moorgate' => 1];
$platforms['Angel'] = 2;

echo count($platforms); // 3
unset($platforms['Moorgate']);
GObasics.go
platforms := map[string]int{"Bank": 3, "Moorgate": 1}
platforms["Angel"] = 2

fmt.Println(len(platforms)) // 3
delete(platforms, "Moorgate")

Mind the gap

map[string]int means string keys and int values, and every key and value must have those types. delete is a no-op if the key isn't there.

2Missing keys

PHPmissing.php
<?php
$p = $platforms['Croydon'] ?? null; // null
isset($platforms['Croydon']);       // false

array_key_exists("King's Cross", $platforms);
GOmissing.go
p := platforms["Croydon"] // 0: the zero value

p, ok := platforms["King's Cross"]
if ok {
	// it's really there, even if p is 0
}

Mind the gap

A missing key gives you the value type's zero value, not null. To tell "missing" from "zero", use the second result: p, ok := m[k].

3Counting things

PHPcount.php
<?php
$taps = ['Bank', 'Angel', 'Bank'];

$counts = array_count_values($taps);
// ['Bank' => 2, 'Angel' => 1]
GOcount.go
taps := []string{"Bank", "Angel", "Bank"}

counts := map[string]int{}
for _, s := range taps {
	counts[s]++ // a missing key starts at 0
}

Mind the gap

No array_count_values, but you rarely miss it. Because a missing key reads as 0, counts[s]++ just works, without PHP's ?? 0.

4Maps share, like slices

PHPshare.php
<?php
$copy = $platforms;
$copy['Oval'] = 4;

// $platforms has no 'Oval'
GOshare.go
same := platforms
same["Oval"] = 4

// platforms["Oval"] is 4 too

Mind the gap

Assigning or passing a map shares it, just like a slice (2.3). Use maps.Clone when you need an independent copy.