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:
ifor loops,rfor reader,wfor writer,ctxfor 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 known. - 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, nodo...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.