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.
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.
rangegives 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
- Define
Nodewith name, zone, ready and pods; build a slice of five nodes. - Count Ready nodes per zone in a
map[string]intand print the zones sorted. - Add a method
(n Node) Healthy() booland a pointer method(n *Node) Cordon()that sets a field. - Write a
Checkerinterface with two implementations and run both through one function. - Marshal the slice to JSON with
json.MarshalIndentand 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.