</>
CodeLearn Pro
Start Learning Free โ†’

Go (Golang) โ€” Complete Beginner Tutorial

6 modules ยท 17 examples ยท Click โ–ถ Run on any example to see the output

โœฆ Beginner
โ† Overview

๐Ÿ’ก How to use this page

Read the explanation, study the code, then hit โ–ถ Run to see the output.

๐Ÿ“ฆ
Module 01

Variables & Data Types

Every Go program starts with "package main" and a main() function โ€” that is where execution begins. Go is statically typed, meaning every variable has a fixed type. You can declare types explicitly with var, or let Go figure it out using the := shorthand (called short variable declaration).

Variables โ€” var and :=

In Go there are two ways to create variables. The var keyword is explicit โ€” you can use it anywhere including outside functions. The := shorthand is faster but only works inside functions. Go never lets you declare a variable without using it โ€” unused variables are a compile error.

main.goGo
package main

import "fmt"

func main() {
    // var keyword โ€” explicit type declaration
    var name string = "Alice"
    var age  int    = 25
    var pi   float64 = 3.14159

    // := shorthand โ€” Go figures out the type
    city    := "Cape Town"
    score   := 95
    passing := true

    // Print them
    fmt.Println(name, age, city)
    fmt.Println("Score:", score)
    fmt.Println("Passing:", passing)
    fmt.Println("Pi:", pi)

    // var block โ€” declare many at once
    var (
        firstName = "Bob"
        lastName  = "Smith"
        yearBorn  = 1999
    )
    fmt.Println(firstName, lastName, yearBorn)
}
$ go run main.go
๐Ÿ’ก

Go will refuse to compile if you declare a variable and never use it. This forces clean code โ€” no mystery variables sitting unused. If you genuinely need to ignore a value, use the blank identifier: _ = someValue.

Data Types & fmt Formatting

Go has clear, predictable types. int for whole numbers, float64 for decimals, string for text, bool for true/false. The fmt package handles all printing โ€” fmt.Println adds a newline, fmt.Printf lets you format with verbs like %s (string), %d (integer), %f (float), %v (any value), %T (type).

main.goGo
package main

import "fmt"

func main() {
    var (
        playerName string  = "Alice"
        lives      int     = 3
        speed      float64 = 9.8
        isActive   bool    = true
    )

    // fmt.Println โ€” simple print with newline
    fmt.Println("Player:", playerName)

    // fmt.Printf โ€” formatted print with verbs
    fmt.Printf("Name:    %s\n", playerName)
    fmt.Printf("Lives:   %d\n", lives)
    fmt.Printf("Speed:   %.2f m/s\n", speed)  // 2 decimal places
    fmt.Printf("Active:  %v\n", isActive)
    fmt.Printf("Type of lives: %T\n", lives)

    // fmt.Sprintf โ€” format into a string (don't print)
    summary := fmt.Sprintf("%s has %d lives", playerName, lives)
    fmt.Println(summary)

    // Constants โ€” values that never change
    const maxLives = 5
    const gravity  = 9.81
    fmt.Printf("Max lives: %d, Gravity: %.2f\n", maxLives, gravity)
}
$ go run main.go
๐Ÿ’ก

%v is the "default format" verb โ€” it works for any type. When in doubt, use %v. Use %+v on a struct to also print field names. Use %#v to print Go syntax representation.

Strings & Basic Operations

Strings in Go are sequences of bytes enclosed in double quotes. Go has a strings package with many helpful functions, and the + operator joins strings. The len() function returns the number of bytes (not always characters for Unicode).

main.goGo
package main

import (
    "fmt"
    "strings"
)

func main() {
    name := "Alice Smith"
    lang := "Go"

    // Joining strings with +
    greeting := "Hello, " + name + "!"
    fmt.Println(greeting)

    // String length
    fmt.Println("Length:", len(name))

    // strings package functions
    fmt.Println(strings.ToUpper(name))
    fmt.Println(strings.ToLower(name))
    fmt.Println(strings.Contains(name, "Alice"))
    fmt.Println(strings.HasPrefix(name, "Ali"))
    fmt.Println(strings.HasSuffix(name, "Smith"))
    fmt.Println(strings.Replace(name, "Alice", "Bob", 1))
    fmt.Println(strings.TrimSpace("  hello  "))

    // Multi-line string with backticks
    message := `Hello from Go!
This spans multiple lines.
No need for escape characters.`
    fmt.Println(message)
}
$ go run main.go
๐Ÿ’ก

Use backtick strings (raw string literals) when your text spans multiple lines or contains backslashes and quotes. They are like Python's triple-quoted strings โ€” no escape sequences needed inside.

โš–๏ธ
Module 02

Control Flow

Go has if/else for decisions, a single for keyword for ALL loops (no while keyword!), and switch for multiple conditions. Go also has a unique defer keyword that delays a function call until the surrounding function returns โ€” very handy for cleanup.

if / else if / else

Go if statements do not need parentheses around the condition โ€” that is intentional. The curly braces are always required. A unique Go feature: you can add a short statement before the condition, scoping a variable to just the if block.

main.goGo
package main

import "fmt"

func main() {
    score := 73

    // Basic if / else if / else
    if score >= 80 {
        fmt.Println("Grade: A")
    } else if score >= 70 {
        fmt.Println("Grade: B")
    } else if score >= 60 {
        fmt.Println("Grade: C")
    } else if score >= 50 {
        fmt.Println("Grade: D")
    } else {
        fmt.Println("Grade: F")
    }

    // if with an init statement โ€” x only exists inside this if block
    if x := score * 2; x > 100 {
        fmt.Printf("Double score %d is over 100!\n", x)
    } else {
        fmt.Printf("Double score is %d\n", x)
    }

    // Checking errors (the most common Go pattern)
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}
$ go run main.go
๐Ÿ’ก

The "if err != nil" pattern is everywhere in Go. Go does not have exceptions โ€” errors are just values returned from functions. Always check your errors โ€” ignoring them is the Go equivalent of catching and swallowing exceptions.

for โ€” Go's Only Loop

Go has exactly ONE loop keyword: for. But it does the job of for, while, and do-while from other languages. Use it with three parts (classic C-style), with just a condition (like while), or with range to loop over collections.

main.goGo
package main

import "fmt"

func main() {
    // Classic for loop (like C)
    for i := 1; i <= 5; i++ {
        fmt.Printf("Count: %d\n", i)
    }

    fmt.Println("---")

    // "while" style โ€” just the condition
    n := 1
    for n < 100 {
        n *= 2
    }
    fmt.Println("First power of 2 over 100:", n)

    fmt.Println("---")

    // range โ€” loop over a slice
    fruits := []string{"apple", "banana", "cherry"}
    for index, fruit := range fruits {
        fmt.Printf("%d: %s\n", index, fruit)
    }

    fmt.Println("---")

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

    fmt.Println("---")

    // break and continue
    for i := 0; i < 10; i++ {
        if i == 7 {
            break
        }
        if i%2 == 0 {
            continue
        }
        fmt.Print(i, " ")
    }
    fmt.Println()
}
$ go run main.go
๐Ÿ’ก

The blank identifier _ discards a value Go would otherwise force you to use. "for _, fruit := range fruits" says "I want the value but not the index". You will see _ constantly in real Go code.

switch & defer

Go switch does not fall through by default (no break needed!). It also supports expressions, multiple values per case, and no condition at all (like a cleaner if/else chain). defer runs a statement when the surrounding function exits โ€” perfect for cleanup like closing files.

main.goGo
package main

import "fmt"

func main() {
    // switch โ€” no break needed, no fallthrough by default
    day := "Wednesday"

    switch day {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
        fmt.Println(day, "is a weekday")
    case "Saturday", "Sunday":
        fmt.Println(day, "is the weekend!")
    default:
        fmt.Println("Unknown day")
    }

    // switch with no condition (like if/else chain)
    score := 82
    switch {
    case score >= 90:
        fmt.Println("A โ€” Excellent")
    case score >= 80:
        fmt.Println("B โ€” Good")
    case score >= 70:
        fmt.Println("C โ€” Average")
    default:
        fmt.Println("Below average")
    }

    // defer โ€” runs LAST, when the function exits
    fmt.Println("Start of work")
    defer fmt.Println("Cleanup: closing resources") // runs at end
    defer fmt.Println("Cleanup: saving state")       // runs before above
    fmt.Println("Doing work...")
    fmt.Println("Finishing work")
    // deferred calls run here, in LIFO order
}
$ go run main.go
๐Ÿ’ก

defer calls stack up in LIFO order (last in, first out) โ€” like a stack of plates. The most common real-world use: defer file.Close() right after you open a file, so you never forget to close it no matter how the function exits.

โš™๏ธ
Module 03

Functions

Functions in Go are first-class citizens. Go's most distinctive feature is multiple return values โ€” a function can return both a result AND an error, which is the foundation of Go's error handling. Functions can also be assigned to variables and passed around.

Defining Functions

The func keyword defines a function. Parameter types come AFTER the name (unlike C/Java). The return type comes after the parameters. If multiple parameters share the same type, you can list them together.

main.goGo
package main

import "fmt"

// Basic function โ€” no return value
func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

// Function with a return value
func square(n int) int {
    return n * n
}

// Multiple parameters of the same type โ€” shorthand
func add(a, b int) int {
    return a + b
}

// Multiple return values โ€” Go's superpower
func minMax(nums []int) (int, int) {
    min, max := nums[0], nums[0]
    for _, n := range nums {
        if n < min { min = n }
        if n > max { max = n }
    }
    return min, max
}

func main() {
    greet("Alice")
    greet("Bob")

    fmt.Println("5ยฒ =", square(5))
    fmt.Println("3 + 7 =", add(3, 7))

    nums := []int{4, 7, 1, 9, 3, 6}
    min, max := minMax(nums)
    fmt.Printf("Min: %d, Max: %d\n", min, max)
}
$ go run main.go
๐Ÿ’ก

Multiple return values are how Go handles errors cleanly. The convention is always: (result, error). The caller must deal with both โ€” this makes error handling impossible to accidentally skip.

Variadic Functions & First-Class Functions

A variadic function accepts any number of arguments using ... before the type. Functions can be stored in variables, passed as arguments, and returned from other functions โ€” they are first-class values in Go.

main.goGo
package main

import "fmt"

// Variadic function โ€” accepts any number of ints
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// Function as a variable (function type)
func apply(nums []int, fn func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = fn(n)
    }
    return result
}

func main() {
    // Variadic calls
    fmt.Println(sum(1, 2, 3))
    fmt.Println(sum(10, 20, 30, 40))
    fmt.Println(sum(100))

    // Spread a slice into a variadic function
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(sum(numbers...))

    // Anonymous function (lambda)
    double := func(n int) int { return n * 2 }
    fmt.Println(double(7))

    // Pass a function as an argument
    doubled := apply(numbers, func(n int) int { return n * 2 })
    fmt.Println(doubled)

    squared := apply(numbers, func(n int) int { return n * n })
    fmt.Println(squared)
}
$ go run main.go
๐Ÿ’ก

To pass a slice to a variadic function, append ... after the slice name: sum(numbers...). Without the ..., Go would try to pass the whole slice as a single argument, which is a type mismatch.

๐Ÿ—๏ธ
Module 04

Structs & Methods

Go does not have classes โ€” it uses structs to group related data together. Methods are functions attached to a struct. This gives you the benefits of object-oriented programming (organising code around data) without the complexity of inheritance.

Defining Structs & Creating Instances

A struct is a collection of fields, each with a name and type. Create an instance by listing field values, or use field names for clarity (recommended). Access fields with the dot (.) operator.

main.goGo
package main

import "fmt"

// Define a struct
type Student struct {
    Name  string
    Age   int
    Grade string
    Score float64
}

// Another struct
type Address struct {
    Street string
    City   string
    Code   string
}

// Struct with nested struct
type Teacher struct {
    Name    string
    Subject string
    Address Address   // embedded struct
}

func main() {
    // Create instances
    alice := Student{
        Name:  "Alice",
        Age:   20,
        Grade: "A",
        Score: 95.5,
    }

    bob := Student{"Bob", 18, "B", 82.0} // positional (avoid!)

    fmt.Printf("Name: %s, Score: %.1f, Grade: %s\n",
        alice.Name, alice.Score, alice.Grade)

    // Modify a field
    bob.Score = 85.0
    fmt.Printf("Bob updated: %.1f\n", bob.Score)

    // Nested struct access
    teacher := Teacher{
        Name:    "Mr Adams",
        Subject: "Computer Science",
        Address: Address{
            Street: "123 Main St",
            City:   "Cape Town",
            Code:   "8001",
        },
    }
    fmt.Printf("%s lives in %s\n", teacher.Name, teacher.Address.City)
}
$ go run main.go
๐Ÿ’ก

Always use named fields when creating structs: Student{Name: "Alice", Age: 20}. Positional struct literals (Student{"Alice", 20}) break if someone adds a field to the struct in the middle โ€” a painful bug to track down.

Methods on Structs

A method is a function with a "receiver" โ€” the struct it belongs to. Use a pointer receiver (*Student) when you want to modify the struct. Use a value receiver (Student) when you only need to read. By convention, keep them consistent across a type.

main.goGo
package main

import "fmt"

type Student struct {
    Name  string
    Score float64
}

// Value receiver โ€” reads only, does not modify
func (s Student) Grade() string {
    switch {
    case s.Score >= 90:
        return "A"
    case s.Score >= 80:
        return "B"
    case s.Score >= 70:
        return "C"
    case s.Score >= 60:
        return "D"
    default:
        return "F"
    }
}

// Value receiver โ€” formats the struct as a string
func (s Student) String() string {
    return fmt.Sprintf("%s (%.0f%% โ€” %s)", s.Name, s.Score, s.Grade())
}

// Pointer receiver โ€” modifies the original struct
func (s *Student) AddBonus(points float64) {
    s.Score += points
    if s.Score > 100 {
        s.Score = 100
    }
}

func main() {
    alice := Student{Name: "Alice", Score: 87.5}
    bob   := Student{Name: "Bob",   Score: 62.0}

    fmt.Println(alice.String())
    fmt.Println(bob.String())

    // Pointer receiver modifies alice directly
    alice.AddBonus(5)
    fmt.Println("After bonus:", alice.String())

    bob.AddBonus(10)
    fmt.Println("After bonus:", bob.String())
}
$ go run main.go
๐Ÿ’ก

Use a pointer receiver (*Student) when the method needs to change the struct, or when the struct is large (avoids copying). Use a value receiver (Student) for small read-only methods. Be consistent โ€” if one method uses a pointer receiver, all methods on that type should.

๐Ÿ“‹
Module 05

Arrays, Slices & Maps

Arrays in Go have a fixed size โ€” once created, they cannot grow. Slices are the dynamic, flexible alternative you will use almost always. Maps store key-value pairs โ€” like Python dictionaries or JavaScript objects. These three data structures cover the vast majority of real Go programs.

Slices โ€” Dynamic Arrays

A slice is a view into an underlying array but with dynamic length. Create them with []Type{values} or make([]Type, length). Use append() to add items โ€” it returns a new slice. len() gives the current length, cap() the capacity.

main.goGo
package main

import "fmt"

func main() {
    // Create a slice with initial values
    fruits := []string{"apple", "banana", "cherry"}
    fmt.Println(fruits)
    fmt.Println("Length:", len(fruits))

    // Access by index
    fmt.Println("First:", fruits[0])
    fmt.Println("Last:", fruits[len(fruits)-1])

    // Append โ€” always save the return value!
    fruits = append(fruits, "mango")
    fruits = append(fruits, "kiwi", "grape") // append multiple
    fmt.Println("After append:", fruits)

    // Slicing a slice [start:end] (end is exclusive)
    fmt.Println("First 3:", fruits[:3])
    fmt.Println("Last 2:", fruits[len(fruits)-2:])
    fmt.Println("Middle:", fruits[2:5])

    // make โ€” create a slice with specific length/capacity
    scores := make([]int, 5)  // 5 zeros
    for i := range scores {
        scores[i] = (i + 1) * 10
    }
    fmt.Println("Scores:", scores)

    // Iterate with range
    for i, fruit := range fruits {
        fmt.Printf("  [%d] %s\n", i, fruit)
    }
}
$ go run main.go
๐Ÿ’ก

Never forget to save the result of append: fruits = append(fruits, "mango"). If you write just append(fruits, "mango") without saving, the original slice is unchanged. This is a very common beginner mistake in Go.

Maps โ€” Key-Value Storage

A map stores values indexed by keys. Create with make(map[KeyType]ValueType) or with a map literal. When you look up a key, Go returns TWO values โ€” the value AND a boolean saying whether the key existed. Always check that second value!

main.goGo
package main

import "fmt"

func main() {
    // Create a map with a literal
    grades := map[string]string{
        "Alice":   "A",
        "Bob":     "B",
        "Charlie": "A",
        "Diana":   "C",
    }

    // Access a value
    fmt.Println("Alice's grade:", grades["Alice"])

    // Safe lookup โ€” check if key exists
    grade, ok := grades["Edward"]
    if ok {
        fmt.Println("Edward:", grade)
    } else {
        fmt.Println("Edward not found")
    }

    // Add a new key
    grades["Edward"] = "B"
    fmt.Println("After adding Edward:", grades["Edward"])

    // Delete a key
    delete(grades, "Diana")
    fmt.Println("After deleting Diana, length:", len(grades))

    // Iterate over a map
    fmt.Println("All grades:")
    for name, grade := range grades {
        fmt.Printf("  %s: %s\n", name, grade)
    }

    // Map of slices โ€” a common pattern
    courses := map[string][]string{
        "Alice": {"Maths", "Science", "Go Programming"},
        "Bob":   {"History", "English"},
    }
    fmt.Println("Alice's courses:", courses["Alice"])
}
$ go run main.go
๐Ÿ’ก

Map iteration order in Go is intentionally random โ€” every run may print in a different order. This is by design to prevent programmers from depending on map order. If you need sorted output, extract the keys into a slice and sort it first.

๐Ÿงต
Module 06

Goroutines & Channels

Go's killer feature is built-in concurrency. A goroutine is a lightweight thread โ€” you can run thousands of them cheaply. Channels are typed pipes that goroutines use to communicate safely. Together they make concurrent programming much simpler than in most languages.

Goroutines โ€” go keyword

Add the go keyword before a function call to run it as a goroutine โ€” it starts immediately in the background while your main program continues. The main() function is itself a goroutine. When main() exits, all goroutines stop, so you need to wait for them.

main.goGo
package main

import (
    "fmt"
    "sync"
    "time"
)

func printNumbers(label string) {
    for i := 1; i <= 5; i++ {
        fmt.Printf("[%s] %d\n", label, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // WaitGroup waits for goroutines to finish
    var wg sync.WaitGroup

    // Launch 3 goroutines simultaneously
    wg.Add(3)

    go func() {
        defer wg.Done()
        printNumbers("A")
    }()

    go func() {
        defer wg.Done()
        printNumbers("B")
    }()

    go func() {
        defer wg.Done()
        printNumbers("C")
    }()

    fmt.Println("All goroutines launched โ€” waiting...")
    wg.Wait() // block until all goroutines call Done()
    fmt.Println("All done!")
}
$ go run main.go
๐Ÿ’ก

sync.WaitGroup is the standard way to wait for a collection of goroutines to finish. Add(n) says "I am launching n goroutines". Each goroutine calls Done() when it finishes. Wait() blocks until the counter reaches zero.

Channels โ€” Safe Communication

A channel is a typed pipe between goroutines. Send a value with ch <- value. Receive a value with value := <-ch. Sending and receiving block until the other side is ready โ€” this natural synchronisation prevents race conditions.

main.goGo
package main

import "fmt"

// Worker function sends results down a channel
func double(nums []int, result chan<- int) {
    for _, n := range nums {
        result <- n * 2   // send to channel
    }
    close(result)         // signal: no more values
}

func main() {
    // Buffered channel โ€” holds up to 5 values without blocking
    results := make(chan int, 5)
    numbers := []int{1, 2, 3, 4, 5}

    // Launch goroutine to do the work
    go double(numbers, results)

    // Receive all results from the channel
    for val := range results {
        fmt.Println("Got:", val)
    }

    fmt.Println("---")

    // Simple ping-pong with channels
    ping := make(chan string, 1)
    pong := make(chan string, 1)

    go func() {
        msg := <-ping          // wait for message
        pong <- msg + " pong"  // send response
    }()

    ping <- "ping"
    response := <-pong
    fmt.Println(response)
}
$ go run main.go
๐Ÿ’ก

The Go concurrency mantra: "Do not communicate by sharing memory; instead, share memory by communicating." Channels encourage you to pass data between goroutines rather than letting multiple goroutines read/write the same variable โ€” which would cause race conditions.

๐ŸŽ‰

You finished the Go tutorial!

You now know variables, control flow, functions, structs, slices, maps, goroutines and channels. These are the core building blocks of real Go programs used in production every day.