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/arm64Using Official Installer:
- Download the installer from go.dev/dl
- Run the
.pkgfile - 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 versionWindows Installation
- Download the
.msiinstaller from go.dev/dl - Run the installer
- Open a new Command Prompt
- 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 serverPart 2: Setting Up Your Development Environment
VS Code Setup (Recommended)
-
Install VS Code: Download from code.visualstudio.com
-
Install Go Extension:
- Open VS Code
- Press
Cmd+Shift+X(Mac) orCtrl+Shift+X(Windows/Linux) - Search for "Go" by the Go Team at Google
- Click Install
-
Install Go Tools:
- Open Command Palette:
Cmd+Shift+PorCtrl+Shift+P - Type "Go: Install/Update Tools"
- Select all tools and click OK
- Open Command Palette:
Essential tools installed:
gopls- Language server for autocomplete and intellisensedlv- Debuggergofumpt- Stricter formattergolangci-lint- Linter
GoLand Setup (JetBrains)
If you prefer JetBrains IDEs:
- Download GoLand from jetbrains.com/go
- Install and open GoLand
- 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-goThe go mod init command creates a go.mod file:
module hello-go
go 1.22Write 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 programimport "fmt"- Imports the format package for printingfunc main()- Entry point of the programfmt.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 installGo Commands Reference
| Command | Description |
|---|---|
go run | Compile and run without creating binary |
go build | Compile and create binary |
go install | Compile and install to GOPATH/bin |
go fmt | Format source code |
go vet | Report suspicious code constructs |
go test | Run tests |
go mod init | Initialize new module |
go mod tidy | Add missing and remove unused modules |
go get | Add 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:
| Type | Zero Value |
|---|---|
int, int8, int16, int32, int64 | 0 |
uint, uint8, uint16, uint32, uint64 | 0 |
float32, float64 | 0.0 |
bool | false |
string | "" (empty) |
| pointers, slices, maps, channels, interfaces | nil |
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
rangeto iterate over runes correctly rune=int32for Unicode code pointsbyte=uint8for 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
breakneeded - cases don't fall through by default - Use
fallthroughkeyword 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:
- Pointer Receiver vs Value Receiver
- Go Pointers vs C++ Pointers - Compare Go's safer approach to C++'s powerful but dangerous pointers
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 intVariable 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 ConfigError 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"] = 14. 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:
- Go Learning Roadmap - Full series overview
- Never Use Arrays in Go - Why slices are almost always better
- Pointer Receiver vs Value Receiver - Deep dive into method receivers
- Composition Over Inheritance - Go's approach to code reuse
- Go Pointers vs C++ Pointers - Compare pointer semantics across languages
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.