Skip to main content
Go: Maps Structures
  1. Posts/

Go: Maps Structures

·858 words·5 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

Following the Learn Go with Tests guide, exploring Go’s built-in key-value data structure: maps.

Maps
#

dictionary := map[string]string{"test": "this is just a test"}

Map syntax: map[KeyType]ValueType{key: value}

Key constraints: Keys must be comparable types. Go’s comparison operators (==, !=) must be able to compare two values of the key type.

Compatible key types Examples:

Type Category Examples Comparison Rules
Boolean bool Equal if both true or both false
Integer int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 Compared numerically
Floating-point float32, float64 Compared per IEEE 754 standard
Complex complex64, complex128 Equal if both real and imaginary parts are equal
String string Compared lexically byte-wise
Struct struct{...} Comparable if all fields are comparable; equal if all corresponding non-blank fields are equal
Array [n]T Comparable if element type is comparable; equal if all corresponding elements are equal

More here (Go Language Specification - Comparison operators)

Incompatible key types:

Type Comparable ?
Slices ([]T) Not comparable
Maps (map[K]V) Not comparable
Functions (func(...)) Not comparable

Values: Can be any type, including other maps

Search #

func Search(dictionary map[string]string, word string) string {
    return dictionary[word]  // Access value by key
}

Map access: map[key] returns the value for that key.

Custom Type
#

Instead of passing map[string]string everywhere, create a custom type:

type Dictionary map[string]string

func (d Dictionary) Search(word string) string {
    return d[word]
}

// Usage becomes cleaner:
dictionary := Dictionary{"test": "this is just a test"}
result := dictionary.Search("test")

Benefits of custom types:

  • More expressive API
  • Attach methods to the type
  • Hide implementation details

Missing Keys
#

Maps return two values when accessed:

func (d Dictionary) Search(word string) (string, error) {
    definition, ok := d[word]  // ok is true if key exists
    if !ok {
        return "", errors.New("could not find the word you were looking for")
    }
    
    return definition, nil
}

Key insight: value, exists := map[key] is the idiomatic way to check if a key exists.

Adding
#

func (d Dictionary) Add(word, definition string) {
    d[word] = definition  // Simple assignment
}

Map Pointer
#

Important: Maps are reference types - you can modify them without passing pointers:

func modifyMap(m map[string]string) {
    m["new"] = "value"  // This modifies the original map
}

func main() {
    myMap := map[string]string{"existing": "value"}
    modifyMap(myMap)
    fmt.Println(myMap) // Prints: map[existing:value new:value]
}

Why? Map values are pointers to the underlying hash table structure. So no need to pass them as * pointers explicitly

Nil vs Empty Maps
#

Dangerous: var m map[string]string creates a nil map (panic on write)

Safe initialization:

// Method 1: literal syntax
var dictionary = map[string]string{}

// Method 2: make function
var dictionary = make(map[string]string)

// Both create an empty, writeable map

Error Comparison
#

var ErrWordExists = errors.New("cannot add word because it already exists")

func (d Dictionary) Add(word, definition string) error {
    _, err := d.Search(word)
    
    switch err {
    case ErrNotFound:
        d[word] = definition
        return nil
    case nil:
        return ErrWordExists  // Word already exists
    default:
        return err  // Unexpected error
    }
}

Switch on errors provides clean error handling with safety nets.

errors.Is
#

Important: For production code, use errors.Is instead of direct error comparison:

import "errors"

func (d Dictionary) Add(word, definition string) error {
    _, err := d.Search(word)
    
    switch {
    case errors.Is(err, ErrNotFound):
        d[word] = definition
        return nil
    case err == nil:
        return ErrWordExists  // Word already exists
    default:
        return err  // Unexpected error
    }
}

Why errors.Is is better:

  1. Wrapped error support - Works with fmt.Errorf("context: %w", err) and error wrapping
  2. Future-proof - Handles custom error types that implement Is(error) bool method
  3. Chain traversal - Automatically unwraps error chains to find the target error

Example of why direct comparison fails:

baseErr := ErrNotFound
wrappedErr := fmt.Errorf("database error: %w", baseErr)

// Direct comparison fails
if wrappedErr == ErrNotFound { // false - different pointers
    // This code never runs
}

// errors.Is succeeds  
if errors.Is(wrappedErr, ErrNotFound) { // true - unwraps and finds ErrNotFound
    // This code runs correctly
}

Custom Error Types
#

For better error handling, create custom error types:

type DictionaryErr string

// Error implements the built-in error interface
func (e DictionaryErr) Error() string {
    return string(e)
}

const (
    ErrNotFound = DictionaryErr(
        "could not find the word you were looking for")
    ErrWordExists = DictionaryErr(
        "cannot add word because it already exists")
    ErrWordDoesNotExist = DictionaryErr(
        "cannot update word because it does not exist")
)

Updates
#

func (d Dictionary) Update(word, definition string) error {
    _, err := d.Search(word)
    
    switch err {
    case ErrNotFound:
        return ErrWordDoesNotExist  // Can't update non-existent word
    case nil:
        d[word] = definition  // Word exists, update it
        return nil
    default:
        return err
    }
}

Deletions
#

Go provides a built-in delete function:

func (d Dictionary) Delete(word string) error {
    _, err := d.Search(word)
    
    switch err {
    case ErrNotFound:
        return ErrWordDoesNotExist
    case nil:
        delete(d, word)  // Built-in delete function
        return nil
    default:
        return err
    }
}

Delete function: delete(map, key) removes the key-value pair.

Operations Summary
#

Operation Syntax Notes
Create map[string]string{} or make(map[string]string) Use these, not var m map[string]string
Read value, exists := m[key] Check exists to avoid zero values
Write m[key] = value Creates or updates
Delete delete(m, key) Safe to call on non-existent keys
Length len(m) Number of key-value pairs