Go for Infrastructure Engineers›02 · Go through Bash and Python eyes

Lesson 02 of 9 · Part 1 — The language

Go through Bash and Python eyes

Everyday Go for people who know Bash and Python: variables and types, structs instead of dicts, slices and maps, loops, methods, pointers when they matter, and interfaces, with a side-by-side comparison.

Beginner
Key wordsvariablestypesstructsslicesmapsrangemethodspointersinterfacesstruct tagszero valuesPython to Go

Side by side

Idea Bash Python Go
Variable n=3 n = 3 n := 3 (type inferred, fixed)
List arr=(a b) ["a", "b"] []string{"a", "b"} (slice)
Dict declare -A m {"a": 1} map[string]int{"a": 1}
Record — dict / dataclass struct
Loop for x in "${arr[@]}" for x in arr: for _, x := range arr {}
Function f() { … } def f(a): func f(a string) (int, error) {}
Error exit code, set -e exception returned error value (lesson 03)
Null empty string None zero values, nil for pointers, slices, maps

In Python, a box can hold anything and you find out what's inside when you open it. In Go, every box has a label saying what fits in it: a box labelled "number" can't hold a word. It feels strict at first, and then it saves you from the 3 a.m. surprise.

Structs instead of dicts

package main

import "fmt"

type Node struct {
    Name   string
    Zone   string
    Ready  bool
    Pods   int
    Labels map[string]string
}

func main() {
    nodes := []Node{
        {Name: "n1", Zone: "a", Ready: true, Pods: 42},
        {Name: "n2", Zone: "b", Ready: false, Pods: 0},
    }
    notReady := 0
    for _, n := range nodes {
        if !n.Ready {
            notReady++
            fmt.Printf("%s in zone %s is NotReady\n", n.Name, n.Zone)
        }
    }
    fmt.Println("not ready:", notReady)
}
  • Fields with a capital letter are exported (visible outside the package); lower-case is private. The same rule applies to functions and types.
  • range gives a copy of each element; to change elements in place, index them: nodes[i].Ready = true.

Slices and maps

package main

import (
    "fmt"
    "sort"
)

func main() {
    pods := []string{"web-1", "web-2", "db-0"}
    pods = append(pods, "cache-0")
    fmt.Println(len(pods), pods[0], pods[1:3]) // 4 web-1 [web-2 db-0]

    perZone := map[string]int{}
    for _, z := range []string{"a", "b", "a"} {
        perZone[z]++ // missing keys start at the zero value, 0
    }
    if n, ok := perZone["c"]; !ok {
        fmt.Println("no nodes in zone c", n)
    }

    zones := make([]string, 0, len(perZone))
    for z := range perZone {
        zones = append(zones, z)
    }
    sort.Strings(zones) // map order is random; sort when output order matters
    fmt.Println(zones)
}

Writing to a nil map panics; create maps with map[...]...{} or make. A nil slice is fine to append to.

Methods and pointers

package main

import "fmt"

type Pool struct {
    Name  string
    Nodes int
}

// Value receiver: reads only.
func (p Pool) String() string { return fmt.Sprintf("%s (%d nodes)", p.Name, p.Nodes) }

// Pointer receiver: changes the pool.
func (p *Pool) Scale(n int) { p.Nodes = n }

func main() {
    p := Pool{Name: "apps", Nodes: 3}
    p.Scale(5) // Go takes &p for you
    fmt.Println(p) // apps (5 nodes): Println uses String()
}

Rule of thumb: if any method needs a pointer receiver, give all of that type's methods pointer receivers.

Interfaces: behaviour, not inheritance

An interface lists methods; any type with those methods satisfies it, without declaring it:

package main

import "fmt"

type Checker interface {
    Name() string
    Check() error
}

type DiskCheck struct{ UsedPct int }

func (d DiskCheck) Name() string { return "disk" }
func (d DiskCheck) Check() error {
    if d.UsedPct > 85 {
        return fmt.Errorf("disk %d%% used", d.UsedPct)
    }
    return nil
}

func runAll(checks []Checker) {
    for _, c := range checks {
        if err := c.Check(); err != nil {
            fmt.Printf("FAIL %s: %v\n", c.Name(), err)
            continue
        }
        fmt.Printf("ok   %s\n", c.Name())
    }
}

func main() {
    runAll([]Checker{DiskCheck{UsedPct: 40}, DiskCheck{UsedPct: 91}})
}

Small interfaces (one or two methods, like io.Reader or fmt.Stringer) are the Go way; they make code easy to test with fake implementations.

Struct tags and JSON

type Cluster struct {
    Name    string `json:"name"`
    Version string `json:"version"`
    Nodes   int    `json:"nodeCount,omitempty"`
}

Tags tell encoding/json (and YAML libraries) how to map fields; lesson 05 uses them to decode API responses.

Try it: a tiny inventory

  1. Define Node with name, zone, ready and pods; build a slice of five nodes.
  2. Count Ready nodes per zone in a map[string]int and print the zones sorted.
  3. Add a method (n Node) Healthy() bool and a pointer method (n *Node) Cordon() that sets a field.
  4. Write a Checker interface with two implementations and run both through one function.
  5. Marshal the slice to JSON with json.MarshalIndent and print it.

Going deeper: generics, briefly

Go has generics since 1.18: func Map[T, U any](in []T, f func(T) U) []U. The standard slices and maps packages (slices.Sort, slices.Contains, maps.Keys) cover most everyday needs; write your own generic helpers rarely.

Recap

  • Structs replace fixed-key dicts; capitalised names are exported.
  • Slices grow with append; maps need creating before writing; map order is random.
  • Methods attach behaviour; use pointer receivers to modify or avoid copies.
  • Interfaces are satisfied implicitly; keep them small.
  • Struct tags map fields to JSON and YAML.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.