Back to blog

Go Phase 1: Fundamentals - Installation, Syntax & Data Types

gogolangprogrammingfundamentalsbackend
Go Phase 1: Fundamentals - Installation, Syntax & Data Types

Welcome to Phase 1 of the Go Learning Roadmap! This comprehensive guide covers everything you need to get started with Go programming - from installation to writing your first complete programs.

By the end of this phase, you'll have a solid foundation in Go syntax, data types, control flow, and functions that will prepare you for more advanced topics.

What You'll Learn

✅ Install Go and set up your development environment
✅ Understand Go's project structure and modules
✅ Master variables, constants, and basic data types
✅ Write control flow with if/else, switch, and for loops
✅ Create functions with multiple return values
✅ Work with arrays, slices, and maps
✅ Use pointers to work with memory efficiently


Part 1: Installing Go

macOS Installation

Using Homebrew (Recommended):

# Install Go
brew install go
 
# Verify installation
go version
# go version go1.22.0 darwin/arm64

Using Official Installer:

  1. Download the installer from go.dev/dl
  2. Run the .pkg file
  3. Follow the installation wizard

Linux Installation

# Download the latest version (check go.dev/dl for current version)
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz
 
# Remove old installation and extract
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz
 
# Add to PATH (add to ~/.bashrc or ~/.zshrc)
export PATH=$PATH:/usr/local/go/bin
 
# Reload shell
source ~/.bashrc
 
# Verify
go version

Windows Installation

  1. Download the .msi installer from go.dev/dl
  2. Run the installer
  3. Open a new Command Prompt
  4. Verify with go version

Verify Your Installation

# Check Go version
go version
 
# Check Go environment
go env
 
# Important variables:
# GOROOT - Where Go is installed
# GOPATH - Where your Go code lives (optional with modules)
# GOPROXY - Module proxy server

Part 2: Setting Up Your Development Environment

  1. Install VS Code: Download from code.visualstudio.com

  2. Install Go Extension:

    • Open VS Code
    • Press Cmd+Shift+X (Mac) or Ctrl+Shift+X (Windows/Linux)
    • Search for "Go" by the Go Team at Google
    • Click Install
  3. Install Go Tools:

    • Open Command Palette: Cmd+Shift+P or Ctrl+Shift+P
    • Type "Go: Install/Update Tools"
    • Select all tools and click OK

Essential tools installed:

  • gopls - Language server for autocomplete and intellisense
  • dlv - Debugger
  • gofumpt - Stricter formatter
  • golangci-lint - Linter

GoLand Setup (JetBrains)

If you prefer JetBrains IDEs:

  1. Download GoLand from jetbrains.com/go
  2. Install and open GoLand
  3. Go is automatically configured (no additional setup needed)

Editor Settings for VS Code

Create .vscode/settings.json in your project:

{
  "go.formatTool": "gofumpt",
  "go.lintTool": "golangci-lint",
  "go.useLanguageServer": true,
  "[go]": {
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.organizeImports": "explicit"
    }
  }
}

Part 3: Your First Go Program

Create a New Project

# Create project directory
mkdir hello-go
cd hello-go
 
# Initialize Go module
go mod init hello-go

The go mod init command creates a go.mod file:

module hello-go
 
go 1.22

Write Hello World

Create main.go:

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello, Go!")
}

Understanding the Code:

  • package main - Declares this is an executable program
  • import "fmt" - Imports the format package for printing
  • func main() - Entry point of the program
  • fmt.Println() - Prints text with a newline

Run Your Program

# Run directly (doesn't create binary)
go run main.go
# Output: Hello, Go!
 
# Build binary
go build -o hello
./hello
# Output: Hello, Go!
 
# Build and install to $GOPATH/bin
go install

Go Commands Reference

CommandDescription
go runCompile and run without creating binary
go buildCompile and create binary
go installCompile and install to GOPATH/bin
go fmtFormat source code
go vetReport suspicious code constructs
go testRun tests
go mod initInitialize new module
go mod tidyAdd missing and remove unused modules
go getAdd dependency to current module

Part 4: Variables and Constants

Variable Declaration

Go offers multiple ways to declare variables:

package main
 
import "fmt"
 
func main() {
    // Method 1: var with explicit type
    var name string = "Alice"
    var age int = 30
 
    // Method 2: var with type inference
    var city = "Tokyo"       // inferred as string
    var score = 95.5         // inferred as float64
 
    // Method 3: Short declaration (most common)
    country := "Japan"       // inferred type
    isActive := true         // bool
 
    // Method 4: Multiple variables
    var x, y, z int = 1, 2, 3
    a, b := "hello", 42
 
    // Method 5: var block
    var (
        firstName = "Bob"
        lastName  = "Smith"
        height    = 180
    )
 
    fmt.Println(name, age, city, score, country, isActive)
    fmt.Println(x, y, z, a, b)
    fmt.Println(firstName, lastName, height)
}

Zero Values

Uninitialized variables get their zero value:

package main
 
import "fmt"
 
func main() {
    var i int          // 0
    var f float64      // 0.0
    var s string       // "" (empty string)
    var b bool         // false
    var p *int         // nil (null pointer)
 
    fmt.Printf("int: %d, float: %f, string: %q, bool: %t, pointer: %v\n",
        i, f, s, b, p)
    // int: 0, float: 0.000000, string: "", bool: false, pointer: <nil>
}

Zero values by type:

TypeZero Value
int, int8, int16, int32, int640
uint, uint8, uint16, uint32, uint640
float32, float640.0
boolfalse
string"" (empty)
pointers, slices, maps, channels, interfacesnil

Constants

Constants are immutable values set at compile time:

package main
 
import "fmt"
 
// Package-level constants
const Pi = 3.14159
const AppName = "MyApp"
 
// Constant block
const (
    StatusOK       = 200
    StatusNotFound = 404
    StatusError    = 500
)
 
// iota: auto-incrementing integer
const (
    Sunday    = iota // 0
    Monday           // 1
    Tuesday          // 2
    Wednesday        // 3
    Thursday         // 4
    Friday           // 5
    Saturday         // 6
)
 
// iota with expressions
const (
    _  = iota             // 0 (ignored with blank identifier)
    KB = 1 << (10 * iota) // 1 << 10 = 1024
    MB                    // 1 << 20 = 1048576
    GB                    // 1 << 30 = 1073741824
    TB                    // 1 << 40 = 1099511627776
)
 
func main() {
    fmt.Println("Pi:", Pi)
    fmt.Println("StatusOK:", StatusOK)
    fmt.Println("Monday:", Monday)
    fmt.Println("KB:", KB, "MB:", MB, "GB:", GB)
}

iota is powerful for:

  • Enumerations
  • Bit flags
  • Consecutive integer constants

Part 5: Basic Data Types

Numeric Types

package main
 
import "fmt"
 
func main() {
    // Integers
    var i8 int8 = 127             // -128 to 127
    var i16 int16 = 32767         // -32768 to 32767
    var i32 int32 = 2147483647    // -2B to 2B
    var i64 int64 = 9223372036854775807
 
    var u8 uint8 = 255            // 0 to 255
    var u16 uint16 = 65535        // 0 to 65535
    var u32 uint32 = 4294967295   // 0 to 4B
    var u64 uint64 = 18446744073709551615
 
    // int and uint are platform-dependent (32 or 64 bit)
    var i int = 42
    var u uint = 42
 
    // Floating point
    var f32 float32 = 3.14
    var f64 float64 = 3.14159265358979
 
    // Complex numbers
    var c64 complex64 = 1 + 2i
    var c128 complex128 = 1.5 + 2.5i
 
    fmt.Println(i8, i16, i32, i64)
    fmt.Println(u8, u16, u32, u64)
    fmt.Println(i, u)
    fmt.Println(f32, f64)
    fmt.Println(c64, c128)
}

Type selection guidelines:

  • int: Use for general integers (most common)
  • int64: When you need large numbers or exact size
  • float64: Default for floating point (more precision)
  • uint8 (byte): For byte manipulation
  • int32 (rune): For Unicode code points

Strings and Runes

package main
 
import (
    "fmt"
    "strings"
    "unicode/utf8"
)
 
func main() {
    // Strings are immutable sequences of bytes
    s := "Hello, 世界"
 
    // String length (bytes, not characters!)
    fmt.Println("Byte length:", len(s))          // 13 bytes
    fmt.Println("Rune count:", utf8.RuneCountInString(s)) // 9 characters
 
    // Accessing bytes
    fmt.Printf("First byte: %c\n", s[0])         // H
 
    // Iterating by byte
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Println()
 
    // Iterating by rune (correct for Unicode)
    for i, r := range s {
        fmt.Printf("%d: %c (U+%04X)\n", i, r, r)
    }
 
    // String operations
    fmt.Println(strings.ToUpper(s))
    fmt.Println(strings.Contains(s, "世"))
    fmt.Println(strings.Split("a,b,c", ","))
    fmt.Println(strings.Join([]string{"a", "b", "c"}, "-"))
 
    // Raw strings (no escape sequences)
    raw := `Line 1
Line 2
    Tab preserved`
    fmt.Println(raw)
 
    // Rune type for single characters
    var r rune = ''
    fmt.Printf("Rune: %c, Unicode: U+%04X, Value: %d\n", r, r, r)
}

Key points:

  • Strings are UTF-8 encoded by default
  • len(s) returns bytes, not characters
  • Use range to iterate over runes correctly
  • rune = int32 for Unicode code points
  • byte = uint8 for raw bytes

Booleans

package main
 
import "fmt"
 
func main() {
    var t bool = true
    var f bool = false
 
    // Boolean operators
    fmt.Println("AND:", t && f)      // false
    fmt.Println("OR:", t || f)       // true
    fmt.Println("NOT:", !t)          // false
 
    // Comparison operators return bool
    x, y := 10, 20
    fmt.Println("x == y:", x == y)   // false
    fmt.Println("x != y:", x != y)   // true
    fmt.Println("x < y:", x < y)     // true
    fmt.Println("x >= y:", x >= y)   // false
}

Type Conversion

Go requires explicit type conversion - no implicit casting:

package main
 
import (
    "fmt"
    "strconv"
)
 
func main() {
    // Numeric conversions
    var i int = 42
    var f float64 = float64(i)     // int to float64
    var u uint = uint(i)           // int to uint
 
    fmt.Println(i, f, u)
 
    // String to number
    s := "123"
    n, err := strconv.Atoi(s)      // string to int
    if err != nil {
        fmt.Println("Error:", err)
    }
    fmt.Println("Parsed:", n)
 
    // Number to string
    str := strconv.Itoa(456)       // int to string
    fmt.Println("String:", str)
 
    // Float conversions
    fs := "3.14159"
    fv, _ := strconv.ParseFloat(fs, 64)
    fmt.Println("Float:", fv)
 
    // Bool conversions
    bs := "true"
    bv, _ := strconv.ParseBool(bs)
    fmt.Println("Bool:", bv)
 
    // Using Sprintf for formatting
    formatted := fmt.Sprintf("%d items @ $%.2f", 5, 9.99)
    fmt.Println(formatted) // "5 items @ $9.99"
}

Part 6: Control Flow

If/Else Statements

package main
 
import "fmt"
 
func main() {
    x := 10
 
    // Basic if
    if x > 5 {
        fmt.Println("x is greater than 5")
    }
 
    // If-else
    if x > 15 {
        fmt.Println("x is greater than 15")
    } else {
        fmt.Println("x is not greater than 15")
    }
 
    // If-else if-else
    score := 85
    if score >= 90 {
        fmt.Println("Grade: A")
    } else if score >= 80 {
        fmt.Println("Grade: B")
    } else if score >= 70 {
        fmt.Println("Grade: C")
    } else {
        fmt.Println("Grade: F")
    }
 
    // If with initialization statement (idiomatic Go!)
    if value := compute(); value > 10 {
        fmt.Println("Value is:", value)
        // 'value' is only accessible within this block
    }
 
    // Common pattern: error handling
    if err := doSomething(); err != nil {
        fmt.Println("Error:", err)
        return
    }
}
 
func compute() int {
    return 42
}
 
func doSomething() error {
    return nil // no error
}

Switch Statements

package main
 
import (
    "fmt"
    "time"
)
 
func main() {
    // Basic switch
    day := "Monday"
    switch day {
    case "Monday":
        fmt.Println("Start of work week")
    case "Friday":
        fmt.Println("TGIF!")
    case "Saturday", "Sunday": // Multiple values
        fmt.Println("Weekend!")
    default:
        fmt.Println("Midweek")
    }
 
    // Switch with no condition (cleaner than if-else chains)
    hour := time.Now().Hour()
    switch {
    case hour < 12:
        fmt.Println("Good morning!")
    case hour < 17:
        fmt.Println("Good afternoon!")
    default:
        fmt.Println("Good evening!")
    }
 
    // Switch with initialization
    switch os := "linux"; os {
    case "darwin":
        fmt.Println("macOS")
    case "linux":
        fmt.Println("Linux")
    default:
        fmt.Printf("Unknown: %s\n", os)
    }
 
    // fallthrough (rarely used, must be explicit)
    switch num := 2; num {
    case 1:
        fmt.Println("One")
        fallthrough
    case 2:
        fmt.Println("Two")
        fallthrough
    case 3:
        fmt.Println("Three")
    }
    // Prints: Two, Three
 
    // Type switch
    whatAmI(21)
    whatAmI("hello")
    whatAmI(true)
}
 
func whatAmI(i interface{}) {
    switch t := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", t)
    case string:
        fmt.Printf("String: %s\n", t)
    case bool:
        fmt.Printf("Boolean: %t\n", t)
    default:
        fmt.Printf("Unknown type: %T\n", t)
    }
}

Key differences from other languages:

  • No break needed - cases don't fall through by default
  • Use fallthrough keyword if you need fall-through behavior
  • Switch on any type, not just integers
  • Switch without condition acts like if-else chain

For Loops

Go has only one looping construct: for. It replaces while, do-while, and foreach:

package main
 
import "fmt"
 
func main() {
    // Traditional for loop
    for i := 0; i < 5; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println() // 0 1 2 3 4
 
    // While-style loop
    count := 0
    for count < 3 {
        fmt.Println("Count:", count)
        count++
    }
 
    // Infinite loop
    iteration := 0
    for {
        iteration++
        if iteration > 3 {
            break // Exit the loop
        }
        fmt.Println("Iteration:", iteration)
    }
 
    // Continue and break
    for i := 0; i < 10; i++ {
        if i%2 == 0 {
            continue // Skip even numbers
        }
        if i > 7 {
            break // Stop at 7
        }
        fmt.Print(i, " ")
    }
    fmt.Println() // 1 3 5 7
 
    // Range over slices
    fruits := []string{"apple", "banana", "cherry"}
    for index, fruit := range fruits {
        fmt.Printf("%d: %s\n", index, fruit)
    }
 
    // Range - index only
    for i := range fruits {
        fmt.Println("Index:", i)
    }
 
    // Range - value only (ignore index with blank identifier)
    for _, fruit := range fruits {
        fmt.Println("Fruit:", fruit)
    }
 
    // Range over maps
    ages := map[string]int{"Alice": 30, "Bob": 25}
    for name, age := range ages {
        fmt.Printf("%s is %d years old\n", name, age)
    }
 
    // Range over strings (runes!)
    for i, r := range "Go世界" {
        fmt.Printf("%d: %c\n", i, r)
    }
}

Defer, Panic, and Recover

package main
 
import "fmt"
 
func main() {
    // defer: Schedule function call to run when surrounding function returns
    defer fmt.Println("This prints last")
    fmt.Println("This prints first")
 
    // Multiple defers execute in LIFO order (stack)
    defer fmt.Println("Third (deferred first)")
    defer fmt.Println("Second (deferred second)")
    defer fmt.Println("First (deferred third)")
 
    // Common use: cleanup
    cleanup()
 
    // panic and recover
    safeDivide(10, 2)
    safeDivide(10, 0) // Would panic without recover
    fmt.Println("Program continues after recovery")
}
 
func cleanup() {
    // Simulating file operations
    fmt.Println("Opening file...")
    defer fmt.Println("Closing file...") // Will always run
    fmt.Println("Writing to file...")
    // Even if there's an error, defer runs
}
 
func safeDivide(a, b int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
 
    if b == 0 {
        panic("division by zero!")
    }
    fmt.Println(a / b)
}

defer use cases:

  • Closing files, network connections
  • Unlocking mutexes
  • Cleanup operations
  • Logging function exit
  • Recovering from panics

Part 7: Functions

Basic Functions

package main
 
import "fmt"
 
// Simple function
func greet(name string) {
    fmt.Println("Hello,", name)
}
 
// Function with return value
func add(a, b int) int {
    return a + b
}
 
// Multiple parameters of same type
func multiply(x, y, z int) int {
    return x * y * z
}
 
// Multiple return values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}
 
// Named return values
func rectangle(width, height float64) (area, perimeter float64) {
    area = width * height
    perimeter = 2 * (width + height)
    return // naked return uses named values
}
 
// Named returns with explicit return (preferred for clarity)
func rectangleExplicit(width, height float64) (area, perimeter float64) {
    area = width * height
    perimeter = 2 * (width + height)
    return area, perimeter // explicit is clearer
}
 
func main() {
    greet("Alice")
 
    sum := add(3, 5)
    fmt.Println("Sum:", sum)
 
    product := multiply(2, 3, 4)
    fmt.Println("Product:", product)
 
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Printf("Result: %.2f\n", result)
    }
 
    a, p := rectangle(5, 3)
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", a, p)
}

Variadic Functions

package main
 
import "fmt"
 
// Variadic function - accepts any number of arguments
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}
 
// Variadic with fixed parameters first
func printf(format string, args ...interface{}) {
    fmt.Printf(format, args...)
}
 
func main() {
    // Call with individual arguments
    fmt.Println(sum(1, 2, 3))       // 6
    fmt.Println(sum(1, 2, 3, 4, 5)) // 15
    fmt.Println(sum())              // 0
 
    // Call with slice using ...
    numbers := []int{10, 20, 30}
    fmt.Println(sum(numbers...))    // 60
 
    // printf example
    printf("Name: %s, Age: %d\n", "Alice", 30)
}

Anonymous Functions and Closures

package main
 
import "fmt"
 
func main() {
    // Anonymous function
    func() {
        fmt.Println("I'm anonymous!")
    }()
 
    // Assign anonymous function to variable
    double := func(x int) int {
        return x * 2
    }
    fmt.Println(double(5)) // 10
 
    // Closure: function that captures variables from outer scope
    counter := makeCounter()
    fmt.Println(counter()) // 1
    fmt.Println(counter()) // 2
    fmt.Println(counter()) // 3
 
    // Another counter is independent
    counter2 := makeCounter()
    fmt.Println(counter2()) // 1
 
    // Closure capturing loop variable
    funcs := make([]func(), 3)
    for i := 0; i < 3; i++ {
        i := i // Create new variable for each iteration!
        funcs[i] = func() {
            fmt.Println(i)
        }
    }
    funcs[0]() // 0
    funcs[1]() // 1
    funcs[2]() // 2
}
 
// Closure example: counter factory
func makeCounter() func() int {
    count := 0 // This variable is captured
    return func() int {
        count++
        return count
    }
}

Functions as First-Class Citizens

package main
 
import "fmt"
 
// Function type
type MathFunc func(int, int) int
 
// Function that accepts function as parameter
func apply(fn MathFunc, a, b int) int {
    return fn(a, b)
}
 
// Function that returns a function
func makeMultiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}
 
func main() {
    // Passing functions as arguments
    add := func(a, b int) int { return a + b }
    subtract := func(a, b int) int { return a - b }
 
    fmt.Println(apply(add, 10, 5))      // 15
    fmt.Println(apply(subtract, 10, 5)) // 5
 
    // Function factory
    double := makeMultiplier(2)
    triple := makeMultiplier(3)
 
    fmt.Println(double(5))  // 10
    fmt.Println(triple(5))  // 15
}

Part 8: Pointers

Pointers hold memory addresses. Go has pointers but no pointer arithmetic.

package main
 
import "fmt"
 
func main() {
    // Creating pointers
    x := 42
    var p *int = &x        // p holds address of x
 
    fmt.Println("Value of x:", x)         // 42
    fmt.Println("Address of x:", &x)      // 0xc000018090 (example)
    fmt.Println("Value of p:", p)         // 0xc000018090 (same address)
    fmt.Println("Value at p:", *p)        // 42 (dereferencing)
 
    // Modifying through pointer
    *p = 100
    fmt.Println("New value of x:", x)     // 100
 
    // nil pointers
    var nilPtr *int
    fmt.Println("nil pointer:", nilPtr)   // <nil>
    // fmt.Println(*nilPtr)               // PANIC! Can't dereference nil
 
    // Check for nil before dereferencing
    if nilPtr != nil {
        fmt.Println(*nilPtr)
    }
 
    // new() allocates memory and returns pointer
    p2 := new(int)
    fmt.Println("p2 points to:", *p2)     // 0 (zero value)
    *p2 = 50
    fmt.Println("p2 now points to:", *p2) // 50
}

Pointers with Functions

package main
 
import "fmt"
 
// Pass by value - original unchanged
func doubleValue(x int) {
    x = x * 2
    fmt.Println("Inside doubleValue:", x)
}
 
// Pass by reference (pointer) - modifies original
func doublePointer(x *int) {
    *x = *x * 2
    fmt.Println("Inside doublePointer:", *x)
}
 
// Returning pointer to local variable (safe in Go!)
func createUser(name string) *User {
    // Go's escape analysis moves this to heap
    return &User{Name: name}
}
 
type User struct {
    Name string
}
 
func main() {
    n := 10
 
    // Pass by value
    doubleValue(n)
    fmt.Println("After doubleValue:", n)    // Still 10
 
    // Pass by pointer
    doublePointer(&n)
    fmt.Println("After doublePointer:", n)  // Now 20
 
    // Returning pointer
    user := createUser("Alice")
    fmt.Println("User:", user.Name)
}

When to use pointers:

✅ When you need to modify the original value
✅ When passing large structs (avoid copying)
✅ When you need to signal "no value" with nil
✅ When implementing methods that modify receiver

When to avoid pointers:

✅ For small, immutable values
✅ When you want guaranteed immutability
✅ For basic types (int, bool, string) unless modification needed

For a deeper dive, see our companion posts:


Part 9: Arrays and Slices

Arrays (Fixed Size)

package main
 
import "fmt"
 
func main() {
    // Declare array with explicit size
    var arr [5]int
    fmt.Println("Empty array:", arr) // [0 0 0 0 0]
 
    // Array literal
    arr2 := [5]int{1, 2, 3, 4, 5}
    fmt.Println("Array literal:", arr2)
 
    // Let compiler count elements
    arr3 := [...]int{10, 20, 30}
    fmt.Println("Auto-sized:", arr3)
 
    // Access and modify
    arr2[0] = 100
    fmt.Println("Modified:", arr2[0])
    fmt.Println("Length:", len(arr2))
 
    // Arrays are VALUE types - copying creates new array
    arr4 := arr2
    arr4[0] = 999
    fmt.Println("Original:", arr2[0]) // 100 (unchanged!)
    fmt.Println("Copy:", arr4[0])     // 999
 
    // Array of arrays (2D)
    matrix := [2][3]int{
        {1, 2, 3},
        {4, 5, 6},
    }
    fmt.Println("Matrix:", matrix)
}

Slices (Dynamic, Reference Type)

Slices are much more common than arrays in Go:

package main
 
import "fmt"
 
func main() {
    // Create slice with make
    s := make([]int, 5)    // length 5, capacity 5
    fmt.Println("slice:", s, "len:", len(s), "cap:", cap(s))
 
    // Create slice with make (length, capacity)
    s2 := make([]int, 3, 10) // length 3, capacity 10
    fmt.Println("slice2:", s2, "len:", len(s2), "cap:", cap(s2))
 
    // Slice literal
    s3 := []int{1, 2, 3, 4, 5}
    fmt.Println("literal:", s3)
 
    // Slice from array
    arr := [5]int{10, 20, 30, 40, 50}
    slice := arr[1:4]      // Elements 1, 2, 3 (excludes 4)
    fmt.Println("from array:", slice) // [20 30 40]
 
    // Slice operations
    slice2 := s3[1:3]      // Elements at index 1, 2
    slice3 := s3[:3]       // Elements 0, 1, 2
    slice4 := s3[2:]       // Elements from index 2 to end
    slice5 := s3[:]        // Full slice (copy of reference)
    fmt.Println(slice2, slice3, slice4, slice5)
 
    // Slices are REFERENCE types
    original := []int{1, 2, 3}
    reference := original
    reference[0] = 999
    fmt.Println("Original:", original[0]) // 999 (changed!)
}

Slice Operations

package main
 
import "fmt"
 
func main() {
    // Append elements
    s := []int{1, 2, 3}
    s = append(s, 4)           // Append single element
    s = append(s, 5, 6, 7)     // Append multiple elements
    fmt.Println("After append:", s)
 
    // Append another slice
    more := []int{8, 9, 10}
    s = append(s, more...)      // ... spreads the slice
    fmt.Println("After merge:", s)
 
    // Copy slices
    src := []int{1, 2, 3}
    dst := make([]int, len(src))
    copied := copy(dst, src)
    fmt.Println("Copied:", copied, "elements:", dst)
 
    // Delete element (by index)
    s = []int{1, 2, 3, 4, 5}
    i := 2 // Delete element at index 2
    s = append(s[:i], s[i+1:]...)
    fmt.Println("After delete:", s) // [1 2 4 5]
 
    // Insert element
    s = []int{1, 2, 4, 5}
    i = 2           // Insert at index 2
    val := 3
    s = append(s[:i], append([]int{val}, s[i:]...)...)
    fmt.Println("After insert:", s) // [1 2 3 4 5]
 
    // Or use cleaner insertion (Go 1.21+)
    // slices.Insert(s, i, val)
}

Slice Internals

A slice is a struct with three fields: pointer, length, and capacity:

package main
 
import "fmt"
 
func main() {
    // Underlying array: [10 20 30 40 50]
    arr := [5]int{10, 20, 30, 40, 50}
 
    // Slice: points to arr[1], length 3, capacity 4
    s := arr[1:4]
    fmt.Println("Slice:", s)           // [20 30 40]
    fmt.Println("Length:", len(s))     // 3
    fmt.Println("Capacity:", cap(s))   // 4 (can grow to arr end)
 
    // Extending slice within capacity
    s = s[:4]                          // Extend to full capacity
    fmt.Println("Extended:", s)        // [20 30 40 50]
 
    // Capacity exceeded - new array allocated
    original := make([]int, 3, 3)
    fmt.Printf("Before: ptr=%p, cap=%d\n", original, cap(original))
 
    original = append(original, 1)     // Exceeds capacity!
    fmt.Printf("After: ptr=%p, cap=%d\n", original, cap(original))
    // Pointer changed, capacity doubled!
}

Important: Read our companion post Never Use Arrays in Go for more details on arrays vs slices.


Part 10: Maps

Maps are Go's built-in hash table/dictionary:

package main
 
import "fmt"
 
func main() {
    // Create map with make
    ages := make(map[string]int)
    ages["Alice"] = 30
    ages["Bob"] = 25
    fmt.Println("Ages:", ages)
 
    // Map literal
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
        "Carol": 92,
    }
    fmt.Println("Scores:", scores)
 
    // Access values
    aliceScore := scores["Alice"]
    fmt.Println("Alice's score:", aliceScore)
 
    // Check if key exists
    score, exists := scores["David"]
    if exists {
        fmt.Println("David's score:", score)
    } else {
        fmt.Println("David not found")
    }
 
    // Shorter check pattern
    if s, ok := scores["Carol"]; ok {
        fmt.Println("Carol's score:", s)
    }
 
    // Delete key
    delete(scores, "Bob")
    fmt.Println("After delete:", scores)
 
    // Iterate over map (order is random!)
    for name, score := range scores {
        fmt.Printf("%s: %d\n", name, score)
    }
 
    // Map length
    fmt.Println("Map size:", len(scores))
 
    // nil map (can read, cannot write)
    var nilMap map[string]int
    fmt.Println("nil map read:", nilMap["key"]) // 0 (zero value)
    // nilMap["key"] = 1                         // PANIC!
 
    // Maps are reference types
    m1 := map[string]int{"a": 1}
    m2 := m1
    m2["a"] = 100
    fmt.Println("m1:", m1["a"]) // 100 (changed!)
}

Maps with Complex Values

package main
 
import "fmt"
 
type Person struct {
    Name string
    Age  int
}
 
func main() {
    // Map of structs
    people := map[string]Person{
        "alice": {Name: "Alice", Age: 30},
        "bob":   {Name: "Bob", Age: 25},
    }
    fmt.Println(people["alice"])
 
    // Map of slices
    groups := map[string][]string{
        "fruits":     {"apple", "banana", "orange"},
        "vegetables": {"carrot", "broccoli"},
    }
    fmt.Println(groups["fruits"])
 
    // Map of maps (nested)
    nested := map[string]map[string]int{
        "user1": {"score": 100, "level": 5},
        "user2": {"score": 85, "level": 3},
    }
    fmt.Println(nested["user1"]["score"])
 
    // Using map as set (for deduplication)
    items := []string{"apple", "banana", "apple", "cherry", "banana"}
    set := make(map[string]struct{}) // Empty struct uses no memory
 
    for _, item := range items {
        set[item] = struct{}{}
    }
 
    // Get unique items
    unique := make([]string, 0, len(set))
    for item := range set {
        unique = append(unique, item)
    }
    fmt.Println("Unique:", unique)
}

Part 11: Structs

Structs are Go's way to define custom types with named fields:

package main
 
import "fmt"
 
// Define a struct
type Person struct {
    FirstName string
    LastName  string
    Age       int
    Email     string
}
 
// Struct with tags (for JSON, database, etc.)
type User struct {
    ID       int    `json:"id" db:"user_id"`
    Username string `json:"username" db:"username"`
    Email    string `json:"email,omitempty" db:"email"`
}
 
func main() {
    // Create struct - zero values
    var p1 Person
    fmt.Println("Zero:", p1)
 
    // Create with field names
    p2 := Person{
        FirstName: "Alice",
        LastName:  "Smith",
        Age:       30,
        Email:     "alice@example.com",
    }
    fmt.Println("Named:", p2)
 
    // Create without field names (order matters!)
    p3 := Person{"Bob", "Jones", 25, "bob@example.com"}
    fmt.Println("Positional:", p3)
 
    // Access and modify fields
    fmt.Println("Name:", p2.FirstName, p2.LastName)
    p2.Age = 31
    fmt.Println("Updated age:", p2.Age)
 
    // Pointer to struct
    p4 := &Person{FirstName: "Carol"}
    fmt.Println("Pointer:", p4.FirstName) // No -> needed like C!
    p4.Age = 28                           // Automatically dereferenced
 
    // Anonymous struct (useful for one-off types)
    point := struct {
        X, Y int
    }{10, 20}
    fmt.Println("Point:", point)
 
    // Struct comparison (only if all fields are comparable)
    a := Person{FirstName: "Alice"}
    b := Person{FirstName: "Alice"}
    fmt.Println("Equal:", a == b) // true
}

Struct Embedding (Composition)

package main
 
import "fmt"
 
// Base struct
type Address struct {
    Street  string
    City    string
    Country string
}
 
// Embedding Address into Employee
type Employee struct {
    Name    string
    ID      int
    Address // Embedded (anonymous field)
}
 
// Can also embed with name
type Customer struct {
    Name          string
    BillingAddr   Address
    ShippingAddr  Address
}
 
func main() {
    // Embedded struct fields are promoted
    emp := Employee{
        Name: "Alice",
        ID:   123,
        Address: Address{
            Street:  "123 Main St",
            City:    "New York",
            Country: "USA",
        },
    }
 
    // Access embedded fields directly (promoted)
    fmt.Println("Employee:", emp.Name)
    fmt.Println("City:", emp.City)           // Promoted from Address
    fmt.Println("City:", emp.Address.City)   // Also works
 
    // Named embedded fields
    customer := Customer{
        Name: "Bob",
        BillingAddr: Address{
            City: "Los Angeles",
        },
        ShippingAddr: Address{
            City: "San Francisco",
        },
    }
    fmt.Println("Billing:", customer.BillingAddr.City)
    fmt.Println("Shipping:", customer.ShippingAddr.City)
}

For more on composition, see: Favor Composition Over Inheritance


Practice Exercises

Exercise 1: Temperature Converter

Write a program that converts temperatures between Celsius and Fahrenheit:

package main
 
import "fmt"
 
func celsiusToFahrenheit(c float64) float64 {
    return c*9/5 + 32
}
 
func fahrenheitToCelsius(f float64) float64 {
    return (f - 32) * 5 / 9
}
 
func main() {
    c := 25.0
    f := celsiusToFahrenheit(c)
    fmt.Printf("%.1f°C = %.1f°F\n", c, f)
 
    f = 77.0
    c = fahrenheitToCelsius(f)
    fmt.Printf("%.1f°F = %.1f°C\n", f, c)
}

Exercise 2: Word Counter

Count word frequencies in a text:

package main
 
import (
    "fmt"
    "strings"
)
 
func wordCount(text string) map[string]int {
    words := strings.Fields(strings.ToLower(text))
    counts := make(map[string]int)
 
    for _, word := range words {
        counts[word]++
    }
 
    return counts
}
 
func main() {
    text := "Go is great Go is fast Go is simple"
    counts := wordCount(text)
 
    for word, count := range counts {
        fmt.Printf("%s: %d\n", word, count)
    }
}

Exercise 3: FizzBuzz

Classic programming exercise:

package main
 
import "fmt"
 
func fizzBuzz(n int) string {
    switch {
    case n%15 == 0:
        return "FizzBuzz"
    case n%3 == 0:
        return "Fizz"
    case n%5 == 0:
        return "Buzz"
    default:
        return fmt.Sprintf("%d", n)
    }
}
 
func main() {
    for i := 1; i <= 20; i++ {
        fmt.Println(fizzBuzz(i))
    }
}

Best Practices Summary

Naming Conventions

// Exported (public) - PascalCase
type UserService struct {}
func GetUser() {}
 
// Unexported (private) - camelCase
type userRepository struct {}
func validateEmail() {}
 
// Acronyms stay uppercase
type HTTPClient struct {}
var userID int

Variable Declaration

// Use := for local variables
name := "Alice"
 
// Use var for package-level variables
var defaultTimeout = 30 * time.Second
 
// Use var for explicit type
var count int64
 
// Use var for zero-value initialization
var config Config

Error Handling

// Always check errors
result, err := doSomething()
if err != nil {
    return err // or handle appropriately
}
 
// Don't ignore errors with _
// BAD: result, _ := doSomething()

Code Formatting

# Always run gofmt or gofumpt
go fmt ./...
 
# Run static analysis
go vet ./...

Common Mistakes to Avoid

1. Shadowing Variables

// BAD: err is shadowed
var err error
if condition {
    result, err := doSomething() // New err variable!
    // Original err is unchanged
}
 
// GOOD: Use = not :=
var err error
if condition {
    var result int
    result, err = doSomething() // Uses outer err
}

2. Loop Variable Capture

// BAD: All goroutines see final value of i
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i) // Will print 3, 3, 3
    }()
}
 
// GOOD: Copy loop variable
for i := 0; i < 3; i++ {
    i := i // Create new variable
    go func() {
        fmt.Println(i) // Prints 0, 1, 2
    }()
}

3. Nil Map Write

// BAD: Panic on nil map write
var m map[string]int
m["key"] = 1 // PANIC!
 
// GOOD: Initialize first
m := make(map[string]int)
m["key"] = 1

4. String Iteration

s := "Hello, 世界"
 
// BAD: Iterates over bytes
for i := 0; i < len(s); i++ {
    fmt.Printf("%c", s[i]) // Wrong for multi-byte chars
}
 
// GOOD: Use range for runes
for _, r := range s {
    fmt.Printf("%c", r) // Correct!
}

What's Next?

Congratulations on completing Phase 1! You now have a solid foundation in Go fundamentals.

In Phase 2: Core Go Concepts, you'll learn:

  • Structs and methods in depth
  • Interfaces and polymorphism
  • Error handling patterns
  • Package organization
  • Working with Go modules

Related Posts:


Summary

In this post, you learned:

Installation: Set up Go and development environment
Variables: Declaration, zero values, constants, and iota
Data Types: Integers, floats, strings, booleans, type conversion
Control Flow: if/else, switch, for loops, defer
Functions: Multiple returns, variadic, closures
Pointers: Memory addresses and dereferencing
Arrays & Slices: Fixed vs dynamic arrays, slice operations
Maps: Go's hash tables for key-value storage
Structs: Custom types and embedding

You're now ready to move on to Phase 2 and explore Go's core concepts like interfaces, error handling, and packages!

Happy coding! 🚀

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.