Skip to content
8 min read·Lesson 2 of 8

Syntax, Types, and Idioms

The fundamentals: packages, variables, types, structs, slices, maps, control flow, and the idioms experienced Go programmers expect.

Go's syntax is small enough that a one-page cheat sheet covers most of it. This lesson is that cheat sheet with the idioms experienced Go programmers expect — the things that make code feel "Go-y" rather than "Java written in Go."

Hello, World

package main

import "fmt"

func main() {
    fmt.Println("Hello, cloud.")
}

Every file declares a package. main is special — it produces an executable. Other packages produce libraries. Run it with go run main.go or build with go build.

Variables and Types

var name string = "kube"      // explicit type
var count = 3                  // type inferred
port := 8080                   // short form, only inside functions

const MaxRetries = 5           // constant; compile-time
const (
    StatusPending  = "pending"
    StatusRunning  = "running"
    StatusComplete = "complete"
)

Built-in scalar types: bool, string, int/int32/int64, uint, float32/float64, byte (alias for uint8), rune (alias for int32, used for unicode code points). error is also built in but is an interface — covered next lesson.

Zero Values

Every variable is initialised to a zero value automatically — 0 for numbers, "" for strings, false for bools, nil for pointers / slices / maps / interfaces / channels. There is no "undefined."

Functions

func add(a, b int) int {
    return a + b
}

// Multiple return values — the idiomatic way to return errors
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("divide by zero")
    }
    return a / b, nil
}

// Named return values
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

Slices and Maps

The two collection types you use every day.

// Slice — dynamically sized array
items := []string{"a", "b", "c"}
items = append(items, "d")
for i, v := range items {
    fmt.Println(i, v)
}
sub := items[1:3]       // ["b", "c"]
make([]int, 5, 10)      // length 5, capacity 10

// Map — hash table
ages := map[string]int{
    "alice": 30,
    "bob":   25,
}
ages["carol"] = 28
age, ok := ages["dave"]  // ok=false if missing
delete(ages, "alice")
for k, v := range ages {
    fmt.Println(k, v)
}

Slices and maps are both reference types — passing them to a function shares the underlying data. Mutations are visible to the caller.

Structs

The main data-modelling construct.

type Pod struct {
    Name      string
    Namespace string
    Image     string
    Replicas  int
}

p := Pod{Name: "web", Namespace: "default", Image: "nginx", Replicas: 3}
p2 := Pod{"web", "default", "nginx", 3}   // positional, discouraged
fmt.Println(p.Name)
p.Replicas = 5                              // direct field access; no getters

// Pointer
ptr := &p
ptr.Image = "nginx:1.27"   // auto-dereference

Exported (visible to other packages) identifiers start with an uppercase letter; unexported with lowercase. This is the entire visibility model — there is no public / private keyword.

Methods

Methods are functions with a receiver:

func (p Pod) FullName() string {
    return p.Namespace + "/" + p.Name
}

// Pointer receiver — required if you want to mutate p
func (p *Pod) Scale(replicas int) {
    p.Replicas = replicas
}

p.Scale(10)         // Go auto-takes the address
fmt.Println(p.FullName())

Convention: if any method on a type has a pointer receiver, all of them should. Mixing is allowed but confusing.

Control Flow

// if can have an initialiser — useful for error handling
if err := doThing(); err != nil {
    return err
}

// for is the only loop
for i := 0; i < 10; i++ { ... }
for x < 100 { x *= 2 }                  // like 'while'
for { /* infinite */ break }

// switch — no fallthrough by default
switch status {
case "pending":
    // ...
case "running", "complete":
    // ...
default:
    // ...
}

// Switch with no condition — a clean replacement for long if/else
switch {
case x < 0:
    return "negative"
case x == 0:
    return "zero"
default:
    return "positive"
}

Packages and Imports

One folder = one package. Files in the same folder share the same package name. Imports are paths from the module root:

package handlers

import (
    "encoding/json"
    "fmt"
    "net/http"

    "github.com/acme/myapp/internal/store"
)

Standard-library imports first, blank line, then third-party. goimports enforces this automatically.

The Idioms

  • Short variable names in small scopes: i for loops, r for reader, w for writer, ctx for context. Long names for package-level things.
  • Accept interfaces, return structs. Functions take the smallest interface they need; they return concrete types.
  • Don't pre-allocate optimism. Don't make([]int, 0, n) unless you know n.
  • Handle errors immediately. Don't accumulate; check at each call.
  • One way to do things. Use the standard library when it covers the case. Don't reach for a framework.

What's Missing (Deliberately)

  • No classes — only structs + methods
  • No inheritance — only struct embedding (composition)
  • No exceptions — only multi-return errors and panic / recover (used sparingly)
  • No generics until 1.18 — most ecosystem code still uses concrete types or interface{}
  • No while, no do...while, no ternary ?:
  • No optional / default parameters — use config structs or option-functions

These omissions are the language's design philosophy: less is more. Once you stop fighting them, productivity climbs.

Key Takeaways

  • A Go program is a set of packages; main is the entry point.
  • Types are explicit; the := short declaration infers, var is the long form.
  • Slices and maps are the workhorse data structures — both reference types.
  • gofmt is non-negotiable; there is one canonical layout.
  • Composition over inheritance — Go has no classes, only structs and interfaces.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →