← Back to Index

Chapter 2: Basic Syntax

Variables & types, control flow & error handling

1. Variables & Types

Declaration

package main

import "fmt"

func main() {
    // var keyword with explicit type
    var name string = "Alice"
    var age int = 30

    // Type inference
    var score = 95.5

    // Short declaration (most common, only inside functions)
    city := "Tokyo"
    isActive := true

    fmt.Println(name, age, score, city, isActive)
}

Basic Types

Type Examples Zero Value
bool true, false false
int, int8/16/32/64 42, -7 0
float32, float64 3.14, 2.718 0
string "hello" ""
byte (uint8) 'A' 0
rune (int32) 'δΈ­' (Unicode code point) 0

Type Conversion & Constants

// Go requires explicit type conversion (no implicit casting)
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

// Constants
const Pi = 3.14159
const (
    StatusOK    = 200
    StatusError = 500
)

// iota: auto-incrementing constant generator
const (
    Sunday = iota  // 0
    Monday         // 1
    Tuesday        // 2
)

πŸ’‘ Go has zero values: every variable is initialized to its type's zero value if not explicitly assigned. No "undefined" or "null" surprises.

2. Strings

package main

import (
    "fmt"
    "strings"
)

func main() {
    // Strings are immutable UTF-8 byte sequences
    s := "Hello, World!"

    // String operations via strings package
    fmt.Println(strings.ToUpper(s))          // "HELLO, WORLD!"
    fmt.Println(strings.Contains(s, "World")) // true
    fmt.Println(strings.Split("a,b,c", ",")) // [a b c]
    fmt.Println(strings.Join([]string{"a", "b"}, "-")) // "a-b"

    // Formatting with fmt.Sprintf
    name := "Go"
    version := 1.24
    msg := fmt.Sprintf("%s version %.2f", name, version)
    fmt.Println(msg) // "Go version 1.24"

    // Raw strings (backticks) β€” no escape processing
    raw := `Line 1\nStill line 1
Line 2`
    fmt.Println(raw)

    // rune vs byte
    chinese := "δ½ ε₯½"
    fmt.Println(len(chinese))         // 6 (bytes)
    fmt.Println(len([]rune(chinese))) // 2 (characters)

    // Iterate by rune
    for i, r := range chinese {
        fmt.Printf("index=%d rune=%c\n", i, r)
    }
}

πŸ’‘ len(s) returns byte count. For character count, use len([]rune(s)) or utf8.RuneCountInString(s).

3. Arrays & Slices

package main

import "fmt"

func main() {
    // Array: fixed size, rarely used directly
    var arr [3]int = [3]int{1, 2, 3}
    fmt.Println(arr) // [1 2 3]

    // Slice: dynamic, backed by an array (this is what you'll use)
    nums := []int{10, 20, 30}
    fmt.Println(nums)        // [10 20 30]
    fmt.Println(len(nums))   // 3 (length)
    fmt.Println(cap(nums))   // 3 (capacity)

    // make: create slice with length and capacity
    s := make([]int, 0, 10)  // len=0, cap=10

    // append: add elements (may allocate new backing array)
    s = append(s, 1, 2, 3)
    fmt.Println(s) // [1 2 3]

    // Slice operations (half-open interval)
    data := []int{0, 1, 2, 3, 4, 5}
    fmt.Println(data[1:4])  // [1 2 3]
    fmt.Println(data[:3])   // [0 1 2]
    fmt.Println(data[3:])   // [3 4 5]

    // Copy
    dst := make([]int, len(data))
    copy(dst, data)
}

πŸ’‘ Slices are references to underlying arrays. Modifying a sub-slice affects the original. Use copy() when you need an independent copy.

4. Maps

package main

import "fmt"

func main() {
    // Declaration and initialization
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
    }

    // Create with make
    ages := make(map[string]int)

    // CRUD operations
    ages["Alice"] = 30          // Create / Update
    age := ages["Alice"]        // Read
    delete(ages, "Alice")       // Delete

    // Comma-ok pattern: check if key exists
    val, ok := scores["Charlie"]
    if !ok {
        fmt.Println("Charlie not found, val =", val) // val is 0 (zero value)
    }

    // Iteration (order is NOT guaranteed)
    for name, score := range scores {
        fmt.Printf("%s: %d\n", name, score)
    }

    // Length
    fmt.Println(len(scores)) // 2
}

πŸ”„ Map Comparison

Python: dict  |  JS: Map / Object  |  Java: HashMap  |  Go: map[K]V

5. Control Flow

if / else

// Standard if/else
if x > 10 {
    fmt.Println("big")
} else if x > 5 {
    fmt.Println("medium")
} else {
    fmt.Println("small")
}

// if with init statement (variable scoped to if block)
if err := doSomething(); err != nil {
    fmt.Println("error:", err)
}

for (the only loop in Go)

// Classic for loop
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// While-style
n := 1
for n < 100 {
    n *= 2
}

// Infinite loop
for {
    // break to exit
    break
}

// range: iterate over slices, maps, strings, channels
fruits := []string{"apple", "banana", "cherry"}
for index, value := range fruits {
    fmt.Printf("%d: %s\n", index, value)
}

// Ignore index with _
for _, fruit := range fruits {
    fmt.Println(fruit)
}

switch (no break needed)

// switch: cases don't fall through by default
day := "Monday"
switch day {
case "Monday", "Tuesday":
    fmt.Println("Early week")
case "Friday":
    fmt.Println("TGIF!")
default:
    fmt.Println("Other day")
}

// Type switch
var val interface{} = 42
switch v := val.(type) {
case int:
    fmt.Println("int:", v)
case string:
    fmt.Println("string:", v)
}

defer

func readFile() {
    f, err := os.Open("data.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close() // Runs when function returns (LIFO order)

    // ... use f ...
}

πŸ’‘ defer is like finally in Java or with in Python β€” it guarantees cleanup even if the function panics.

6. Error Handling

Go uses explicit error returns instead of try/catch exceptions. The error interface is central to Go.

package main

import (
    "errors"
    "fmt"
    "strconv"
)

// Functions return (result, error)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Custom error with fmt.Errorf (wrapping)
func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("parseAge(%q): %w", s, err)
    }
    if age < 0 || age > 150 {
        return 0, fmt.Errorf("age %d out of range", age)
    }
    return age, nil
}

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}

func main() {
    // Always check errors
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(result)

    // Unwrap errors with errors.Is / errors.As
    _, err = parseAge("abc")
    var numErr *strconv.NumError
    if errors.As(err, &numErr) {
        fmt.Println("Number error:", numErr)
    }
}

πŸ”„ Error Handling: Go vs try/catch

Java/Python/JS: try { ... } catch (Error e) { ... }

Go: result, err := doWork(); if err != nil { ... }

Go's approach is verbose but explicit β€” you always know which function can fail and must handle the error.

πŸ“‹ Chapter Summary

Short Declaration :=

Most common way to declare variables inside functions. Type is inferred automatically.

Zero Values

Every type has a zero value: 0, "", false, nil. No uninitialized variables.

Slices over Arrays

Use slices ([]T) for dynamic collections. Arrays ([N]T) are fixed-size and rarely used directly.

for is the Only Loop

No while or do-while. for handles all looping patterns including range iteration.

Comma-ok Pattern

val, ok := m[key] safely checks map access. A core Go idiom.

Explicit Errors

Return (result, error) and check with if err != nil. No hidden exceptions.