Go (also known as Golang) is a statically typed, compiled programming language designed at Google.
The name "Golang" comes from the domain name golang.org (to make it more searchable).
Go favors composition over inheritance and explicit over implicit behavior.
Key Features of Go
Static Typing: Variables have types known at compile time, catching errors early.
Fast Compilation: Go compiles directly to machine code extremely quickly.
Goroutines: Lightweight threads that enable easy concurrent programming.
Channels: Built-in primitives for safe communication between goroutines.
Garbage Collection: Automatic memory management without manual allocation/deallocation.
Standard Library: Rich, comprehensive standard library for networking, I/O, cryptography, and more.
Cross-Platform: Compiles to native binaries for Windows, Linux, macOS, and many other platforms.
Built-in Tooling: Includes formatter, linter, testing framework, package manager, and documentation generator.
No Generics (until Go 1.18): Originally designed without generics for simplicity, added in 2022.
Setting Up Your First Go Project
Create a project directory:
mkdir hello-go
cd hello-go
Initialize a Go module:
go mod init example.com/hello
This creates a go.mod file that tracks dependencies.
Go modules are the standard way to manage dependencies since Go 1.11.
Hello World in Go
Create a file named main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
package main declares this as an executable program
import "fmt" imports the format/print package
func main() is the entry point of the program
fmt.Println() prints text to the console
Run the program:
go run main.go
Build an executable:
go build main.go
./main
Basic Go Syntax Overview
Variable Declaration:
var name string = "Junzhe"
age := 30 // Short declaration with type inference
Constants:
const Pi = 3.14159
Functions:
func add(x int, y int) int {
return x + y
}
Control Structures:
if x > 10 {
fmt.Println("x is large")
}
for i := 0; i < 5; i++ {
fmt.Println(i)
}
switch day {
case "Monday":
fmt.Println("Start of week")
default:
fmt.Println("Other day")
}
Note: Go has no while loop, for is used for all loops.
Functions must be defined before use or in any order at package level.
Cross-Compiling for Different Platforms
Go makes it easy to build executables for different operating systems:
# Build for Windows from Linux/macOS
GOOS=windows GOARCH=amd64 go build -o hello.exe main.go
# Build for Linux from Windows/macOS
GOOS=linux GOARCH=amd64 go build -o hello main.go
# Build for macOS from Linux/Windows
GOOS=darwin GOARCH=amd64 go build -o hello main.go
Common GOOS and GOARCH values:
GOOS (Operating System)
GOARCH (Architecture)
linux
amd64 (64-bit)
windows
386 (32-bit)
darwin (macOS)
arm
freebsd
arm64
Go Variables and Data Types
What Are Variables?
A variable is a named storage location that holds a value of a specific type.
Variables in Go are:
statically typed (type is known at compile time)
explicitly declared or inferred
initialized with zero values if not assigned
Declaring Variables
Go provides several ways to declare variables:
Using var keyword:
var name string
var age int
var isActive bool
With initialization:
var name string = "Junzhe"
var age int = 30
var isActive bool = true
Type inference (omit type):
var name = "Junzhe" // Go infers string
var age = 30 // Go infers int
var isActive = true // Go infers bool
Short declaration (inside functions only):
name := "Junzhe"
age := 30
isActive := true
The := operator declares and initializes in one step.
Zero Values
Variables declared without initialization receive a zero value:
Constants must be assigned at compile time (no runtime values).
Type Conversion
Go does not allow implicit type conversion:
var i int = 42
var f float64 = i // Error: cannot use i (type int) as type float64
Explicit conversion is required:
var i int = 42
var f float64 = float64(i) // OK: explicit conversion
Common conversions:
var x int = 100
var y float64 = float64(x)
var z int32 = int32(x)
var s string = string(x) // Converts to Unicode character, not "100"
Converting integers to strings (proper way):
import "strconv"
var num int = 42
var str string = strconv.Itoa(num) // "42"
Converting strings to integers:
import "strconv"
var str string = "42"
num, err := strconv.Atoi(str) // num = 42, err = nil
Variable Scope
Variables have different scopes based on where they're declared:
Scope Level
Description
Package-level
Declared outside functions, accessible throughout the package
Function-level
Declared inside functions, accessible only within that function
Block-level
Declared inside blocks (if, for, etc.), accessible only in that block
Example:
package main
var globalVar = "I'm global" // Package-level
func main() {
var localVar = "I'm local" // Function-level
if true {
var blockVar = "I'm in a block" // Block-level
fmt.Println(globalVar) // OK
fmt.Println(localVar) // OK
fmt.Println(blockVar) // OK
}
// fmt.Println(blockVar) // Error: undefined
}
Package-level variables cannot use the := syntax.
Naming Conventions
Go has strict naming rules and conventions:
Basic rules:
must start with a letter or underscore
can contain letters, digits, and underscores
case-sensitive (name and Name are different)
Visibility rules:
Case
Visibility
Example
Starts with uppercase
Exported (public)
MaxValue, UserName
Starts with lowercase
Unexported (private)
maxValue, userName
Naming conventions:
use camelCase for local variables: userName, totalCount
use PascalCase for exported names: UserName, TotalCount
short names are preferred for small scopes: i, j, err
acronyms should be all caps: HTTP, URL, ID
Good examples:
var userCount int
var httpClient *http.Client
var userID string
Avoid using underscore separators except for package-level constants.
The Blank Identifier
The underscore _ is a special identifier that discards values:
// Ignore a return value
result, _ := someFunction() // Ignore error
// Ignore first value
_, value := someFunction() // Ignore result
// Import package for side effects only
import _ "github.com/lib/pq"
Example with range:
numbers := []int{1, 2, 3, 4, 5}
for _, num := range numbers { // Ignore index
fmt.Println(num)
}
Variable Shadowing
A variable in an inner scope can shadow a variable in an outer scope:
package main
import "fmt"
var x = "global"
func main() {
fmt.Println(x) // "global"
x := "local" // Shadows the global x
fmt.Println(x) // "local"
{
x := "block" // Shadows the function-level x
fmt.Println(x) // "block"
}
fmt.Println(x) // "local"
}
Shadowing can lead to bugs, so use it carefully.
Many linters warn about shadowing common variables like err.
Pointers and Variables
Go supports pointers, which store memory addresses:
var x int = 42
var p *int = &x // p holds the address of x
fmt.Println(x) // 42
fmt.Println(p) // 0xc0000b4008 (memory address)
fmt.Println(*p) // 42 (dereference to get value)
Pointer operators:
Operator
Description
&
Address-of operator (gets memory address)
*
Dereference operator (gets value at address)
Modifying through pointers:
var x int = 10
var p *int = &x
*p = 20 // Modify x through pointer
fmt.Println(x) // 20
Zero value of a pointer is nil.
Functions in Go
What Are Functions?
A function is a reusable block of code that performs a specific task.
Functions in Go:
are first-class citizens (can be assigned to variables, passed as arguments)
can return multiple values
support named return values
can be anonymous (function literals)
can form closures
Basic Function Syntax
The basic syntax for declaring a function:
func functionName(parameter1 type1, parameter2 type2) returnType {
// function body
return value
}
func process(value int) {
if value < 0 {
return // Exit early
}
fmt.Println("Processing:", value)
}
Functions With Parameters
Functions can accept zero or more parameters:
// No parameters
func sayHello() {
fmt.Println("Hello!")
}
// One parameter
func square(x int) int {
return x * x
}
// Multiple parameters
func add(x int, y int) int {
return x + y
}
When consecutive parameters have the same type, you can omit the type for all but the last:
// Both are equivalent
func add(x int, y int) int
func add(x, y int) int
Mixed parameter types:
func describe(name string, age int, height float64) {
fmt.Printf("%s is %d years old and %.2f meters tall\n", name, age, height)
}
Multiple Return Values
Go functions can return multiple values:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result) // Result: 5
}
Multiple return values are specified in parentheses:
func swap(x, y string) (string, string) {
return y, x
}
a, b := swap("hello", "world")
fmt.Println(a, b) // world hello
Named Return Values (it somehow reminds me of Pascal)
Return values can be named in the function signature:
func calculate(x, y int) (sum int, product int) {
sum = x + y
product = x * y
return // Naked return
}
func main() {
s, p := calculate(3, 4)
fmt.Println(s, p) // 7 12
}
Named return values are automatically initialized to their zero values.
A naked return returns the named values without explicitly specifying them.
Named returns improve documentation but can reduce clarity in complex functions:
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("division by zero")
return // result will be 0.0 (zero value)
}
result = a / b
return
}
Variadic Functions
A variadic function accepts a variable number of arguments:
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
fmt.Println(sum()) // 0
}
The ... before the type indicates a variadic parameter.
Inside the function, the variadic parameter is treated as a slice.
Anonymous functions with parameters and return values:
add := func(x, y int) int {
return x + y
}
result := add(5, 3)
fmt.Println(result) // 8
Functions as Values
Functions are first-class values and can be:
assigned to variables
passed as arguments
returned from functions
Assigning functions to variables:
func add(x, y int) int {
return x + y
}
func main() {
var operation func(int, int) int
operation = add
result := operation(3, 4)
fmt.Println(result) // 7
}
Passing functions as arguments:
func apply(f func(int, int) int, x, y int) int {
return f(x, y)
}
func multiply(x, y int) int {
return x * y
}
func main() {
result := apply(multiply, 5, 6)
fmt.Println(result) // 30
}
Returning functions from functions:
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
add5 := makeAdder(5)
fmt.Println(add5(3)) // 8
fmt.Println(add5(10)) // 15
}
Closures
A closure is a function that references variables from outside its body:
Multiple defers execute in LIFO (Last In, First Out) order:
func main() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
fmt.Println("Main")
}
// Output:
// Main
// Third
// Second
// First
Common use cases:
Use Case
Example
Closing files
defer file.Close()
Unlocking mutexes
defer mutex.Unlock()
Closing database connections
defer db.Close()
Cleanup operations
defer cleanup()
Practical example with file handling:
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Ensures file is closed
// Read file contents
// Even if an error occurs, file.Close() will be called
return nil
}
Deferred function arguments are evaluated immediately:
func main() {
x := 10
defer fmt.Println(x) // x is evaluated now (10)
x = 20
fmt.Println(x)
}
// Output:
// 20
// 10
Recursive Functions
A function can call itself (recursion):
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
func main() {
fmt.Println(factorial(5)) // 120
}
Fibonacci sequence using recursion:
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
for i := 0; i < 10; i++ {
fmt.Print(fibonacci(i), " ")
}
// Output: 0 1 1 2 3 5 8 13 21 34
}
Always include a base case to prevent infinite recursion.
Recursive tree traversal:
type TreeNode struct {
Value int
Left *TreeNode
Right *TreeNode
}
func traverse(node *TreeNode) {
if node == nil {
return
}
fmt.Println(node.Value)
traverse(node.Left)
traverse(node.Right)
}
Methods
Go doesn't have classes, but you can define methods on types:
type Rectangle struct {
Width float64
Height float64
}
// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
}
Methods can have pointer receivers to modify the receiver:
When method doesn't modify the receiver, small structs
Pointer (r *Type)
When method modifies the receiver, large structs, or consistency
Methods can be defined on any type in the same package:
type MyInt int
func (m MyInt) Double() MyInt {
return m * 2
}
func main() {
num := MyInt(5)
fmt.Println(num.Double()) // 10
}
Function Type Declarations
You can create custom function types:
type Operation func(int, int) int
func add(x, y int) int {
return x + y
}
func multiply(x, y int) int {
return x * y
}
func apply(op Operation, x, y int) int {
return op(x, y)
}
func main() {
fmt.Println(apply(add, 5, 3)) // 8
fmt.Println(apply(multiply, 5, 3)) // 15
}
Init Function
The init() function runs automatically before main():
package main
import "fmt"
func init() {
fmt.Println("Initialization")
}
func main() {
fmt.Println("Main function")
}
// Output:
// Initialization
// Main function
Multiple init() functions can exist in the same package.
They execute in the order they appear in the file.
package calculator
// Exported (public) - can be used by other packages
func Add(x, y int) int {
return x + y
}
// Unexported (private) - only accessible within calculator package
func validate(x int) bool {
return x >= 0
}
Example with struct:
package user
type User struct {
Name string // Exported field
Email string // Exported field
age int // Unexported field (private)
}
Document packages with comments before the package declaration:
// Package calculator provides basic arithmetic operations.
// It supports addition, subtraction, multiplication, and division.
package calculator
Document exported functions:
// Add returns the sum of x and y.
func Add(x, y int) int {
return x + y
}
// Divide returns the quotient of x divided by y.
// It returns an error if y is zero.
func Divide(x, y float64) (float64, error) {
if y == 0 {
return 0, errors.New("division by zero")
}
return x / y, nil
}
View documentation locally:
go doc calculator
go doc calculator.Add
Generate HTML documentation:
godoc -http=:6060
Documentation is automatically published on pkg.go.dev for public packages.
Package Initialization
Packages are initialized in the following order:
Import dependencies
Initialize package-level variables
Run init() functions
Example with init():
package database
import "database/sql"
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", "connection_string")
if err != nil {
panic(err)
}
}
func GetDB() *sql.DB {
return db
}
Multiple init() functions are executed in declaration order.
Each imported package's init() runs before the importing package.
Vendor Directory
The vendor/ directory stores a copy of dependencies:
Most projects don't need vendoring anymore due to Go's module cache.
Package Aliases and Naming Conflicts
Resolve naming conflicts with aliases:
import (
"crypto/rand"
mrand "math/rand" // Alias to avoid conflict
)
func main() {
// Use crypto/rand
bytes := make([]byte, 32)
rand.Read(bytes)
// Use math/rand with alias
num := mrand.Intn(100)
}
Another example:
import (
"database/sql"
"github.com/jmoiron/sqlx"
stdSQL "database/sql" // If needed
)
Understanding Go Module Names and Project Naming
What is a Module Path?
In Go, every project has a module path - a unique identifier for your project.
The module path looks like a URL: example.com/user/project
This appears in your go.mod file:
module github.com/alice/myproject
go 1.21
The module path does not have to be a real website - it's just a naming convention!
However, if you plan to share your code, using a real repository URL makes it importable by others.
golang.org/x/... # Extended Go libraries
go.googlesource.com/... # Google's Go repositories
Creating Your First Module
# 1. Create project directory
$ mkdir hello-world
$ cd hello-world
# 2. Initialize Go module with a name
$ go mod init github.com/yourusername/hello-world
# This creates go.mod file:
# module github.com/yourusername/hello-world
#
# go 1.21
# 3. Create your Go file
$ cat > main.go << 'EOF'
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
EOF
# 4. Run it
$ go run main.go
Hello, World!
# 5. Build executable
$ go build
$ ./hello-world
Hello, World!
module: Your project's module path (the name you chose)
go: Minimum Go version required
require: Dependencies your project needs
replace: Override dependency locations (optional)
Dependencies are added automatically when you import and build:
# Import a package in your code
import "github.com/gin-gonic/gin"
# Build or run - Go automatically adds to go.mod
$ go build
go: downloading github.com/gin-gonic/gin v1.9.1
go: downloading github.com/gin-contrib/sse v0.1.0
...
Importing Your Own Packages
Once you have a module name, you can create subpackages:
For version 2 and above, include version in module path:
// Version 1.x
module github.com/alice/myproject
// Version 2.x and above
module github.com/alice/myproject/v2
// Version 3.x
module github.com/alice/myproject/v3
This allows multiple major versions to coexist:
import (
v1 "github.com/alice/myproject"
v2 "github.com/alice/myproject/v2"
)
func main() {
// Use both versions in same program
v1.DoSomething()
v2.DoSomething()
}
module github.com/alice/myapp
go 1.21
require github.com/alice/mylib v0.0.0
// Point to local directory during development
replace github.com/alice/mylib => ../mylib
In myapp/main.go:
package main
import "github.com/alice/mylib"
func main() {
mylib.DoSomething()
}
The replace directive lets you test local changes before publishing!
Best Practices
Use lowercase: Module paths should be lowercase
# Good
$ go mod init github.com/alice/my-project
# Avoid (even if GitHub allows it)
$ go mod init github.com/Alice/My-Project
Use hyphens, not underscores:
# Preferred
github.com/alice/todo-list
# Less common
github.com/alice/todo_list
Match your repository name exactly:
# If GitHub repo is "my-awesome-project"
$ go mod init github.com/alice/my-awesome-project
# Not
$ go mod init github.com/alice/myawesomeproject
Keep names descriptive but concise:
# Good
github.com/alice/http-router
github.com/alice/json-validator
# Too generic
github.com/alice/utils
github.com/alice/tools
The else keyword must be on the same line as the closing brace of the previous block.
If With Initialization Statement
Go allows a short statement before the condition:
if statement; condition {
// code
}
Example:
if age := getAge(); age >= 18 {
fmt.Println("Adult, age:", age)
} else {
fmt.Println("Minor, age:", age)
}
// age is not accessible here
The variable declared in the initialization is scoped to the if block.
Common pattern with error checking:
if err := doSomething(); err != nil {
fmt.Println("Error:", err)
return
}
// err is not accessible here
Another example:
if value, ok := myMap[key]; ok {
fmt.Println("Found:", value)
} else {
fmt.Println("Key not found")
}
Switch Statements
Switch provides multi-way branching based on value:
switch variable {
case value1:
// code
case value2:
// code
default:
// code if no case matches
}
Example:
day := "Monday"
switch day {
case "Monday":
fmt.Println("Start of the work week")
case "Friday":
fmt.Println("End of the work week")
case "Saturday", "Sunday":
fmt.Println("Weekend!")
default:
fmt.Println("Midweek")
}
Key differences from other languages:
no automatic fall-through (no break needed)
cases don't need to be constants
multiple values in one case (comma-separated)
Switch Without Expression
Switch can work like an if-else chain:
switch {
case condition1:
// code
case condition2:
// code
default:
// code
}
The type keyword is special and only works in type switches.
Fallthrough in Switch
Use fallthrough to explicitly continue to the next case:
switch num := 2; num {
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
fallthrough
case 3:
fmt.Println("Three")
default:
fmt.Println("Other")
}
// Output:
// Two
// Three
fallthrough must be the last statement in a case.
It transfers control to the next case unconditionally (doesn't check the condition).
For Loop - Basic Form
The for loop is Go's only looping construct:
for initialization; condition; post {
// code
}
Example:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// Output: 0 1 2 3 4
Components:
Component
Description
initialization
Executed once before the loop starts
condition
Evaluated before each iteration
post
Executed after each iteration
All three components are optional:
i := 0
for ; i < 5; i++ {
fmt.Println(i)
}
For Loop - While Style
Omit initialization and post to create a while-style loop:
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleConnection(conn)
}
Range-Based For Loop
Use range to iterate over slices, arrays, maps, strings, and channels:
for index, value := range collection {
// code
}
Iterating over a slice:
numbers := []int{10, 20, 30, 40, 50}
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
Iterating over a map:
ages := map[string]int{
"Alice": 25,
"Bob": 30,
"Charlie": 35,
}
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
Iterating over a string (returns runes):
for index, char := range "Hello" {
fmt.Printf("%d: %c\n", index, char)
}
Ignoring index or value with _:
// Only value
for _, value := range numbers {
fmt.Println(value)
}
// Only index
for index := range numbers {
fmt.Println(index)
}
// Just iterate (no variables)
for range numbers {
fmt.Println("Iterating...")
}
Break Statement
The break statement exits the innermost loop:
for i := 0; i < 10; i++ {
if i == 5 {
break // Exit loop when i is 5
}
fmt.Println(i)
}
// Output: 0 1 2 3 4
Break with nested loops (exits only the inner loop):
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break // Only breaks inner loop
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
Break in switch statements:
switch value {
case 1:
fmt.Println("One")
break // Optional - cases don't fall through by default
case 2:
fmt.Println("Two")
}
Note: break is implicit in switch cases unless fallthrough is used.
Continue Statement
The continue statement skips the rest of the current iteration:
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // Skip even numbers
}
fmt.Println(i)
}
// Output: 1 3 5 7 9
Example with validation:
numbers := []int{1, 2, -3, 4, -5, 6}
for _, num := range numbers {
if num < 0 {
continue // Skip negative numbers
}
fmt.Println(num)
}
// Output: 1 2 4 6
Continue in nested loops (continues the innermost loop):
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue // Skip to next j
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
Labels With Break and Continue
Use labels to break or continue outer loops:
OuterLoop:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break OuterLoop // Break outer loop
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
// Output:
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
Continue with labels:
OuterLoop:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue OuterLoop // Continue outer loop
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
Labels must be immediately before a for statement.
Goto Statement
The goto statement jumps to a labeled statement:
func main() {
i := 0
Loop:
fmt.Println(i)
i++
if i < 5 {
goto Loop
}
fmt.Println("Done")
}
goto is rarely used in Go - prefer structured control flow.
Defer in Control Flow
The defer statement schedules a function to run when the surrounding function returns:
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close() // Always executes before function returns
// Read file...
}
Multiple defers execute in LIFO order:
func example() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
fmt.Println("Main")
}
// Output:
// Main
// Third
// Second
// First
Defer with loops:
func processFiles(files []string) {
for _, filename := range files {
file, err := os.Open(filename)
if err != nil {
continue
}
defer file.Close() // All defers execute at function end!
// Process file...
}
// All files closed here
}
Better pattern - use a closure:
func processFiles(files []string) {
for _, filename := range files {
func() {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close() // Closes at end of this iteration
// Process file...
}()
}
}
Arrays and Slices in Go
Understanding Arrays
An array is a fixed-size sequence of elements of the same type.
Key characteristics:
fixed length (cannot be resized)
value type (copying creates a new array)
zero-indexed (first element at index 0)
elements are stored contiguously in memory
Array declaration syntax:
var arrayName [size]Type
The size is part of the array's type - [5]int and [10]int are different types.
Declaring and Initializing Arrays
Declaration without initialization (elements set to zero values):
var numbers [5]int // [0, 0, 0, 0, 0]
var names [3]string // ["", "", ""]
rows := 3
cols := 4
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}
matrix[1][2] = 100
fmt.Println(matrix[1][2]) // 100
Jagged slices (rows with different lengths):
jagged := [][]int{
{1},
{2, 3},
{4, 5, 6},
{7, 8, 9, 10},
}
for i, row := range jagged {
fmt.Printf("Row %d: %v\n", i, row)
}
Iterating Over Arrays and Slices
Using for loop with index:
numbers := []int{10, 20, 30, 40, 50}
for i := 0; i < len(numbers); i++ {
fmt.Println(numbers[i])
}
Using range (recommended):
numbers := []int{10, 20, 30, 40, 50}
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
Iterate with values only:
for _, value := range numbers {
fmt.Println(value)
}
Iterate with indices only:
for index := range numbers {
fmt.Println(index)
}
Iterating backwards:
for i := len(numbers) - 1; i >= 0; i-- {
fmt.Println(numbers[i])
}
Iterate 2D slice:
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
for i, row := range matrix {
for j, value := range row {
fmt.Printf("matrix[%d][%d] = %d\n", i, j, value)
}
}
Common Slice Operations
Check if slice is empty:
if len(slice) == 0 {
fmt.Println("Slice is empty")
}
Reverse a slice:
numbers := []int{1, 2, 3, 4, 5}
for i, j := 0, len(numbers)-1; i < j; i, j = i+1, j-1 {
numbers[i], numbers[j] = numbers[j], numbers[i]
}
fmt.Println(numbers) // [5, 4, 3, 2, 1]
Find element in slice:
numbers := []int{10, 20, 30, 40, 50}
target := 30
found := false
for _, num := range numbers {
if num == target {
found = true
break
}
}
fmt.Println(found) // true
Sum all elements:
numbers := []int{1, 2, 3, 4, 5}
sum := 0
for _, num := range numbers {
sum += num
}
fmt.Println(sum) // 15
Find minimum and maximum:
numbers := []int{34, 12, 56, 23, 89, 5}
min := numbers[0]
max := numbers[0]
for _, num := range numbers {
if num < min {
min = num
}
if num > max {
max = num
}
}
fmt.Printf("Min: %d, Max: %d\n", min, max) // Min: 5, Max: 89
Slice Tricks and Patterns
Pre-allocate slice when size is known:
// Instead of:
var numbers []int
for i := 0; i < 1000; i++ {
numbers = append(numbers, i) // Multiple reallocations
}
// Better:
numbers := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
numbers = append(numbers, i) // No reallocations
}
numbers := []int{1, 2, 2, 3, 3, 3, 4, 5, 5}
seen := make(map[int]bool)
result := []int{}
for _, num := range numbers {
if !seen[num] {
seen[num] = true
result = append(result, num)
}
}
fmt.Println(result) // [1, 2, 3, 4, 5]
Slice Internals and Memory
A slice header contains:
type slice struct {
ptr *Elem // Pointer to array
len int // Length
cap int // Capacity
}
Visualizing slice memory:
array := [6]int{1, 2, 3, 4, 5, 6}
slice := array[1:4]
// slice points to: [2, 3, 4]
// len(slice) = 3
// cap(slice) = 5 (from index 1 to end of array)
Memory leak risk with large arrays:
// Bad - keeps entire array in memory
func getFirstTwo(data []int) []int {
return data[:2] // Still references original array
}
// Good - copy to new slice
func getFirstTwo(data []int) []int {
result := make([]int, 2)
copy(result, data[:2])
return result // Original array can be garbage collected
}
Capacity doubling strategy:
When capacity is exceeded, Go typically doubles the capacity
For very large slices, growth rate may be less than 2x
This ensures append operations are amortized O(1)
Variadic Functions in Go
What Are Variadic Functions?
Variadic functions are functions that can accept a variable number of arguments of the same type.
The last parameter uses ... syntax before the type: func name(args ...Type)
Inside the function, the variadic parameter becomes a slice of that type.
Example from standard library:
fmt.Println("Hello", "World", "!") // Takes any number of arguments
fmt.Printf("%s %s\n", "Hello", "World") // First arg is format, rest are variadic
Basic Syntax
Declaring a variadic function:
// Simple variadic function
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
// Call with different number of arguments
fmt.Println(sum()) // 0 (no arguments)
fmt.Println(sum(1)) // 1 (one argument)
fmt.Println(sum(1, 2, 3)) // 6 (three arguments)
fmt.Println(sum(1, 2, 3, 4, 5)) // 15 (five arguments)
}
Only the last parameter can be variadic
Variadic Parameters Are Slices
Inside the function, the variadic parameter is a regular slice:
func printInfo(names ...string) {
// names is a []string slice
fmt.Printf("Type: %T\n", names) // Type: []string
fmt.Printf("Length: %d\n", len(names))
fmt.Printf("Values: %v\n", names)
// Can use all slice operations
if len(names) > 0 {
fmt.Println("First:", names[0])
}
// Can iterate
for i, name := range names {
fmt.Printf("%d: %s\n", i, name)
}
}
func main() {
printInfo("Alice", "Bob", "Carol")
}
// Output:
// Type: []string
// Length: 3
// Values: [Alice Bob Carol]
// First: Alice
// 0: Alice
// 1: Bob
// 2: Carol
Passing Slices to Variadic Functions
You can pass a slice to a variadic function using the ... operator:
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
// Pass individual arguments
result := sum(1, 2, 3, 4, 5)
fmt.Println(result) // 15
// Pass a slice using ...
nums := []int{1, 2, 3, 4, 5}
result = sum(nums...) // Unpack slice into variadic args
fmt.Println(result) // 15
// Can also use slice literal
result = sum([]int{10, 20, 30}...)
fmt.Println(result) // 60
}
The ... operator unpacks the slice:
numbers := []int{1, 2, 3}
// These are equivalent:
sum(numbers...) // Unpack slice
sum(1, 2, 3) // Individual arguments
// Without ..., it's a type error:
// sum(numbers) // ❌ Error: cannot use []int as int
Important: Unpacking creates a slice reference
func modify(numbers ...int) {
if len(numbers) > 0 {
numbers[0] = 999 // Modify the slice
}
}
func main() {
nums := []int{1, 2, 3}
modify(nums...) // Pass slice
fmt.Println(nums) // [999 2 3] - Original slice modified!
modify(1, 2, 3) // Pass individual values
// Creates a new slice, original data not affected
}
Advanced Patterns
Functional options pattern:
type Server struct {
host string
port int
timeout int
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func NewServer(options ...Option) *Server {
// Default values
server := &Server{
host: "localhost",
port: 8080,
timeout: 30,
}
// Apply options
for _, opt := range options {
opt(server)
}
return server
}
func main() {
// Use defaults
s1 := NewServer()
fmt.Printf("%+v\n", s1) // {host:localhost port:8080 timeout:30}
// Customize some options
s2 := NewServer(
WithHost("example.com"),
WithPort(3000),
)
fmt.Printf("%+v\n", s2) // {host:example.com port:3000 timeout:30}
// Customize all options
s3 := NewServer(
WithHost("api.example.com"),
WithPort(443),
WithTimeout(60),
)
fmt.Printf("%+v\n", s3) // {host:api.example.com port:443 timeout:60}
}
Logger with variadic formatting:
type Logger struct {
prefix string
}
func (l *Logger) Log(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
fmt.Printf("[%s] %s\n", l.prefix, message)
}
func main() {
logger := Logger{prefix: "APP"}
logger.Log("Starting server")
// [APP] Starting server
logger.Log("User %s logged in at %d", "alice", 1234567890)
// [APP] User alice logged in at 1234567890
logger.Log("Processing %d items", 42)
// [APP] Processing 42 items
}
Variadic method receivers:
type Numbers struct {
values []int
}
func (n *Numbers) Add(numbers ...int) {
n.values = append(n.values, numbers...)
}
func (n *Numbers) Sum() int {
total := 0
for _, v := range n.values {
total += v
}
return total
}
func main() {
nums := &Numbers{}
nums.Add(1, 2, 3)
nums.Add(4, 5)
slice := []int{6, 7, 8}
nums.Add(slice...)
fmt.Println(nums.Sum()) // 36
}
Performance Considerations
Memory allocation:
func process(items ...string) {
// items is allocated as a slice each call
fmt.Println(len(items))
}
// Each call allocates a new slice
process("a", "b", "c") // Allocates []string with 3 elements
process("d", "e") // Allocates []string with 2 elements
// If you already have a slice, use ... to avoid extra allocation
existing := []string{"a", "b", "c"}
process(existing...) // No extra allocation, passes existing slice
Passing slice vs individual args:
numbers := []int{1, 2, 3, 4, 5}
// Option 1: Unpack slice (no copy, just passes reference)
sum(numbers...) // Efficient
// Option 2: Individual args (compiler creates new slice)
sum(1, 2, 3, 4, 5) // Less efficient for many args
// For small number of args (< 5), individual args are fine
// For large number of args or existing slice, use ... operator
Benchmarking example:
// For hot paths or performance-critical code:
// If always passing 2-3 args, might be better to have explicit params
func add3(a, b, c int) int {
return a + b + c
}
// Instead of
func addVariadic(numbers ...int) int {
total := 0
for _, n := range numbers {
total += n
}
return total
}
// addVariadic is more flexible but slightly slower
// Difference is negligible for most use cases
Type Aliases in Go
What Are Type Aliases?
Go has two ways to create new type names:
Type definition (creates a new, distinct type): type NewType OldType
Type alias (creates an alternate name for existing type): type NewType = OldType
Type alias example:
// Create an alias for int
type MyInt = int
func main() {
var x MyInt = 42
var y int = 100
// MyInt and int are the SAME type
x = y // ✓ Works - they're identical types
y = x // ✓ Works - they're identical types
fmt.Printf("Type of x: %T\n", x) // Type of x: int
fmt.Printf("Type of y: %T\n", y) // Type of y: int
}
Type definition example (for comparison):
// Create a NEW type based on int
type MyInt int
func main() {
var x MyInt = 42
var y int = 100
// MyInt and int are DIFFERENT types
// x = y // ❌ Error: cannot use y (type int) as type MyInt
// y = x // ❌ Error: cannot use x (type MyInt) as type int
// Must explicitly convert
x = MyInt(y) // ✓ Works with conversion
y = int(x) // ✓ Works with conversion
fmt.Printf("Type of x: %T\n", x) // Type of x: main.MyInt
fmt.Printf("Type of y: %T\n", y) // Type of y: int
}
Type Aliases in the Standard Library
byte and rune:
// Built-in type aliases
type byte = uint8
type rune = int32
func main() {
var b byte = 'A'
var u uint8 = 65
b = u // ✓ Same type
u = b // ✓ Same type
fmt.Printf("%T\n", b) // uint8 (not byte!)
var r rune = '世'
var i int32 = 19990
r = i // ✓ Same type
i = r // ✓ Same type
fmt.Printf("%T\n", r) // int32 (not rune!)
}
any (Go 1.18+):
// any is an alias for interface{}
type any = interface{}
func printAny(v any) {
fmt.Println(v)
}
// Same as
func printInterface(v interface{}) {
fmt.Println(v)
}
// Both are identical
func main() {
printAny("hello")
printInterface(42)
}
Type aliases for migration in standard library:
// In the context package, there were type aliases
// used during refactoring
// Original location
// package oldpkg
// type Context interface { ... }
// New location
// package context
// Alias for compatibility
// type Context = newlocation.Context
Type Aliases with Generics (Go 1.18+)
Alias for generic types:
// Original generic type
type Pair[T, U any] struct {
First T
Second U
}
// Create alias (must include type parameters)
type Coord[T, U any] = Pair[T, U]
func main() {
// Use either name
p := Pair[int, string]{First: 1, Second: "one"}
c := Coord[int, string]{First: 2, Second: "two"}
// They're the same type
p = c // ✓ Works
c = p // ✓ Works
}
Alias for specific instantiation:
// Generic type
type List[T any] []T
// Alias for specific type
type IntList = List[int]
type StringList = List[string]
func main() {
var nums IntList = IntList{1, 2, 3}
var words StringList = StringList{"a", "b", "c"}
// IntList is the same as List[int]
var nums2 List[int] = nums // ✓ Works
}
Simplifying complex generic types:
// Complex generic map type
type Cache[K comparable, V any] map[K]V
// Create simpler aliases for common cases
type StringCache[V any] = Cache[string, V]
type IntCache[V any] = Cache[int, V]
func main() {
// Simpler to write
userCache := StringCache[User]{}
// Instead of
userCache2 := Cache[string, User]{}
}
Type Aliases and Reflection
Reflection sees type aliases as their underlying type:
import "reflect"
type MyString = string
type MyStringType string
func main() {
var s1 MyString = "alias"
var s2 MyStringType = "definition"
var s3 string = "original"
// Type alias: same reflect.Type as original
t1 := reflect.TypeOf(s1)
t3 := reflect.TypeOf(s3)
fmt.Println(t1 == t3) // true
fmt.Println(t1.Name()) // string
// Type definition: different reflect.Type
t2 := reflect.TypeOf(s2)
fmt.Println(t2 == t3) // false
fmt.Println(t2.Name()) // MyStringType
}
Limitations of Type Aliases
Cannot add methods:
type MyInt = int
// ❌ Cannot define methods on non-local type
// func (m MyInt) Double() int {
// return m * 2
// }
// Solution: use type definition
type MyIntType int
func (m MyIntType) Double() int {
return int(m) * 2
} // ✓ Works
No type safety:
type UserID = int
type ProductID = int
func getUser(id UserID) { }
func getProduct(id ProductID) { }
func main() {
var userId UserID = 1
var productId ProductID = 2
// No type safety - both are just int!
getUser(productId) // ✓ Compiles (but logically wrong!)
getProduct(userId) // ✓ Compiles (but logically wrong!)
// With type definitions, you get safety
type UserIDType int
type ProductIDType int
var userId2 UserIDType = 1
var productId2 ProductIDType = 2
// getUser(productId2) // ❌ Type error
}
Cannot use in composite literals of unexported types:
// In package a
package a
type internal struct {
Field int
}
type Public = internal // Alias
// In package b
package b
import "pkg/a"
func main() {
// ❌ Cannot construct - internal is unexported
// v := a.Public{Field: 1}
// Even though Public is exported, it's an alias
// to unexported type internal
}
Maps in Go
What Are Maps?
Maps are Go's built-in key-value data structure (similar to dictionaries in Python or objects in JavaScript).
Maps provide fast lookup, insertion, and deletion based on keys.
In Go, maps are reference types - they reference an underlying hash table.
KeyType must be a comparable type (supports == and !=)
ValueType Can be any type, including slices, maps, or functions
Examples of valid map types:
map[string]int // String keys, int values
map[int]string // Int keys, string values
map[string][]int // String keys, slice values
map[int]map[string]bool // Int keys, nested map values
map[[2]int]string // Array keys, string values
// Struct as key (all fields must be comparable)
type Point struct {
X, Y int
}
map[Point]string // Point keys, string values
map[[]int]string // ❌ Slice keys not allowed
map[map[string]int]bool // ❌ Map keys not allowed
map[func()]string // ❌ Function keys not allowed
Creating Maps
Method 1: Using make
// Create empty map
ages := make(map[string]int)
// With initial capacity hint (for performance)
ages := make(map[string]int, 100) // Hint: expect ~100 elements
Method 2: Map literal
// Create and initialize in one step
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 28,
}
// Empty map literal
ages := map[string]int{}
Method 3: var declaration (creates nil map)
var ages map[string]int // ages is nil
// Can read from nil map (returns zero value)
fmt.Println(ages["Alice"]) // 0
// ❌ Cannot write to nil map - will panic!
// ages["Alice"] = 30 // panic: assignment to entry in nil map
// Must initialize with make first
ages = make(map[string]int)
ages["Alice"] = 30 // ✓ Now it works
ages := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Direct access
age := ages["Alice"] // 30
// Accessing non-existent key returns zero value
age := ages["David"] // 0 (zero value for int)
Checking if key exists:
ages := map[string]int{
"Alice": 30,
"Bob": 0, // Explicitly set to 0
}
// Two-value assignment
age, exists := ages["Alice"]
if exists {
fmt.Println("Alice's age:", age) // Alice's age: 30
}
// Check for Bob (age is 0)
age, exists = ages["Bob"]
fmt.Println(age, exists) // 0 true (key exists, value is 0)
// Check for non-existent key
age, exists = ages["Carol"]
fmt.Println(age, exists) // 0 false (key doesn't exist)
// Common pattern: check and use
if age, ok := ages["Alice"]; ok {
fmt.Println("Found:", age)
}
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 28,
}
// Iterate over key-value pairs
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Output (order is random!):
// Bob is 25 years old
// Carol is 28 years old
// Alice is 30 years old
Iterate over keys only:
for name := range ages {
fmt.Println(name)
}
Iterate over values only (less common):
for _, age := range ages {
fmt.Println(age)
}
IMPORTANT: Map iteration order is random!
// Do NOT rely on iteration order
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 28,
}
// Each run may produce different order
for name := range ages {
fmt.Println(name)
}
// Run 1: Alice, Bob, Carol
// Run 2: Carol, Alice, Bob
// Run 3: Bob, Carol, Alice
// Order is deliberately randomized!
Sorting Map Keys for Consistent Order
Since map iteration is random, sort keys for consistent output:
import (
"fmt"
"sort"
)
ages := map[string]int{
"Carol": 28,
"Alice": 30,
"Bob": 25,
}
// Extract keys into slice
keys := make([]string, 0, len(ages))
for k := range ages {
keys = append(keys, k)
}
// Sort keys
sort.Strings(keys)
// Iterate in sorted order
for _, name := range keys {
fmt.Printf("%s: %d\n", name, ages[name])
}
// Output (always in this order):
// Alice: 30
// Bob: 25
// Carol: 28
Sorting by values:
import (
"fmt"
"sort"
)
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 28,
}
// Create slice of key-value pairs
type kv struct {
Key string
Value int
}
var pairs []kv
for k, v := range ages {
pairs = append(pairs, kv{k, v})
}
// Sort by value
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].Value < pairs[j].Value
})
// Print sorted
for _, pair := range pairs {
fmt.Printf("%s: %d\n", pair.Key, pair.Value)
}
// Output:
// Bob: 25
// Carol: 28
// Alice: 30
Maps Are Reference Types
Maps are references to underlying hash table data structure:
// When you assign a map, both variables point to same data
ages1 := map[string]int{
"Alice": 30,
}
ages2 := ages1 // Both reference same underlying map
ages2["Bob"] = 25 // Modify through ages2
fmt.Println(ages1) // map[Alice:30 Bob:25]
fmt.Println(ages2) // map[Alice:30 Bob:25]
// Both show the change!
Passing maps to functions:
func addPerson(m map[string]int, name string, age int) {
m[name] = age // Modifies the original map
}
func main() {
ages := map[string]int{
"Alice": 30,
}
addPerson(ages, "Bob", 25)
fmt.Println(ages) // map[Alice:30 Bob:25]
// Original map is modified
}
To create a copy, must manually copy entries:
original := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Create a copy
copy := make(map[string]int)
for k, v := range original {
copy[k] = v
}
// Now they're independent
copy["Carol"] = 28
fmt.Println(original) // map[Alice:30 Bob:25]
fmt.Println(copy) // map[Alice:30 Bob:25 Carol:28]
var ages map[string]int // nil
age := ages["Alice"] // ✓ Returns 0 (zero value)
_, ok := ages["Bob"] // ✓ Returns 0, false
len := len(ages) // ✓ Returns 0
for k, v := range ages { // ✓ Loop doesn't execute
fmt.Println(k, v)
}
Writing to nil map causes panic:
var ages map[string]int // nil
ages["Alice"] = 30 // ❌ panic: assignment to entry in nil map
// Must initialize first
ages = make(map[string]int)
ages["Alice"] = 30 // ✓ Works
Deleting from nil map is safe (no-op):
var ages map[string]int // nil
delete(ages, "Alice") // ✓ Safe, does nothing
Maps with Complex Value Types
Map of slices:
// Store multiple values per key
students := make(map[string][]int)
// Add grades
students["Alice"] = []int{95, 87, 92}
students["Bob"] = []int{78, 85, 90}
// Add more grades
students["Alice"] = append(students["Alice"], 88)
fmt.Println(students["Alice"]) // [95 87 92 88]
Map of maps (nested):
// Two-level lookup: country -> city -> population
world := make(map[string]map[string]int)
// Initialize inner maps
world["USA"] = make(map[string]int)
world["Japan"] = make(map[string]int)
// Add data
world["USA"]["NYC"] = 8000000
world["USA"]["LA"] = 4000000
world["Japan"]["Tokyo"] = 14000000
// Access nested data
fmt.Println(world["USA"]["NYC"]) // 8000000
// Check nested key existence
if cities, ok := world["USA"]; ok {
if pop, ok := cities["NYC"]; ok {
fmt.Println("NYC population:", pop)
}
}
Map of structs:
type Person struct {
Age int
City string
}
people := make(map[string]Person)
people["Alice"] = Person{Age: 30, City: "NYC"}
people["Bob"] = Person{Age: 25, City: "LA"}
// Access
alice := people["Alice"]
fmt.Println(alice.City) // NYC
// Cannot modify struct fields directly (map returns a copy)
// people["Alice"].Age = 31 // ❌ Compile error
// Must replace entire struct
alice.Age = 31
people["Alice"] = alice // ✓ Works
// Or use pointer to struct
peoplePtr := make(map[string]*Person)
peoplePtr["Alice"] = &Person{Age: 30, City: "NYC"}
peoplePtr["Alice"].Age = 31 // ✓ Works (modifying through pointer)
Concurrent Access to Maps
WARNING: Maps are NOT safe for concurrent access!
// ❌ UNSAFE - will crash!
m := make(map[int]int)
go func() {
for i := 0; i < 1000; i++ {
m[i] = i // Writing
}
}()
go func() {
for i := 0; i < 1000; i++ {
_ = m[i] // Reading
}
}()
// This will cause: fatal error: concurrent map read and map write
Solution 1: Use sync.Mutex
import "sync"
type SafeMap struct {
mu sync.Mutex
m map[int]int
}
func (sm *SafeMap) Set(key, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
func (sm *SafeMap) Get(key int) (int, bool) {
sm.mu.Lock()
defer sm.mu.Unlock()
val, ok := sm.m[key]
return val, ok
}
func main() {
sm := SafeMap{m: make(map[int]int)}
go func() {
for i := 0; i < 1000; i++ {
sm.Set(i, i)
}
}()
go func() {
for i := 0; i < 1000; i++ {
sm.Get(i)
}
}()
}
Solution 2: Use sync.Map (for specific use cases)
import "sync"
var m sync.Map
// Store
m.Store("key", "value")
// Load
value, ok := m.Load("key")
if ok {
fmt.Println(value)
}
// Delete
m.Delete("key")
// LoadOrStore (atomic)
actual, loaded := m.LoadOrStore("key", "value")
// Range (iterate)
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true // continue iteration
})
// Note: sync.Map uses interface{}, so less type-safe than regular maps
Use sync.Map when: keys are stable (write once, read many), or high contention
Use Mutex when: frequent writes, or need type safety, or map is small
Comparing Maps
Maps cannot be compared directly with == (except to nil):
m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}
// ❌ Compile error
// if m1 == m2 { }
// ✓ Can compare to nil
var m3 map[string]int
if m3 == nil {
fmt.Println("m3 is nil")
}
Strings are immutable - once created, they cannot be modified.
Strings are UTF-8 encoded by default, supporting Unicode characters.
The zero value of a string is "" (empty string).
Basic string operations:
// Declaration and initialization
var s1 string // Zero value: ""
var s2 string = "Hello"
s3 := "World"
// Concatenation
greeting := s2 + " " + s3 // "Hello World"
// Length (number of bytes, not characters!)
length := len(greeting) // 11
// Accessing bytes by index
first := greeting[0] // 'H' (byte value: 72)
last := greeting[len(greeting)-1] // 'd' (byte value: 100)
String Literals
Interpreted string literals (double quotes):
// Use double quotes for interpreted strings
s := "Hello, World!"
// Escape sequences are processed
s1 := "Line 1\nLine 2" // Newline
s2 := "Tab\there" // Tab
s3 := "Quote: \"Hello\"" // Escaped quotes
s4 := "Path: C:\\Users\\Name" // Backslash
s5 := "Unicode: \u4e16\u754c" // Unicode code points (世界)
fmt.Println(s1)
// Output:
// Line 1
// Line 2
Raw string literals (backticks):
// Use backticks for raw strings
s := `Hello, World!`
// No escape sequences - everything literal
s1 := `Line 1\nLine 2` // Contains literal \n
s2 := `Quote: "Hello"` // No need to escape quotes
s3 := `Path: C:\Users\Name` // Backslashes are literal
// Multiline strings
s4 := `Line 1
Line 2
Line 3`
// Regular expressions (common use case)
regex := `^\d{3}-\d{3}-\d{4}$` // Phone number pattern
fmt.Println(s1)
// Output: Line 1\nLine 2(literal backslash-n)
Common escape sequences:
Escape
Meaning
Example
\n
Newline
"Line1\nLine2"
\t
Tab
"Name:\tValue"
\\
Backslash
"C:\\path"
\"
Double quote
"He said \"Hi\""
\'
Single quote
'\'x\''
\r
Carriage return
"text\r\n"
\uXXXX
Unicode (4 hex digits)
"\u4e16" (世)
\UXXXXXXXX
Unicode (8 hex digits)
"\U0001F600" (😀)
String Immutability
Strings in Go are immutable - you cannot modify their contents:
s := "Hello"
// ❌ Cannot modify string bytes
// s[0] = 'h' // Compile error: cannot assign to s[0]
// ✓ Must create new string
s = "hello" // Creates new string, reassigns variable
// String operations create new strings
s2 := s + " World" // New string created
s3 := strings.ToUpper(s) // New string created
// Original strings unchanged
fmt.Println(s) // "hello"
fmt.Println(s2) // "hello World"
fmt.Println(s3) // "HELLO"
Why immutability matters:
Strings can be safely shared between goroutines (no race conditions)
Strings can be used as map keys (won't change unexpectedly)
String values can be optimized by the compiler
To modify strings, convert to []byte or use strings.Builder:
s := "Hello"
// Method 1: Convert to []byte slice
bytes := []byte(s)
bytes[0] = 'h'
s = string(bytes)
fmt.Println(s) // "hello"
// Method 2: Use strings.Builder for multiple modifications
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(" ")
builder.WriteString("World")
s = builder.String()
fmt.Println(s) // "Hello World"
UTF-8 Encoding and Runes
Go strings are UTF-8 encoded sequences of bytes.
A rune is a Unicode code point (alias for int32).
One character may use 1-4 bytes in UTF-8.
Length vs character count:
s1 := "Hello"
fmt.Println(len(s1)) // 5 bytes, 5 characters
s2 := "世界" // "World" in Chinese
fmt.Println(len(s2)) // 6 bytes (3 bytes per character)
// To count actual characters (runes), use utf8.RuneCountInString
import "unicode/utf8"
count := utf8.RuneCountInString(s2)
fmt.Println(count) // 2 characters
// Or convert to []rune
runes := []rune(s2)
fmt.Println(len(runes)) // 2 characters
Rune literals (single quotes):
// Single character in single quotes
var r1 rune = 'A' // Unicode code point 65
var r2 rune = '世' // Unicode code point 19990
var r3 rune = '\n' // Newline character
var r4 rune = '\u4e16' // Unicode escape
fmt.Printf("%c %d\n", r1, r1) // A 65
fmt.Printf("%c %d\n", r2, r2) // 世 19990
// Rune is int32
var i int32 = r1
fmt.Println(i) // 65
Iterating over runes vs bytes:
s := "Hello世界"
// Iterate bytes (wrong for multi-byte characters!)
fmt.Println("Bytes:")
for i := 0; i < len(s); i++ {
fmt.Printf("%d: %c (%d)\n", i, s[i], s[i])
}
// H, e, l, l, o, then garbage for multi-byte characters
// Iterate runes (correct!)
fmt.Println("\nRunes:")
for i, r := range s {
fmt.Printf("%d: %c (%d)\n", i, r, r)
}
// Output:
// 0: H (72)
// 1: e (101)
// 2: l (108)
// 3: l (108)
// 4: o (111)
// 5: 世 (19990)
// 8: 界 (30028)
// Note: indices jump (5 -> 8) because 世 takes 3 bytes
String Indexing and Slicing
Indexing returns bytes, not characters:
s := "Hello"
// Access individual bytes
first := s[0] // 'H' (byte value: 72)
second := s[1] // 'e' (byte value: 101)
fmt.Printf("%c\n", first) // H
fmt.Printf("%d\n", first) // 72
// ❌ Index out of bounds panics
// last := s[100] // panic: index out of range
Slicing creates substrings:
s := "Hello, World!"
// Basic slicing
sub1 := s[0:5] // "Hello" (index 0 to 4)
sub2 := s[7:12] // "World"
sub3 := s[7:] // "World!" (from 7 to end)
sub4 := s[:5] // "Hello" (from start to 4)
sub5 := s[:] // "Hello, World!" (entire string)
// Slicing is based on byte positions, not character positions!
s2 := "世界你好"
bytes := s2[0:3] // "世" (first character is 3 bytes)
// bytes := s2[0:2] would be invalid UTF-8!
Safe substring extraction:
// To extract first N characters (not bytes)
func firstNRunes(s string, n int) string {
runes := []rune(s)
if len(runes) <= n {
return s
}
return string(runes[:n])
}
s := "世界你好"
fmt.Println(firstNRunes(s, 2)) // "世界"
// Check bounds before slicing
func safeSlice(s string, start, end int) string {
if start < 0 || start > len(s) || end > len(s) || start > end {
return ""
}
return s[start:end]
}
String Concatenation
Using + operator (simple cases):
// Simple concatenation
s1 := "Hello"
s2 := "World"
result := s1 + " " + s2 // "Hello World"
// Multiple concatenations
path := "home" + "/" + "user" + "/" + "documents"
// Note: Each + creates a new string (inefficient for many concatenations)
Using fmt.Sprintf (formatting):
name := "Alice"
age := 30
// Format string with values
s := fmt.Sprintf("Name: %s, Age: %d", name, age)
fmt.Println(s) // "Name: Alice, Age: 30"
// Complex formatting
price := 19.99
s2 := fmt.Sprintf("Price: $%.2f", price)
fmt.Println(s2) // "Price: $19.99"
Using strings.Join (slices):
import "strings"
// Join slice of strings
words := []string{"Go", "is", "awesome"}
sentence := strings.Join(words, " ")
fmt.Println(sentence) // "Go is awesome"
// Join with different separator
csv := strings.Join([]string{"a", "b", "c"}, ",")
fmt.Println(csv) // "a,b,c"
Using strings.Builder (efficient for many concatenations):
import "strings"
// Efficient string building
var builder strings.Builder
builder.WriteString("Hello")
builder.WriteString(" ")
builder.WriteString("World")
builder.WriteByte('!')
result := builder.String()
fmt.Println(result) // "Hello World!"
// Builder in a loop (much more efficient than +)
var b strings.Builder
for i := 0; i < 1000; i++ {
b.WriteString("Line ")
b.WriteString(fmt.Sprintf("%d\n", i))
}
output := b.String()
Performance comparison:
// ❌ Slow: Creates many intermediate strings
func buildStringSlow(n int) string {
s := ""
for i := 0; i < n; i++ {
s += "x" // Creates new string each iteration
}
return s
}
// ✓ Fast: Efficient memory usage
func buildStringFast(n int) string {
var builder strings.Builder
builder.Grow(n) // Pre-allocate capacity
for i := 0; i < n; i++ {
builder.WriteByte('x')
}
return builder.String()
}
// For n=10000:
// buildStringSlow: ~50ms
// buildStringFast: ~0.1ms
bytes := []byte{72, 101, 108, 108, 111}
// Convert to string
s := string(bytes)
fmt.Println(s) // "Hello"
[]rune to string:
runes := []rune{72, 101, 108, 108, 111}
// Convert to string
s := string(runes)
fmt.Println(s) // "Hello"
Performance note:
// string <-> []byte conversion makes a copy
s := "Hello"
b := []byte(s) // Allocates new slice, copies data
s2 := string(b) // Allocates new string, copies data
// For read-only operations, consider keeping as string
// For many modifications, work with []byte then convert once
Iterating Over Strings
Range loop (iterates over runes):
s := "Hello世界"
// Range automatically decodes UTF-8 to runes
for i, r := range s {
fmt.Printf("%d: %c (U+%04X)\n", i, r, r)
}
// Output:
// 0: H (U+0048)
// 1: e (U+0065)
// 2: l (U+006C)
// 3: l (U+006C)
// 4: o (U+006F)
// 5: 世 (U+4E16)
// 8: 界 (U+754C)
// Note: Index jumps from 5 to 8 (世 takes 3 bytes)
Index loop (iterates over bytes):
s := "Hello"
// Loop through byte indices
for i := 0; i < len(s); i++ {
fmt.Printf("%d: %c (%d)\n", i, s[i], s[i])
}
// ❌ Don't use this for multi-byte characters!
s2 := "世界"
for i := 0; i < len(s2); i++ {
fmt.Printf("%c ", s2[i]) // Prints garbage
}
Using strings.Reader:
import "strings"
s := "Hello"
reader := strings.NewReader(s)
// Read rune by rune
for {
r, size, err := reader.ReadRune()
if err != nil {
break
}
fmt.Printf("%c (size: %d bytes)\n", r, size)
}
name := "Alice"
age := 30
// Printf: prints to stdout
fmt.Printf("Name: %s, Age: %d\n", name, age)
// Sprintf: returns string
s := fmt.Sprintf("Name: %s, Age: %d", name, age)
// Print: simple printing (no formatting)
fmt.Print("Hello ", "World\n")
// Println: adds spaces and newline
fmt.Println("Hello", "World") // "Hello World\n"
Regular Expressions with Strings
Basic regex matching:
import "regexp"
// Compile pattern
pattern := `^\d{3}-\d{3}-\d{4}$` // Phone: 123-456-7890
re := regexp.MustCompile(pattern)
// Test if matches
matched := re.MatchString("123-456-7890")
fmt.Println(matched) // true
matched2 := re.MatchString("invalid")
fmt.Println(matched2) // false
Finding matches:
s := "Contact: alice@example.com or bob@test.com"
// Find email addresses
re := regexp.MustCompile(`\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b`)
// Find first match
first := re.FindString(s)
fmt.Println(first) // "alice@example.com"
// Find all matches
all := re.FindAllString(s, -1)
fmt.Println(all) // ["alice@example.com", "bob@test.com"]
Replacing with regex:
s := "Price: $19.99, Cost: $5.00"
// Replace all dollar amounts
re := regexp.MustCompile(`\$\d+\.\d+`)
replaced := re.ReplaceAllString(s, "XXX")
fmt.Println(replaced) // "Price: XXX, Cost: XXX"
// Replace with function
re2 := regexp.MustCompile(`\d+`)
result := re2.ReplaceAllStringFunc("The year is 2024", func(match string) string {
return "[" + match + "]"
})
fmt.Println(result) // "The year is [2024]"
String Performance Tips
Tip 1: Use strings.Builder for concatenation in loops
// ❌ Slow: O(n²) complexity
func buildSlow(words []string) string {
result := ""
for _, word := range words {
result += word + " "
}
return result
}
// ✓ Fast: O(n) complexity
func buildFast(words []string) string {
var b strings.Builder
for _, word := range words {
b.WriteString(word)
b.WriteString(" ")
}
return b.String()
}
Tip 2: Use strings.Join for slices
words := []string{"Go", "is", "fast"}
// ✓ Efficient
result := strings.Join(words, " ")
// Less efficient
result2 := words[0] + " " + words[1] + " " + words[2]
Tip 3: Avoid unnecessary conversions
s := "Hello"
// ❌ Unnecessary conversions
bytes := []byte(s)
s2 := string(bytes)
// ✓ Keep as string if possible
// Only convert when you need to modify
Tip 4: Pre-allocate Builder capacity
// If you know approximate size
var b strings.Builder
b.Grow(1000) // Pre-allocate for ~1000 bytes
for i := 0; i < 100; i++ {
b.WriteString("Line\n")
}
result := b.String()
Tip 5: Reuse strings.Builder
var b strings.Builder
for i := 0; i < 10; i++ {
b.Reset() // Reuse the builder
b.WriteString("Iteration ")
b.WriteString(fmt.Sprintf("%d", i))
result := b.String()
fmt.Println(result)
}
Common Mistakes and How to Avoid Them
Mistake 1: Confusing byte length with character count
s := "Hello"
// ❌ Error: cannot assign
// s[0] = 'h'
// ✓ Convert to []byte or []rune
bytes := []byte(s)
bytes[0] = 'h'
s = string(bytes)
Mistake 3: Slicing multi-byte characters
s := "世界"
// ❌ Wrong: Cuts in middle of character
// invalid := s[0:2] // Invalid UTF-8!
// ✓ Correct: Work with runes
runes := []rune(s)
first := string(runes[0:1]) // "世"
Mistake 4: Using + for many concatenations
// ❌ Slow for large n
result := ""
for i := 0; i < 10000; i++ {
result += "x"
}
// ✓ Use strings.Builder
var b strings.Builder
for i := 0; i < 10000; i++ {
b.WriteString("x")
}
result := b.String()
Mistake 5: Not checking for empty string before indexing
func getFirst(s string) byte {
// ❌ Panics if s is empty
// return s[0]
// ✓ Check length first
if len(s) == 0 {
return 0
}
return s[0]
}
Mistake 6: Iterating bytes for multi-byte strings
s := "Hello世界"
// ❌ Wrong: Iterates bytes, breaks multi-byte chars
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i]) // Prints garbage for 世界
}
// ✓ Correct: Use range (iterates runes)
for _, r := range s {
fmt.Printf("%c ", r) // Prints all characters correctly
}
Pointers in Go
Pointer Syntax
Declaring pointer variables:
var x int = 42
// Declare pointer to int
var p *int // p is nil (zero value of pointer)
// Assign address to pointer
p = &x // Now p points to x
// Declare and initialize
p2 := &x // Type inferred as *int
Pointer type notation:
var p1 *int // Pointer to int
var p2 *string // Pointer to string
var p3 *bool // Pointer to bool
var p4 *[]int // Pointer to slice of ints
var p5 *MyStruct // Pointer to struct
The & operator (address-of):
x := 100
p := &x // Get address of x
fmt.Printf("x = %d\n", x) // x = 100
fmt.Printf("address of x = %p\n", p) // address of x = 0xc000012028
The * operator (dereference):
x := 100
p := &x
// Read value through pointer
value := *p // value = 100
// Modify value through pointer
*p = 200 // Changes x to 200
fmt.Println(x) // 200 (x was modified!)
fmt.Println(*p) // 200
Pointers and Functions
Pass by value (default in Go):
func modify(x int) {
x = 100 // Modifies local copy only
}
func main() {
num := 10
modify(num)
fmt.Println(num) // 10 (unchanged)
}
Pass by pointer (to modify original):
func modify(x *int) {
*x = 100 // Modifies original value
}
func main() {
num := 10
modify(&num)
fmt.Println(num) // 100 (changed!)
}
Returning pointers:
func createPerson(name string, age int) *Person {
p := Person{
Name: name,
Age: age,
}
return &p // ✓ Safe in Go (p survives beyond function)
}
func main() {
person := createPerson("Alice", 30)
fmt.Printf("%+v\n", person) // &{Name:Alice Age:30}
}
Pointers and Structs
Accessing struct fields through pointers:
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 30}
ptr := &p
// Go automatically dereferences
fmt.Println(ptr.Name) // "Alice" (Go does: (*ptr).Name)
// Both syntaxes work
fmt.Println((*ptr).Name) // "Alice" (explicit)
fmt.Println(ptr.Name) // "Alice" (automatic)
// Modify through pointer
ptr.Age = 31
fmt.Println(p.Age) // 31 (original modified)
}
Methods with pointer receivers:
type Counter struct {
count int
}
// Pointer receiver - can modify
func (c *Counter) Increment() {
c.count++
}
// Value receiver - cannot modify original
func (c Counter) IncrementBad() {
c.count++ // Modifies copy, not original
}
func main() {
c := Counter{}
c.Increment()
c.Increment()
fmt.Println(c.count) // 2
c.IncrementBad()
fmt.Println(c.count) // 2 (unchanged)
}
Creating struct pointers:
// Method 1: Create value, then take address
p1 := Person{Name: "Alice", Age: 30}
ptr1 := &p1
// Method 2: Use & with literal
ptr2 := &Person{Name: "Bob", Age: 25}
// Method 3: Use new (creates zero-valued struct)
ptr3 := new(Person) // &Person{Name: "", Age: 0}
ptr3.Name = "Carol"
ptr3.Age = 28
Nil Pointers
The zero value of a pointer is nil (doesn't point to anything).
Checking for nil:
var p *int // p is nil
if p == nil {
fmt.Println("Pointer is nil")
}
// Assign address
x := 42
p = &x
if p != nil {
fmt.Println("Pointer is not nil")
fmt.Println(*p) // 42
}
Dereferencing nil pointer causes panic:
var p *int // nil pointer
// ❌ Panic: runtime error
// value := *p
// ✓ Always check before dereferencing
if p != nil {
value := *p
fmt.Println(value)
}
Safe struct field access:
type Person struct {
Name string
Age int
}
var p *Person // nil
// ❌ Panic
// name := p.Name
// ✓ Check first
if p != nil {
name := p.Name
fmt.Println(name)
}
Slices are already references (don't need pointers):
func modifySlice(s []int) {
s[0] = 100 // Modifies original (slice is reference type)
}
func main() {
slice := []int{1, 2, 3}
modifySlice(slice)
fmt.Println(slice) // [100 2 3] (changed!)
// Usually don't need *[]int
}
Structs in Go
What Are Structs?
A struct is a composite data type that groups together variables (fields) under a single name.
Basic example:
// Define a struct type
type Person struct{
Name string
Age int
}
// Create and use a struct
func main() {
p := Person {
Name: "Alice",
Age: 30,
}
fmt.Println(p.Name) // "Alice"
fmt.Println(p.Age) // 30
}
Declaring Structs
Basic struct declaration:
type Person struct {
Name string
Age int
City string
}
type Point struct {
X float64
Y float64
}
type Rectangle struct {
Width float64
Height float64
}
Fields with same type on one line:
type Point struct {
X, Y float64 // Both are float64
}
type Person struct {
FirstName, LastName string // Both are string
Age int
}
Exported vs unexported fields:
type User struct {
Name string // Exported (starts with capital letter)
Email string // Exported
password string // Unexported (starts with lowercase)
}
// From another package:
// u.Name = "Alice" // ✓ Works
// u.password = "123" // ❌ Error: unexported field
Creating Struct Instances
Method 1: Struct literal with field names
p := Person{
Name: "Alice",
Age: 30,
City: "NYC",
}
// Order doesn't matter with field names
p2 := Person{
Age: 25,
City: "LA",
Name: "Bob",
}
Method 2: Struct literal without field names
// Must provide all fields in order
p := Person{"Alice", 30, "NYC"}
// ❌ Error if fields missing or wrong order
// p := Person{"Alice", 30} // Missing City
// p := Person{30, "Alice", "NYC"} // Wrong order
Method 3: Zero value (var declaration)
var p Person
// p.Name = "" (zero value for string)
// p.Age = 0 (zero value for int)
// p.City = "" (zero value for string)
fmt.Printf("%+v\n", p) // {Name: Age:0 City:}
Method 4: Using new
p := new(Person) // Returns *Person (pointer)
// All fields are zero values
p.Name = "Alice"
p.Age = 30
fmt.Printf("%+v\n", p) // &{Name:Alice Age:30 City:}
Method 5: Partial initialization
p := Person{
Name: "Alice",
// Age and City get zero values
}
fmt.Printf("%+v\n", p) // {Name:Alice Age:0 City:}
p := Person{Name: "Alice", Age: 30}
ptr := &p
// Go automatically dereferences
ptr.Name = "Bob" // Same as (*ptr).Name = "Bob"
ptr.Age = 31 // Same as (*ptr).Age = 31
// Both syntaxes work
(*ptr).Name = "Carol" // Explicit dereference
ptr.Name = "David" // Automatic dereference
fmt.Println(p) // {David 31 }
Anonymous Structs
Structs without a named type, useful for one-time use:
// Declare and initialize in one step
person := struct {
Name string
Age int
}{
Name: "Alice",
Age: 30,
}
fmt.Println(person.Name) // "Alice"
Common use case: Table-driven tests
tests := []struct {
input int
expected int
}{
{input: 2, expected: 4},
{input: 3, expected: 9},
{input: 4, expected: 16},
}
for _, test := range tests {
result := square(test.input)
if result != test.expected {
t.Errorf("square(%d) = %d; want %d",
test.input, result, test.expected)
}
}
Use case: Grouping related variables
func processData() {
stats := struct {
Total int
Success int
Failed int
}{
Total: 100,
Success: 95,
Failed: 5,
}
fmt.Printf("Success rate: %.1f%%\n",
float64(stats.Success)/float64(stats.Total)*100)
}
Nested Structs
Structs can contain other structs as fields:
type Address struct {
Street string
City string
ZipCode string
}
type Person struct {
Name string
Age int
Address Address // Nested struct
}
func main() {
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "NYC",
ZipCode: "10001",
},
}
// Access nested fields
fmt.Println(p.Address.City) // "NYC"
fmt.Println(p.Address.ZipCode) // "10001"
}
Multiple levels of nesting:
type Coordinates struct {
Latitude float64
Longitude float64
}
type Address struct {
Street string
City string
Coordinates Coordinates
}
type Person struct {
Name string
Address Address
}
func main() {
p := Person{
Name: "Alice",
Address: Address{
City: "NYC",
Coordinates: Coordinates{
Latitude: 40.7128,
Longitude: -74.0060,
},
},
}
// Access deeply nested fields
fmt.Println(p.Address.Coordinates.Latitude) // 40.7128
}
Embedded Structs (Composition)
Go supports composition through struct embedding (no inheritance):
type Person struct {
Name string
Age int
}
type Employee struct {
Person // Embedded struct (no field name)
EmployeeID int
Department string
}
func main() {
e := Employee{
Person: Person{
Name: "Alice",
Age: 30,
},
EmployeeID: 12345,
Department: "Engineering",
}
// Can access embedded fields directly
fmt.Println(e.Name) // "Alice" (promoted field)
fmt.Println(e.Person.Name) // "Alice" (explicit)
fmt.Println(e.EmployeeID) // 12345
}
Field promotion:
type Address struct {
City string
Zip string
}
type Person struct {
Name string
Address // Embedded
}
func main() {
p := Person{
Name: "Alice",
Address: Address{
City: "NYC",
Zip: "10001",
},
}
// Both work - field promotion
fmt.Println(p.City) // "NYC"
fmt.Println(p.Address.City) // "NYC"
}
Method promotion:
type Person struct {
Name string
}
func (p Person) Greet() string {
return "Hello, I'm " + p.Name
}
type Employee struct {
Person // Embedded
JobTitle string
}
func main() {
e := Employee{
Person: Person{Name: "Alice"},
JobTitle: "Engineer",
}
// Method is promoted from Person
fmt.Println(e.Greet()) // "Hello, I'm Alice"
}
Handling name conflicts:
type Person struct {
Name string
}
type Company struct {
Name string
}
type Employee struct {
Person
Company
}
func main() {
e := Employee{
Person: Person{Name: "Alice"},
Company: Company{Name: "Acme Corp"},
}
// ❌ Ambiguous
// fmt.Println(e.Name) // Error: ambiguous selector e.Name
// ✓ Must be explicit
fmt.Println(e.Person.Name) // "Alice"
fmt.Println(e.Company.Name) // "Acme Corp"
}
type Counter struct {
count int
}
// Value receiver - works on copy
func (c Counter) GetCount() int {
return c.count
}
// Value receiver - DOESN'T modify original
func (c Counter) IncrementBad() {
c.count++ // Modifies copy
}
// Pointer receiver - modifies original
func (c *Counter) Increment() {
c.count++ // Modifies original
}
func main() {
counter := Counter{}
counter.IncrementBad()
fmt.Println(counter.GetCount()) // 0 (unchanged)
counter.Increment()
counter.Increment()
fmt.Println(counter.GetCount()) // 2
}
Struct Tags
Struct tags provide metadata about fields, used by packages like encoding/json:
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"` // Omit if empty
}
func main() {
p := Person{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
}
// Marshal to JSON
data, _ := json.Marshal(p)
fmt.Println(string(data))
// {"name":"Alice","age":30,"email":"alice@example.com"}
// Unmarshal from JSON
jsonStr := `{"name":"Bob","age":25}`
var p2 Person
json.Unmarshal([]byte(jsonStr), &p2)
fmt.Printf("%+v\n", p2) // {Name:Bob Age:25 Email:}
}
Common tag options:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // Omit if empty
Password string `json:"-"` // Never include
Age int `json:"age,string"` // Convert to string
Created int64 `json:"created,omitempty"`
}
Multiple tags:
type Person struct {
Name string `json:"name" xml:"name" db:"full_name"`
Age int `json:"age" xml:"age" db:"person_age"`
Email string `json:"email,omitempty" xml:"email" db:"email_address"`
}
Using reflect to read tags:
import "reflect"
type Person struct {
Name string `json:"name" required:"true"`
}
func main() {
t := reflect.TypeOf(Person{})
field, _ := t.FieldByName("Name")
jsonTag := field.Tag.Get("json")
requiredTag := field.Tag.Get("required")
fmt.Println(jsonTag) // "name"
fmt.Println(requiredTag) // "true"
}
Comparing Structs
Structs can be compared with == and != if all fields are comparable:
Empty structs have zero size, useful for signaling:
type Empty struct{}
// Size of empty struct
var e Empty
fmt.Println(unsafe.Sizeof(e)) // 0 bytes
// Common use: Set implementation
type Set map[string]struct{}
func main() {
set := make(Set)
// Add elements
set["apple"] = struct{}{}
set["banana"] = struct{}{}
// Check membership
if _, exists := set["apple"]; exists {
fmt.Println("apple is in set")
}
// Delete
delete(set, "apple")
}
Use case: Channel signaling
done := make(chan struct{})
go func() {
// Do work
time.Sleep(2 * time.Second)
// Signal completion
close(done)
}()
// Wait for signal
<-done
fmt.Println("Done!")
Constructor Functions
Go doesn't have constructors, but you can create factory functions:
type Person struct {
Name string
Age int
Email string
}
// Constructor function (convention: New + TypeName)
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
Email: strings.ToLower(name) + "@example.com",
}
}
func main() {
p := NewPerson("Alice", 30)
fmt.Printf("%+v\n", p)
// &{Name:Alice Age:30 Email:alice@example.com}
}
Constructor with validation:
func NewPerson(name string, age int) (*Person, error) {
if name == "" {
return nil, errors.New("name cannot be empty")
}
if age < 0 || age > 150 {
return nil, errors.New("invalid age")
}
return &Person{
Name: name,
Age: age,
}, nil
}
func main() {
p, err := NewPerson("Alice", 30)
if err != nil {
log.Fatal(err)
}
fmt.Println(p.Name)
}
Constructor with options (functional options pattern):
type Person struct {
Name string
Age int
Email string
}
type Option func(*Person)
func WithEmail(email string) Option {
return func(p *Person) {
p.Email = email
}
}
func WithAge(age int) Option {
return func(p *Person) {
p.Age = age
}
}
func NewPerson(name string, opts ...Option) *Person {
p := &Person{Name: name}
for _, opt := range opts {
opt(p)
}
return p
}
func main() {
p := NewPerson("Alice", WithAge(30), WithEmail("alice@test.com"))
fmt.Printf("%+v\n", p)
}
type Person struct {
name string // Unexported
age int // Unexported
}
// Getter (no "Get" prefix)
func (p *Person) Name() string {
return p.name
}
// Setter (use "Set" prefix)
func (p *Person) SetName(name string) {
p.name = name
}
func (p *Person) Age() int {
return p.age
}
func (p *Person) SetAge(age int) error {
if age < 0 || age > 150 {
return errors.New("invalid age")
}
p.age = age
return nil
}
Pattern 3: Implementing interfaces
type Animal interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
func main() {
animals := []Animal{
Dog{Name: "Buddy"},
Cat{Name: "Whiskers"},
}
for _, animal := range animals {
fmt.Println(animal.Speak())
}
}
Interfaces in Go
What Are Interfaces?
An interface is a type that specifies a set of method signatures.
Any type that implements all the methods automatically satisfies the interface (implicit implementation).
Interfaces enable polymorphism and abstraction in Go.
Key difference from other languages: No explicit "implements" keyword needed!
Basic example:
// Define an interface
type Speaker interface {
Speak() string
}
// Dog implements Speaker (implicitly)
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
// Cat implements Speaker (implicitly)
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
// Function that accepts any Speaker
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // "Woof!"
MakeSound(cat) // "Meow!"
}
Defining Interfaces
Basic interface syntax:
type InterfaceName interface {
MethodName1(params) returnType
MethodName2(params) returnType
}
// Examples
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
Interface naming conventions:
Single-method interfaces often end in "-er": Reader, Writer, Closer
Use descriptive names for multi-method interfaces: FileSystem, DataStore
Empty interface (accepts any type):
// Empty interface (old style)
interface{}
// Empty interface (Go 1.18+ style)
any // alias for interface{}
// Can hold any value
var x interface{}
x = 42
x = "hello"
x = []int{1, 2, 3}
var y any
y = 3.14
y = true
Implementing Interfaces
A type implements an interface by implementing all its methods:
type Writer interface {
Write(p []byte) (n int, err error)
}
// MyWriter implements Writer
type MyWriter struct{}
func (w MyWriter) Write(p []byte) (n int, err error) {
fmt.Println(string(p))
return len(p), nil
}
func main() {
var w Writer = MyWriter{} // ✓ MyWriter implements Writer
w.Write([]byte("Hello"))
}
No explicit declaration needed:
// Don't need to write:
// type Dog implements Speaker // ❌ Not valid Go
// Just implement the methods:
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
// Dog now automatically implements Speaker!
An interface value holds both a concrete type and a value of that type:
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
func main() {
var s Speaker
fmt.Printf("Value: %v, Type: %T\n", s, s)
// Value: , Type:
s = Dog{}
fmt.Printf("Value: %v, Type: %T\n", s, s)
// Value: {}, Type: main.Dog
// Interface value contains:
// 1. Type information (Dog)
// 2. Value ({})
}
nil interfaces:
var s Speaker // nil interface
if s == nil {
fmt.Println("s is nil") // This prints
}
// ❌ Calling method on nil interface panics
// s.Speak() // panic: nil pointer dereference
Interface with nil value (not nil interface):
type Speaker interface {
Speak() string
}
type Dog struct {
Name string
}
func (d *Dog) Speak() string {
if d == nil {
return "..."
}
return "Woof! I'm " + d.Name
}
func main() {
var d *Dog // nil pointer
var s Speaker = d // Interface holds nil value
// Interface itself is NOT nil
fmt.Println(s == nil) // false!
// But the value inside is nil
s.Speak() // "..." (method handles nil receiver)
}
Type Assertions
Type assertions extract the concrete value from an interface:
var i interface{} = "hello"
// Type assertion
s := i.(string)
fmt.Println(s) // "hello"
// Wrong type assertion panics
// n := i.(int) // panic: interface conversion
// Safe type assertion (two-value form)
s, ok := i.(string)
if ok {
fmt.Println("String:", s) // String: hello
}
n, ok := i.(int)
if ok {
fmt.Println("Int:", n)
} else {
fmt.Println("Not an int") // Not an int
}
Practical example:
type Animal interface {
Speak() string
}
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
func (d Dog) Fetch() string {
return "Fetching!"
}
func main() {
var animal Animal = Dog{Name: "Buddy"}
// Can call interface methods
fmt.Println(animal.Speak()) // "Woof!"
// ❌ Can't call non-interface methods
// fmt.Println(animal.Fetch()) // Error: Fetch not in Animal
// ✓ Use type assertion to access Dog-specific methods
if dog, ok := animal.(Dog); ok {
fmt.Println(dog.Fetch()) // "Fetching!"
}
}
Type Switches
Type switches allow checking and handling multiple types:
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case bool:
fmt.Printf("Boolean: %t\n", v)
case nil:
fmt.Println("Nil value")
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
describe(42) // Integer: 42
describe("hello") // String: hello
describe(true) // Boolean: true
describe(3.14) // Unknown type: float64
describe(nil) // Nil value
}
Type switch with multiple types per case:
func processValue(i interface{}) {
switch v := i.(type) {
case int, int32, int64:
fmt.Println("Some kind of int:", v)
case float32, float64:
fmt.Println("Some kind of float:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
}
Type switch with interface types:
func processReader(r interface{}) {
switch v := r.(type) {
case io.Reader:
fmt.Println("It's a Reader")
// Can use v as io.Reader
case io.Writer:
fmt.Println("It's a Writer")
case io.ReadWriter:
fmt.Println("It's both Reader and Writer")
default:
fmt.Println("Not a Reader or Writer")
}
}
Empty Interface and any
The empty interface can hold values of any type:
func printAnything(val interface{}) {
fmt.Println(val)
}
// Or with Go 1.18+
func printAnything(val any) {
fmt.Println(val)
}
func main() {
printAnything(42)
printAnything("hello")
printAnything([]int{1, 2, 3})
printAnything(struct{ X int }{X: 10})
}
Use cases for empty interface:
// 1. Generic data structures (before generics)
type Box struct {
Value interface{}
}
// 2. JSON unmarshaling to unknown structure
var data interface{}
json.Unmarshal(jsonBytes, &data)
// 3. fmt.Println accepts any
fmt.Println("Can print", 42, true, []int{1, 2})
// 4. Storing heterogeneous data
items := []interface{}{42, "hello", 3.14, true}
Limitations - need type assertions:
var x interface{} = 42
// ❌ Can't do arithmetic directly
// result := x + 10 // Error
// ✓ Need type assertion
if num, ok := x.(int); ok {
result := num + 10
fmt.Println(result) // 52
}
Common Standard Library Interfaces
io.Reader and io.Writer:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Example: Custom reader
type StringReader struct {
data string
pos int
}
func (r *StringReader) Read(p []byte) (n int, err error) {
if r.pos >= len(r.data) {
return 0, io.EOF
}
n = copy(p, r.data[r.pos:])
r.pos += n
return n, nil
}
func main() {
reader := &StringReader{data: "Hello, World!"}
buf := make([]byte, 5)
for {
n, err := reader.Read(buf)
if err == io.EOF {
break
}
fmt.Print(string(buf[:n]))
}
// Output: Hello, World!
}
error interface:
type error interface {
Error() string
}
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return ValidationError{
Field: "age",
Message: "must be non-negative",
}
}
return nil
}
func main() {
if err := validateAge(-5); err != nil {
fmt.Println(err) // age: must be non-negative
}
}
fmt.Stringer:
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
func main() {
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // Alice (30 years old)
// fmt.Println automatically calls String() method
}
sort.Interface:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func main() {
people := []Person{
{"Bob", 30},
{"Alice", 25},
{"Carol", 35},
}
sort.Sort(ByAge(people))
fmt.Println(people) // [{Alice 25} {Bob 30} {Carol 35}]
}
Interface Composition
Interfaces can be composed of other interfaces:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed interface
type ReadWriter interface {
Reader
Writer
}
// Another composed interface
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Standard library examples:
// io.ReadWriteCloser
type ReadWriteCloser interface {
io.Reader
io.Writer
io.Closer
}
// io.ReadSeeker
type ReadSeeker interface {
io.Reader
io.Seeker
}
Value receiver: Type and pointer to type both satisfy interface
type Speaker interface {
Speak() string
}
type Dog struct{}
// Value receiver
func (d Dog) Speak() string {
return "Woof!"
}
func main() {
dog := Dog{}
ptr := &Dog{}
var s1 Speaker = dog // ✓ Works
var s2 Speaker = ptr // ✓ Also works
s1.Speak()
s2.Speak()
}
Pointer receiver: Only pointer to type satisfies interface
type Counter interface {
Increment()
GetCount() int
}
type MyCounter struct {
count int
}
// Pointer receiver
func (c *MyCounter) Increment() {
c.count++
}
func (c *MyCounter) GetCount() int {
return c.count
}
func main() {
counter := MyCounter{}
ptr := &MyCounter{}
// ❌ Error: MyCounter doesn't implement Counter
// var c1 Counter = counter
// ✓ Works
var c2 Counter = ptr
c2.Increment()
fmt.Println(c2.GetCount()) // 1
}
Method Receiver
Type T satisfies interface?
*T satisfies interface?
Value (T)
✓ Yes
✓ Yes
Pointer (*T)
❌ No
✓ Yes
Interface Design Guidelines
The "Rule of Least Power":
// ✓ Good: Only require what you need
func CountBytes(r io.Reader) (int, error) {
// Only needs Read()
}
// ❌ Avoid: Requiring more than needed
func CountBytes(f *os.File) (int, error) {
// Unnecessarily requires os.File
}
Interface size guideline:
0 methods: Empty interface any - use sparingly
1 method: Perfect! Most Go interfaces have 1 method
2-3 methods: Still good, if closely related
4+ methods: Consider splitting
Where to define interfaces:
// ✓ Define in package that uses it
package consumer
type DataStore interface {
Save(data string) error
}
func Process(store DataStore) {
// Use interface
}
// ❌ Don't define in package that implements it
package provider
type DataStore interface {
Save(data string) error
}
type MySQLStore struct{}
func (m MySQLStore) Save(data string) error { /* ... */ }
Concurrency in Go
What Is Concurrency?
Concurrency is about dealing with multiple things at once (structure).
Parallelism is about doing multiple things at once (execution).
Concurrency is about the design, parallelism is about the execution.
Go's concurrency model is based on Communicating Sequential Processes (CSP).
"Don't communicate by sharing memory, share memory by communicating."
Goroutines
A goroutine is a lightweight thread managed by the Go runtime.
Goroutines are much cheaper than OS threads, you can have thousands or millions.
Creating a goroutine:
func sayHello() {
fmt.Println("Hello from goroutine!")
}
func main() {
// Run in new goroutine
go sayHello()
// Main goroutine continues
fmt.Println("Hello from main!")
// Wait a bit so goroutine can finish
time.Sleep(time.Second)
}
// Output (order may vary):
// Hello from main!
// Hello from goroutine!
Anonymous goroutines:
func main() {
go func() {
fmt.Println("Hello from anonymous goroutine!")
}()
time.Sleep(time.Second)
}
Main goroutine exits → all other goroutines are terminated immediately
Goroutines run concurrently, not necessarily in order
Creating a goroutine is very cheap (~2KB stack initially)
Channels
Channels are typed conduits for communication between goroutines.
Channels allow goroutines to synchronize without explicit locks.
Creating channels:
// Unbuffered channel
ch := make(chan int)
// Buffered channel (capacity 5)
ch := make(chan int, 5)
// Channel of strings
messages := make(chan string)
// Channel of structs
type Result struct {
Value int
Error error
}
results := make(chan Result)
Sending and receiving:
ch := make(chan int)
// Send to channel
go func() {
ch <- 42 // Send 42 to channel
}()
// Receive from channel
value := <-ch // Receive from channel
fmt.Println(value) // 42
Channels block until ready:
ch := make(chan int)
// This would block forever (deadlock)
// ch <- 42 // No receiver!
// This would also block forever (deadlock)
// value := <-ch // No sender!
// Need sender and receiver
go func() {
ch <- 42 // Sender in goroutine
}()
value := <-ch // Receiver in main
fmt.Println(value) // 42
Unbuffered vs Buffered Channels
Unbuffered channels (synchronous):
ch := make(chan int) // No buffer
go func() {
fmt.Println("Sending...")
ch <- 42
fmt.Println("Sent!")
}()
time.Sleep(time.Second) // Wait before receiving
value := <-ch
fmt.Println("Received:", value)
// Output:
// Sending...
// (1 second pause)
// Sent!
// Received: 42
// Sender blocks until receiver is ready
Buffered channels (asynchronous up to capacity):
ch := make(chan int, 2) // Buffer size 2
// Can send without receiver (up to capacity)
ch <- 1
ch <- 2
fmt.Println("Sent 2 values")
// Third send would block
// ch <- 3 // Blocks until someone receives
// Receive values
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
Aspect
Unbuffered
Buffered
Creation
make(chan T)
make(chan T, n)
Send blocks when
No receiver ready
Buffer full
Receive blocks when
No sender ready
Buffer empty
Synchronization
Guaranteed rendezvous
Decoupled (up to buffer)
Use case
Tight synchronization
Throughput optimization
Channel Directions
Channels can be restricted to send-only or receive-only:
// Producer sends to channel
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// Consumer receives from channel
func consumer(ch <-chan int) {
for value := range ch {
fmt.Println("Received:", value)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
Closing Channels
Closing a channel indicates no more values will be sent:
ch := make(chan int)
go func() {
ch <- 1
ch <- 2
ch <- 3
close(ch) // Close when done sending
}()
// Receive until channel is closed
for value := range ch {
fmt.Println(value)
}
// Prints: 1, 2, 3
Checking if channel is closed:
ch := make(chan int)
go func() {
ch <- 42
close(ch)
}()
// Two-value receive
value, ok := <-ch
if ok {
fmt.Println("Received:", value) // Received: 42
} else {
fmt.Println("Channel closed")
}
// After close, receiving returns zero value
value2, ok2 := <-ch
fmt.Println(value2, ok2) // 0 false
Important rules:
Only the sender should close a channel
Don't close a channel if receivers might still send (panic!)
Closing an already-closed channel panics
Sending to a closed channel panics
Receiving from a closed channel returns zero value and false
Common pattern: Signal completion
done := make(chan bool)
go func() {
// Do work
time.Sleep(time.Second)
// Signal completion
done <- true
}()
// Wait for completion
<-done
fmt.Println("Done!")
The Select Statement
Select lets you wait on multiple channel operations:
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
// Wait for whichever comes first
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
}
// Prints: Received: one (after 1 second)
Select with multiple ready channels (random choice):
ch1 := make(chan int)
ch2 := make(chan int)
// Both send immediately
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()
time.Sleep(100 * time.Millisecond)
// Random selection when both ready
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
}
// Randomly prints either "ch1: 1" or "ch2: 2"
Default case (non-blocking):
ch := make(chan int)
// Non-blocking receive
select {
case value := <-ch:
fmt.Println("Received:", value)
default:
fmt.Println("No value ready")
}
// Prints: No value ready (immediately)
Timeout pattern:
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "result"
}()
select {
case result := <-ch:
fmt.Println("Got:", result)
case <-time.After(1 * time.Second):
fmt.Println("Timeout!")
}
// Prints: Timeout! (after 1 second)
Select in a loop:
ch1 := make(chan int)
ch2 := make(chan int)
done := make(chan bool)
go func() {
for i := 0; i < 3; i++ {
ch1 <- i
time.Sleep(500 * time.Millisecond)
}
done <- true
}()
go func() {
for i := 0; i < 3; i++ {
ch2 <- i * 10
time.Sleep(700 * time.Millisecond)
}
}()
for {
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
case <-done:
fmt.Println("Done!")
return
}
}
WaitGroups
sync.WaitGroup waits for a collection of goroutines to finish:
import "sync"
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement counter when done
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment counter
go worker(i, &wg)
}
wg.Wait() // Block until counter is 0
fmt.Println("All workers done")
}
Important WaitGroup rules:
Call Add() before starting goroutine
Call Done() when goroutine finishes (use defer)
Call Wait() to block until all done
Don't copy WaitGroup - pass by pointer
Common mistake - Add in goroutine:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
// ❌ Wrong: Race condition
go func(id int) {
wg.Add(1) // Don't add in goroutine!
defer wg.Done()
fmt.Println(id)
}(i)
}
wg.Wait() // Might not wait for all
// ✓ Correct: Add before starting goroutine
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // Add before go
go func(id int) {
defer wg.Done()
fmt.Println(id)
}(i)
}
wg.Wait()
Mutexes
Mutex (mutual exclusion) protects shared data from concurrent access:
import "sync"
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
// 1000 goroutines incrementing
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Final value:", counter.Value()) // 1000
}
Always use defer with Unlock:
func (c *Counter) SafeMethod() {
c.mu.Lock()
defer c.mu.Unlock() // ✓ Ensures unlock even if panic
// Do work
// If panic occurs, defer still runs
}
// ❌ Dangerous - might not unlock if error/panic
func (c *Counter) UnsafeMethod() {
c.mu.Lock()
// Do work that might panic
c.mu.Unlock() // Might never reach here!
}
RWMutex (Read-Write Mutex):
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // Multiple readers allowed
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // Exclusive access for writing
defer c.mu.Unlock()
c.data[key] = value
}
// Multiple Get() can run concurrently
// Set() blocks all Get() and other Set()
Common Concurrency Patterns
Pattern 1: Worker Pool
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 9 jobs
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// Collect results
for a := 1; a <= 9; a++ {
<-results
}
}
Pattern 2: Pipeline
// Stage 1: Generate numbers
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Stage 2: Square numbers
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
// Stage 3: Print results
func main() {
// Pipeline: generate -> square -> print
numbers := generate(2, 3, 4, 5)
squares := square(numbers)
for result := range squares {
fmt.Println(result)
}
// Output: 4, 9, 16, 25
}
Pattern 3: Fan-Out, Fan-In
func fanOut(in <-chan int, n int) []<-chan int {
outs := make([]<-chan int, n)
for i := 0; i < n; i++ {
outs[i] = worker(in)
}
return outs
}
func worker(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for v := range in {
out <- v * 2
time.Sleep(100 * time.Millisecond)
}
close(out)
}()
return out
}
func fanIn(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Pattern 4: Quit Channel
func worker(quit <-chan bool) {
for {
select {
case <-quit:
fmt.Println("Worker stopping")
return
default:
// Do work
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
quit := make(chan bool)
go worker(quit)
time.Sleep(2 * time.Second)
quit <- true // Signal worker to stop
time.Sleep(100 * time.Millisecond)
}
Pattern 5: Rate Limiting
func main() {
requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
requests <- i
}
close(requests)
// Rate limiter: 1 request per 200ms
limiter := time.Tick(200 * time.Millisecond)
for req := range requests {
<-limiter // Wait for limiter
fmt.Println("Request", req, time.Now())
}
}
sync.Once
sync.Once ensures a function is only called once:
import "sync"
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
fmt.Println("Creating database connection")
instance = &Database{}
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
db := GetDatabase()
fmt.Printf("DB: %p\n", db)
}()
}
wg.Wait()
// "Creating database connection" prints only once
// All goroutines get same instance
}
Pass context as first parameter: func Do(ctx context.Context, ...)
Don't store contexts in structs
Always defer cancel() to avoid leaks
Use context.Background() at top level
Common Mistakes and How to Avoid Them
Mistake 1: Forgetting WaitGroup Done()
var wg sync.WaitGroup
wg.Add(1)
go func() {
// ❌ Forgot wg.Done()
fmt.Println("Working...")
}()
wg.Wait() // Blocks forever!
// ✓ Always use defer
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Working...")
}()
Mistake 2: Not closing channels
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
// ❌ Forgot to close
}()
// Blocks forever after receiving 5 values
for val := range ch {
fmt.Println(val)
}
// ✓ Close when done sending
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
Mistake 3: Closing channel from receiver
// ❌ Wrong: Receiver closes
go func() {
for val := range ch {
fmt.Println(val)
}
close(ch) // Sender might still be sending!
}()
// ✓ Correct: Sender closes
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
Mistake 4: Goroutine leaks
// ❌ Goroutine never exits
func leak() {
ch := make(chan int)
go func() {
val := <-ch // Blocks forever if nothing sent
fmt.Println(val)
}()
// Goroutine continues running forever
}
// ✓ Use context for cancellation
func noLeak() {
ch := make(chan int)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
return
}
}()
}
Mistake 5: Race conditions without synchronization
// ❌ Race condition
counter := 0
for i := 0; i < 1000; i++ {
go func() {
counter++ // Not thread-safe!
}()
}
// counter might not be 1000
// ✓ Use mutex
var mu sync.Mutex
counter := 0
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
Mistake 6: Using time.Sleep to wait for goroutines
// ❌ Unreliable
go doWork()
time.Sleep(time.Second) // Hope it finishes in 1 second?
// ✓ Use WaitGroup or channels
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
Detecting Race Conditions
Go has a built-in race detector:
# Run with race detector
go run -race main.go
# Test with race detector
go test -race
# Build with race detector
go build -race
Example race condition:
package main
import "fmt"
func main() {
counter := 0
go func() {
counter++ // Write
}()
fmt.Println(counter) // Read
}
// Running with -race will report:
// WARNING: DATA RACE
Best Practices
1. Share memory by communicating (use channels)
// ✓ Prefer channels
ch := make(chan Data)
go producer(ch)
go consumer(ch)
// Instead of sharing memory with locks
// var data Data
// var mu sync.Mutex
2. Use WaitGroups for goroutine synchronization
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
doWork(id)
}(i)
}
wg.Wait()
3. Always defer mutex unlocks
mu.Lock()
defer mu.Unlock()
// Do work
4. Use buffered channels for better throughput
// Unbuffered: sender blocks until receiver ready
ch := make(chan int)
// Buffered: sender doesn't block until buffer full
ch := make(chan int, 100)
5. Limit goroutine count for resource-intensive tasks
// Worker pool pattern
jobs := make(chan Job, 100)
results := make(chan Result, 100)
// Limit to 10 workers
for w := 0; w < 10; w++ {
go worker(jobs, results)
}
6. Use context for cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go worker(ctx)
7. Test with race detector
go test -race ./...
Defer in Go
What Is Defer?
Defer schedules a function call to be executed when the surrounding function returns.
Deferred calls are executed in LIFO (Last In, First Out) order.
Defer is commonly used for cleanup tasks: closing files, unlocking mutexes, etc.
When the function returns, deferred calls execute in reverse order (LIFO)
Deferred calls run even if the function panics
Visual example:
func example() {
defer fmt.Println("1") // Added to stack first
defer fmt.Println("2") // Added second
defer fmt.Println("3") // Added third
fmt.Println("Function body")
}
// Stack of deferred calls:
// [3] <- Top (executes first)
// [2]
// [1] <- Bottom (executes last)
// Output:
// Function body
// 3
// 2
// 1
Defer executes when function returns:
func example() int {
defer fmt.Println("Defer 1")
if true {
defer fmt.Println("Defer 2")
return 42
// Defers execute here, before return
}
defer fmt.Println("Defer 3") // Never added to stack
return 0
}
// Output:
// Defer 2
// Defer 1
// (Returns 42)
Common Use Cases
Use case 1: Closing files
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // ✓ Always closes, even if error occurs
// Read file
data := make([]byte, 100)
_, err = file.Read(data)
if err != nil {
return err // file.Close() called before return
}
// Process data
fmt.Println(string(data))
return nil // file.Close() called before return
}
Use case 2: Unlocking mutexes
var mu sync.Mutex
func criticalSection() {
mu.Lock()
defer mu.Unlock() // ✓ Always unlocks, even if panic
// Critical section code
// If panic occurs, defer still runs
doWork()
}
Use case 3: Timing function execution
func measure(name string) func() {
start := time.Now()
return func() {
fmt.Printf("%s took %v\n", name, time.Since(start))
}
}
func slowFunction() {
defer measure("slowFunction")() // Note the () to call returned function
time.Sleep(2 * time.Second)
// Do work
}
func main() {
slowFunction()
// Output: slowFunction took 2.001s
}
Use case 4: Database transactions
func updateUser(db *sql.DB, userID int, name string) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // Rollback if we don't commit
_, err = tx.Exec("UPDATE users SET name = ? WHERE id = ?", name, userID)
if err != nil {
return err // Rollback happens here
}
return tx.Commit() // Commit overrides rollback
}
Use case 5: Cleanup temporary resources
func processData() error {
tempFile, err := os.CreateTemp("", "data-*.tmp")
if err != nil {
return err
}
defer os.Remove(tempFile.Name()) // Clean up temp file
defer tempFile.Close() // Close file handle
// Use temp file
tempFile.Write([]byte("data"))
return nil
// File closed and deleted automatically
}
Defer with Function Arguments
Important: Function arguments are evaluated immediately when defer is called!
func example() {
x := 1
defer fmt.Println(x) // x is evaluated NOW (x = 1)
x = 2
fmt.Println("Function body:", x)
}
// Output:
// Function body: 2
// 1 (not 2!)
Using closures to capture current values:
func example() {
x := 1
// Closure captures x by reference
defer func() {
fmt.Println(x) // x evaluated when defer runs
}()
x = 2
fmt.Println("Function body:", x)
}
// Output:
// Function body: 2
// 2 (uses current value!)
Practical example:
func printValues() {
for i := 0; i < 3; i++ {
// ❌ Wrong: i is evaluated immediately
defer fmt.Println(i)
}
}
func main() {
printValues()
}
// Output:
// 2
// 1
// 0
// (Prints in reverse, with final values)
func printValuesCorrect() {
for i := 0; i < 3; i++ {
// ✓ Correct: capture i in closure
i := i // Create new variable for each iteration
defer func() {
fmt.Println(i)
}()
}
}
func main() {
printValuesCorrect()
}
// Output:
// 2
// 1
// 0
Multiple Defer Statements
Multiple defers execute in reverse order (LIFO - stack):
func multipleDefers() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")
fmt.Println("Function body")
}
// Output:
// Function body
// Third defer
// Second defer
// First defer
Practical example: Nested resource cleanup
func process() error {
// Open database connection
db, err := openDB()
if err != nil {
return err
}
defer db.Close() // Closes last
// Begin transaction
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // Closes second (before db)
// Open file
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer file.Close() // Closes first
// Process
// ...
return tx.Commit()
}
// Cleanup order:
// 1. file.Close()
// 2. tx.Rollback()
// 3. db.Close()
Defer in Loops
Common pitfall: Defers in loops don't execute until function returns!
func processFiles(filenames []string) {
for _, filename := range filenames {
file, err := os.Open(filename)
if err != nil {
continue
}
defer file.Close() // ❌ Problem: All defers wait until function ends!
// Process file
}
// All files stay open until here!
// Could run out of file descriptors
}
Solution 1: Use a separate function
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // ✓ Closes when processFile returns
// Process file
return nil
}
func processFiles(filenames []string) {
for _, filename := range filenames {
processFile(filename) // ✓ File closed after each iteration
}
}
Solution 2: Use anonymous function
func processFiles(filenames []string) {
for _, filename := range filenames {
func() {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close() // ✓ Closes when anonymous function returns
// Process file
}()
}
}
Solution 3: Manual cleanup (if appropriate)
func processFiles(filenames []string) {
for _, filename := range filenames {
file, err := os.Open(filename)
if err != nil {
continue
}
// Process file
file.Close() // ✓ Close immediately after processing
}
}
func example() (result int) {
// Execution order:
// 1. Set result = 41
result = 41
// 2. Run deferred function (modifies result to 42)
defer func() {
result++
}()
// 3. Return result (which is now 42)
return result
}
Practical use: Error handling
func doSomething() (err error) {
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer func() {
closeErr := file.Close()
if err == nil { // Only set error if no previous error
err = closeErr
}
}()
// Process file
// If error occurs here, it takes precedence
// If no error, close error is returned
return nil
}
Without named returns (can't modify):
func example() int {
result := 41
defer func() {
result++ // Modifies local variable, not return value
}()
return result // Returns 41
}
func main() {
fmt.Println(example()) // 41 (not modified)
}
6. Don't panic about performance unless profiling shows issue
// ✓ Clarity and safety first
defer cleanup()
// Profile before optimizing away defer
When NOT to Use Defer
1. Long-running functions with early resource release
func process() {
file, _ := os.Open("small.txt")
// Process small file
data, _ := io.ReadAll(file)
file.Close() // ✓ Close now, not at end
// Long computation that doesn't need file
time.Sleep(1 * time.Hour)
}
2. Performance-critical tight loops
// Only if profiling shows defer is bottleneck
for i := 0; i < 1000000; i++ {
mu.Lock()
data[i] = i
mu.Unlock() // Manual unlock if defer is measured bottleneck
}
3. When you need immediate cleanup
func streamProcess() {
for {
conn := accept()
data := conn.Read()
process(data)
conn.Close() // ✓ Close immediately, don't accumulate
}
}
Error Handling in Go
Go's Error Philosophy
Go uses explicit error handling - no exceptions or try-catch.
Errors are values that functions return, not thrown.
The convention: last return value is an error.
"Errors are values. Don't just check errors, handle them gracefully."
Basic example:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
The error Interface
The error type is a built-in interface:
type error interface {
Error() string
}
// Any type with an Error() string method implements error
// Standard pattern
result, err := someFunction()
if err != nil {
// Handle error
return err
}
// Use result
Creating Errors
Method 1: errors.New (simple string)
import "errors"
func validate(age int) error {
if age < 0 {
return errors.New("age cannot be negative")
}
return nil
}
Method 2: fmt.Errorf (formatted string)
import "fmt"
func validate(name string, age int) error {
if age < 0 {
return fmt.Errorf("invalid age %d for user %s", age, name)
}
return nil
}
func main() {
err := validate("Alice", -5)
if err != nil {
fmt.Println(err) // invalid age -5 for user Alice
}
}
Method 3: Predefined errors (package-level)
var (
ErrNotFound = errors.New("not found")
ErrInvalidData = errors.New("invalid data")
ErrTimeout = errors.New("timeout")
)
func fetchUser(id int) (*User, error) {
if id < 0 {
return nil, ErrInvalidData
}
user := database.Find(id)
if user == nil {
return nil, ErrNotFound
}
return user, nil
}
// Caller can check specific errors
user, err := fetchUser(123)
if err == ErrNotFound {
fmt.Println("User not found")
}
Method 4: Custom error types
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
}
func validateEmail(email string) error {
if !strings.Contains(email, "@") {
return ValidationError{
Field: "email",
Message: "must contain @ symbol",
}
}
return nil
}
Error Wrapping (Go 1.13+)
Error wrapping adds context while preserving the original error:
// Wrap with %w verb
func readConfig(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
defer file.Close()
// Process file
return nil
}
func main() {
err := readConfig("config.yaml")
if err != nil {
fmt.Println(err)
// Output: failed to read config: open config.yaml: no such file or directory
}
}
Difference between %v and %w:
originalErr := errors.New("original error")
// %v - formats error but doesn't wrap
err1 := fmt.Errorf("context: %v", originalErr)
// Cannot unwrap to get originalErr
// %w - wraps error
err2 := fmt.Errorf("context: %w", originalErr)
// Can unwrap to get originalErr
Why wrap errors?
Add context to errors as they bubble up
Preserve original error for inspection
Create error chains with full context
func loadUser(id int) (*User, error) {
data, err := database.Query(id)
if err != nil {
return nil, fmt.Errorf("loading user %d: %w", id, err)
}
user, err := parseUser(data)
if err != nil {
return nil, fmt.Errorf("parsing user data: %w", err)
}
return user, nil
}
// Error chain:
// parsing user data: invalid JSON: unexpected token at position 10
Checking Wrapped Errors: errors.Is
errors.Is checks if an error matches a target, even if wrapped:
var ErrNotFound = errors.New("not found")
func getUser(id int) error {
// ... some code
return fmt.Errorf("user lookup failed: %w", ErrNotFound)
}
func main() {
err := getUser(123)
// ❌ Wrong: doesn't work with wrapped errors
if err == ErrNotFound {
fmt.Println("Not found") // Won't print!
}
// ✓ Correct: works with wrapped errors
if errors.Is(err, ErrNotFound) {
fmt.Println("Not found") // Prints!
}
}
// ❌ Type assertion - doesn't work with wrapped errors
if validationErr, ok := err.(ValidationError); ok {
// Won't work if error is wrapped
}
// ✓ errors.As - works with wrapped errors
var validationErr ValidationError
if errors.As(err, &validationErr) {
// Works even if error is wrapped
}
Common pattern: Custom error handling
type NetworkError struct {
Code int
Message string
}
func (e NetworkError) Error() string {
return fmt.Sprintf("network error %d: %s", e.Code, e.Message)
}
func handleError(err error) {
var netErr NetworkError
if errors.As(err, &netErr) {
if netErr.Code >= 500 {
log.Printf("Server error: %v", netErr)
} else if netErr.Code >= 400 {
log.Printf("Client error: %v", netErr)
}
}
}
Custom Error Types
Simple custom error:
type NotFoundError struct {
Resource string
ID int
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID)
}
func getUser(id int) (*User, error) {
user := database.Find(id)
if user == nil {
return nil, NotFoundError{Resource: "User", ID: id}
}
return user, nil
}
Error with multiple fields:
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed for field '%s' with value '%v': %s",
e.Field, e.Value, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return ValidationError{
Field: "age",
Value: age,
Message: "must be between 0 and 150",
}
}
return nil
}
Error with context and cause:
type ServiceError struct {
Op string // Operation that failed
Service string // Service name
Err error // Underlying error
}
func (e *ServiceError) Error() string {
return fmt.Sprintf("%s: %s failed: %v", e.Service, e.Op, e.Err)
}
func (e *ServiceError) Unwrap() error {
return e.Err // Allows errors.Is and errors.As to work
}
func fetchData() error {
err := networkCall()
if err != nil {
return &ServiceError{
Op: "fetch",
Service: "UserService",
Err: err,
}
}
return nil
}
Panic is for unrecoverable errors (not normal error handling):
func mustConnect() *Database {
db, err := connect()
if err != nil {
panic(fmt.Sprintf("cannot connect to database: %v", err))
}
return db
}
// Panic should only be used for:
// - Programmer errors (bugs)
// - Initialization failures that make program unusable
// - Impossible situations that indicate bugs
Recover catches panics (use sparingly):
func safeHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Panic: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
// Handler code that might panic
riskyOperation()
}
Converting panic to error:
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic occurred: %v", r)
}
}()
result = a / b // May panic if b == 0
return result, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Error: panic occurred: runtime error...
}
}
When to use panic vs error:
Situation
Use
Reason
Expected errors (file not found, network timeout)
error
Caller should handle
Invalid input from user
error
Expected, recoverable
Initialization failure (can't continue)
panic
Program can't function
Programmer error (nil pointer, out of bounds)
panic
Indicates a bug
Configuration error at startup
panic or error
Depends on severity
Multiple Return Values
Go functions commonly return result and error:
// Standard pattern: (result, error)
func fetchUser(id int) (*User, error) {
if id < 0 {
return nil, errors.New("invalid ID")
}
user := database.Find(id)
if user == nil {
return nil, ErrNotFound
}
return user, nil
}
Multiple results with error:
func parseCoordinates(s string) (lat, lng float64, err error) {
parts := strings.Split(s, ",")
if len(parts) != 2 {
return 0, 0, errors.New("invalid format")
}
lat, err = strconv.ParseFloat(parts[0], 64)
if err != nil {
return 0, 0, fmt.Errorf("invalid latitude: %w", err)
}
lng, err = strconv.ParseFloat(parts[1], 64)
if err != nil {
return 0, 0, fmt.Errorf("invalid longitude: %w", err)
}
return lat, lng, nil
}
func main() {
lat, lng, err := parseCoordinates("40.7128,-74.0060")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Lat: %f, Lng: %f\n", lat, lng)
}
Boolean + error pattern:
// Found/not found with error
func findUser(id int) (*User, bool, error) {
if id < 0 {
return nil, false, errors.New("invalid ID")
}
user := database.Find(id)
if user == nil {
return nil, false, nil // Not found, no error
}
return user, true, nil // Found
}
func main() {
user, found, err := findUser(123)
if err != nil {
log.Fatal(err)
}
if !found {
fmt.Println("User not found")
return
}
fmt.Println("User:", user.Name)
}
Sentinel Errors
Sentinel errors are predefined package-level error values:
package user
import "errors"
// Sentinel errors
var (
ErrNotFound = errors.New("user not found")
ErrAlreadyExists = errors.New("user already exists")
ErrInvalidInput = errors.New("invalid input")
)
func GetUser(id int) (*User, error) {
user := database.Find(id)
if user == nil {
return nil, ErrNotFound
}
return user, nil
}
// Caller can check specific errors
import "user"
u, err := user.GetUser(123)
if err == user.ErrNotFound {
// Handle not found case
}
Standard library sentinel errors:
import (
"io"
"os"
)
// io package
if err == io.EOF {
// End of file
}
if err == io.ErrUnexpectedEOF {
// Unexpected end
}
// os package
if errors.Is(err, os.ErrNotExist) {
// File doesn't exist
}
if errors.Is(err, os.ErrPermission) {
// Permission denied
}
if errors.Is(err, os.ErrExist) {
// File already exists
}
func TestValidationError(t *testing.T) {
err := validateEmail("invalid")
var validationErr ValidationError
if !errors.As(err, &validationErr) {
t.Fatal("expected ValidationError")
}
if validationErr.Field != "email" {
t.Errorf("got field %s, want email", validationErr.Field)
}
}
First-Class Functions in Go
What Are First-Class Functions?
In Go, functions are first-class citizens - they can be:
Assigned to variables
Passed as arguments to other functions
Returned from functions
Stored in data structures
This enables powerful programming patterns like callbacks, closures, and higher-order functions.
Basic example:
func add(a, b int) int {
return a + b
}
func main() {
// Assign function to variable
operation := add
// Call function through variable
result := operation(3, 4)
fmt.Println(result) // 7
}
Function Types
Every function has a type based on its signature:
// Function that takes two ints and returns an int
func add(a, b int) int {
return a + b
}
// Type of add is: func(int, int) int
// Declare variable with function type
var operation func(int, int) int
operation = add
result := operation(5, 3) // 8
Declaring function types:
// Define a custom function type
type BinaryOperation func(int, int) int
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }
func main() {
var op BinaryOperation
op = add
fmt.Println(op(3, 4)) // 7
op = multiply
fmt.Println(op(3, 4)) // 12
}
Function type with multiple return values:
// Function that returns result and error
type Calculator func(int, int) (int, error)
func safeDivide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
var calc Calculator = safeDivide
result, err := calc(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5
}
Functions as Parameters (Higher-Order Functions)
Functions that take other functions as parameters are called higher-order functions:
// apply takes a function and applies it to two values
func apply(a, b int, operation func(int, int) int) int {
return operation(a, b)
}
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }
func main() {
result1 := apply(3, 4, add) // 7
result2 := apply(3, 4, multiply) // 12
fmt.Println(result1, result2)
}
Example: Map function (transform slice)
// Map applies a function to each element in a slice
func Map(slice []int, fn func(int) int) []int {
result := make([]int, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func double(x int) int { return x * 2 }
func square(x int) int { return x * x }
func main() {
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, double)
fmt.Println(doubled) // [2 4 6 8 10]
squared := Map(numbers, square)
fmt.Println(squared) // [1 4 9 16 25]
}
Example: Filter function
// Filter returns elements that match the predicate
func Filter(slice []int, predicate func(int) bool) []int {
result := []int{}
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func isEven(x int) bool { return x%2 == 0 }
func isPositive(x int) bool { return x > 0 }
func main() {
numbers := []int{-2, -1, 0, 1, 2, 3, 4, 5}
evens := Filter(numbers, isEven)
fmt.Println(evens) // [-2 0 2 4]
positives := Filter(numbers, isPositive)
fmt.Println(positives) // [1 2 3 4 5]
}
Example: Reduce function
// Reduce combines all elements using a function
func Reduce(slice []int, initial int, fn func(int, int) int) int {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
func add(a, b int) int { return a + b }
func multiply(a, b int) int { return a * b }
func main() {
numbers := []int{1, 2, 3, 4, 5}
sum := Reduce(numbers, 0, add)
fmt.Println(sum) // 15
product := Reduce(numbers, 1, multiply)
fmt.Println(product) // 120
}
Functions as Return Values
Functions can return other functions:
// makeAdder returns a function that adds n to its input
func makeAdder(n int) func(int) int {
return func(x int) int {
return x + n
}
}
func main() {
add5 := makeAdder(5)
add10 := makeAdder(10)
fmt.Println(add5(3)) // 8
fmt.Println(add10(3)) // 13
}
Example: Function factory
// makeMultiplier creates a function that multiplies by n
func makeMultiplier(n int) func(int) int {
return func(x int) int {
return x * n
}
}
func main() {
double := makeMultiplier(2)
triple := makeMultiplier(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15
}
Example: Configurable operations
type Operation string
const (
Add Operation = "add"
Subtract Operation = "subtract"
Multiply Operation = "multiply"
)
func getOperation(op Operation) func(int, int) int {
switch op {
case Add:
return func(a, b int) int { return a + b }
case Subtract:
return func(a, b int) int { return a - b }
case Multiply:
return func(a, b int) int { return a * b }
default:
return func(a, b int) int { return 0 }
}
}
func main() {
op := getOperation(Add)
result := op(10, 5)
fmt.Println(result) // 15
}
Anonymous Functions and Function Literals
Anonymous functions (function literals) are functions without a name:
func main() {
// Define and call immediately
func() {
fmt.Println("Hello from anonymous function!")
}()
// Assign to variable
greet := func(name string) {
fmt.Printf("Hello, %s!\n", name)
}
greet("Alice")
// Pass as argument
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(x int) int {
return x * 2
})
fmt.Println(doubled) // [2 4 6 8 10]
}
Anonymous function with parameters and return value:
func main() {
// Calculate immediately
result := func(a, b int) int {
return a*a + b*b
}(3, 4)
fmt.Println(result) // 25 (3² + 4²)
}
Common use: Sorting with custom comparator
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
// Sort by age using anonymous function
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Println(people)
// [{Bob 25} {Alice 30} {Charlie 35}]
}
Closures: Functions That Capture Variables
A closure is a function that references variables from outside its body:
func main() {
x := 10
// This function "closes over" the variable x
increment := func() int {
x++ // References x from outer scope
return x
}
fmt.Println(increment()) // 11
fmt.Println(increment()) // 12
fmt.Println(increment()) // 13
fmt.Println(x) // 13 (x was modified)
}
Important: Closures capture variables by reference
// ❌ Common mistake in loops
func main() {
functions := []func(){}
for i := 0; i < 3; i++ {
functions = append(functions, func() {
fmt.Println(i) // Captures i by reference!
})
}
for _, f := range functions {
f() // Prints: 3, 3, 3 (not 0, 1, 2!)
}
}
// ✓ Correct: Create new variable for each iteration
func main() {
functions := []func(){}
for i := 0; i < 3; i++ {
i := i // Create new variable
functions = append(functions, func() {
fmt.Println(i)
})
}
for _, f := range functions {
f() // Prints: 0, 1, 2
}
}
Callback Functions
Callbacks are functions passed to other functions to be called at a specific time:
// Process data and call callback when done
func processData(data string, callback func(string)) {
// Simulate processing
result := strings.ToUpper(data)
// Call the callback with result
callback(result)
}
func main() {
processData("hello", func(result string) {
fmt.Println("Result:", result)
})
// Result: HELLO
}
Example: HTTP handler callbacks
func main() {
// Handler function is a callback
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
}
Method values bind a method to a specific receiver:
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
func (c *Counter) Value() int {
return c.count
}
func main() {
counter := &Counter{}
// Method value: bound to counter instance
inc := counter.Increment
inc() // Same as counter.Increment()
inc()
fmt.Println(counter.Value()) // 2
}
Method expressions create a function from a method:
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
// Method expression: function that takes receiver as first param
areaFunc := Rectangle.Area
rect := Rectangle{Width: 10, Height: 5}
area := areaFunc(rect) // Must pass receiver
fmt.Println(area) // 50
}
Practical example: Using method values with goroutines
type Worker struct {
id int
}
func (w *Worker) Process() {
fmt.Printf("Worker %d processing\n", w.id)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Worker %d done\n", w.id)
}
func main() {
workers := []*Worker{
{id: 1},
{id: 2},
{id: 3},
}
for _, w := range workers {
// Method value captures specific worker
go w.Process()
}
time.Sleep(200 * time.Millisecond)
}
Functions in Data Structures
Functions can be stored in slices, maps, and structs:
// Map of operations
var operations = map[string]func(int, int) int{
"add": func(a, b int) int { return a + b },
"subtract": func(a, b int) int { return a - b },
"multiply": func(a, b int) int { return a * b },
}
func main() {
result := operations["add"](10, 5)
fmt.Println(result) // 15
result = operations["multiply"](10, 5)
fmt.Println(result) // 50
}
type Server struct {
host string
port int
timeout time.Duration
}
type Option func(*Server)
func WithHost(host string) Option {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func NewServer(opts ...Option) *Server {
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
server := NewServer(
WithHost("0.0.0.0"),
WithPort(9000),
)
fmt.Printf("Server: %s:%d\n", server.host, server.port)
}
Pattern 2: Strategy pattern
type SortStrategy func([]int) []int
func bubbleSort(data []int) []int {
// Implementation
return data
}
func quickSort(data []int) []int {
// Implementation
return data
}
type Sorter struct {
strategy SortStrategy
}
func (s *Sorter) Sort(data []int) []int {
return s.strategy(data)
}
func main() {
data := []int{5, 2, 8, 1, 9}
sorter := &Sorter{strategy: quickSort}
sorted := sorter.Sort(data)
fmt.Println(sorted)
}
Pattern 3: Lazy evaluation
// Function that returns a function that computes on demand
func lazy(compute func() int) func() int {
var value int
var computed bool
return func() int {
if !computed {
value = compute()
computed = true
}
return value
}
}
func main() {
expensive := lazy(func() int {
fmt.Println("Computing...")
time.Sleep(1 * time.Second)
return 42
})
fmt.Println("Created lazy value")
fmt.Println(expensive()) // Computing... 42
fmt.Println(expensive()) // 42 (no recomputation)
}
Pattern 4: Memoization
func memoize(fn func(int) int) func(int) int {
cache := make(map[int]int)
return func(n int) int {
if result, found := cache[n]; found {
return result
}
result := fn(n)
cache[n] = result
return result
}
}
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
memoFib := memoize(fibonacci)
fmt.Println(memoFib(40)) // Fast with caching
fmt.Println(memoFib(40)) // Instant (cached)
}
Common Mistakes and How to Avoid Them
Mistake 1: Closure capturing loop variable
// ❌ Wrong: All closures reference same variable
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // Prints 5, 5, 5, 5, 5
}()
}
// ✓ Correct: Create new variable for each iteration
for i := 0; i < 5; i++ {
i := i // Shadow variable
go func() {
fmt.Println(i) // Prints 0, 1, 2, 3, 4
}()
}
// ✓ Also correct: Pass as parameter
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
Mistake 2: Not checking for nil function
type Config struct {
OnComplete func()
}
// ❌ Wrong: may panic if OnComplete is nil
func process(cfg Config) {
// ... work ...
cfg.OnComplete() // Panic if nil!
}
// ✓ Correct: check before calling
func process(cfg Config) {
// ... work ...
if cfg.OnComplete != nil {
cfg.OnComplete()
}
}
Mistake 3: Forgetting that functions are compared by address
func add(a, b int) int { return a + b }
func main() {
f1 := add
f2 := add
// ❌ Don't compare functions directly
if f1 == f2 { // true (same function)
fmt.Println("Same")
}
// But anonymous functions are different
g1 := func() {}
g2 := func() {}
// Comparing these will panic!
// fmt.Println(g1 == g2) // panic: runtime error
}
Mistake 4: Modifying slice in closure incorrectly
// ❌ Wrong: Race condition
items := []int{1, 2, 3}
for _, item := range items {
go func() {
items = append(items, item*2) // Race!
}()
}
// ✓ Correct: Use proper synchronization
var mu sync.Mutex
items := []int{1, 2, 3}
for _, item := range items {
item := item
go func() {
mu.Lock()
items = append(items, item*2)
mu.Unlock()
}()
}
Mistake 5: Not understanding closure lifetime
// ❌ Wrong: function may outlive the data
func makeHandler() http.HandlerFunc {
data := fetchData() // Expensive operation
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%v", data)
}
// data stays in memory as long as handler exists!
}
// ✓ Better: Fetch data on each request if appropriate
func makeHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := fetchData()
fmt.Fprintf(w, "%v", data)
}
}
Best Practices
1. Use closures to encapsulate state
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
3. Use type aliases for complex function signatures
// ✓ Clear and reusable
type HandlerFunc func(http.ResponseWriter, *http.Request)
// ❌ Harder to read and maintain
func register(path string, fn func(http.ResponseWriter, *http.Request)) {}
4. Check for nil before calling function variables
if callback != nil {
callback()
}
5. Document function parameters clearly
// Map applies fn to each element in the slice and returns a new slice.
// fn should be a pure function with no side effects.
func Map(slice []int, fn func(int) int) []int { }
6. Avoid deep nesting with closures
// ❌ Hard to read
func complex() func() func() func() int {
return func() func() func() int {
return func() func() int {
return func() int { return 42 }
}
}
}
// ✓ Keep it simple
func simple() func() int {
return func() int { return 42 }
}
Reflection in Go
What Is Reflection?
Reflection is the ability of a program to inspect and manipulate its own structure and values at runtime.
Go's reflection is provided by the reflect package.
Reflection allows you to:
Examine types and values at runtime
Inspect struct fields and tags
Call methods and functions dynamically
Create and modify values dynamically
When to use reflection:
Building libraries that work with any type (JSON encoding, ORMs)
Implementing generic functionality before Go 1.18 generics
Working with unknown types at compile time
Important: Reflection is powerful but slow and loses type safety. Use it sparingly!
Basic example:
import (
"fmt"
"reflect"
)
func main() {
var x int = 42
// Get type information
t := reflect.TypeOf(x)
fmt.Println("Type:", t) // Type: int
// Get value information
v := reflect.ValueOf(x)
fmt.Println("Value:", v) // Value: 42
fmt.Println("Kind:", v.Kind()) // Kind: int
}
The Core Types: reflect.Type and reflect.Value
The reflect package has two main types:
reflect.Type - represents a Go type
var x int = 42
t := reflect.TypeOf(x)
fmt.Println(t.Name()) // int
fmt.Println(t.Kind()) // int
fmt.Println(t.String()) // int
reflect.Value - represents a value of any type
var x int = 42
v := reflect.ValueOf(x)
fmt.Println(v.Type()) // int
fmt.Println(v.Kind()) // int
fmt.Println(v.Int()) // 42
fmt.Println(v.Interface()) // 42 (as interface{})
Relationship between Type and Value:
var x float64 = 3.14
v := reflect.ValueOf(x)
t := v.Type() // Get Type from Value
// Or directly
t2 := reflect.TypeOf(x)
fmt.Println(t == t2) // true
Key difference:
reflect.Type - metadata about the type (static information)
reflect.Value - actual value and methods to manipulate it
Type Information: reflect.Type
Getting type information:
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Alice", 30}
t := reflect.TypeOf(p)
fmt.Println("Name:", t.Name()) // Person
fmt.Println("Kind:", t.Kind()) // struct
fmt.Println("PkgPath:", t.PkgPath()) // main
fmt.Println("String:", t.String()) // main.Person
}
Type vs Kind:
type MyInt int
func main() {
var x MyInt = 42
t := reflect.TypeOf(x)
fmt.Println("Name:", t.Name()) // MyInt (the defined type)
fmt.Println("Kind:", t.Kind()) // int (the underlying kind)
}
type Person struct {
Name string
}
func main() {
p := &Person{"Alice"}
t := reflect.TypeOf(p)
fmt.Println("Kind:", t.Kind()) // ptr
fmt.Println("Elem:", t.Elem()) // main.Person (what pointer points to)
// Get the underlying struct type
structType := t.Elem()
fmt.Println("Struct Kind:", structType.Kind()) // struct
}
Value Information: reflect.Value
Getting values:
var x int = 42
v := reflect.ValueOf(x)
// Extract the actual value
fmt.Println(v.Int()) // 42
fmt.Println(v.Interface()) // 42 (as interface{})
// Type assertion from Interface()
actual := v.Interface().(int)
fmt.Println(actual) // 42
Type-specific getters:
// For different types
var i int = 42
var f float64 = 3.14
var s string = "hello"
var b bool = true
vi := reflect.ValueOf(i)
fmt.Println(vi.Int()) // 42
vf := reflect.ValueOf(f)
fmt.Println(vf.Float()) // 3.14
vs := reflect.ValueOf(s)
fmt.Println(vs.String()) // hello
vb := reflect.ValueOf(b)
fmt.Println(vb.Bool()) // true
Checking if value is valid and can be set:
var x int
v := reflect.ValueOf(x)
fmt.Println(v.IsValid()) // true (value exists)
fmt.Println(v.CanSet()) // false (passed by value, not settable)
// To make it settable, pass a pointer
v2 := reflect.ValueOf(&x).Elem()
fmt.Println(v2.CanSet()) // true
Converting back to interface{}:
var x int = 42
v := reflect.ValueOf(x)
// Get back as interface{}
i := v.Interface()
// Type assert to get original type
original := i.(int)
fmt.Println(original) // 42
Setting Values
To modify a value through reflection, you must pass a pointer:
func main() {
var x int = 42
// ❌ Wrong: can't set value passed directly
v := reflect.ValueOf(x)
// v.SetInt(100) // Panic: reflect: reflect.Value.SetInt using unaddressable value
// ✓ Correct: pass pointer and use Elem()
v2 := reflect.ValueOf(&x).Elem()
v2.SetInt(100)
fmt.Println(x) // 100
}
Type-specific setters:
var i int
var f float64
var s string
var b bool
reflect.ValueOf(&i).Elem().SetInt(42)
reflect.ValueOf(&f).Elem().SetFloat(3.14)
reflect.ValueOf(&s).Elem().SetString("hello")
reflect.ValueOf(&b).Elem().SetBool(true)
fmt.Println(i, f, s, b) // 42 3.14 hello true
Using Set() with interface{}:
var x int = 42
v := reflect.ValueOf(&x).Elem()
// Set using another reflect.Value
newValue := reflect.ValueOf(100)
v.Set(newValue)
fmt.Println(x) // 100
Important: Type must match!
var x int = 42
v := reflect.ValueOf(&x).Elem()
// ❌ Wrong: trying to set float to int
// v.SetFloat(3.14) // Panic: reflect: call of reflect.Value.SetFloat on int Value
// ✓ Correct: use matching type
v.SetInt(100)
Working with Structs
Inspecting struct fields:
type Person struct {
Name string
Age int
Email string
}
func main() {
p := Person{"Alice", 30, "alice@example.com"}
t := reflect.TypeOf(p)
v := reflect.ValueOf(p)
// Number of fields
fmt.Println("Number of fields:", t.NumField())
// Iterate over fields
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("Field: %s, Type: %s, Value: %v\n",
field.Name, field.Type, value.Interface())
}
}
// Output:
// Number of fields: 3
// Field: Name, Type: string, Value: Alice
// Field: Age, Type: int, Value: 30
// Field: Email, Type: string, Value: alice@example.com
Accessing fields by name:
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Alice", 30}
v := reflect.ValueOf(p)
// Get field by name
nameField := v.FieldByName("Name")
fmt.Println(nameField.String()) // Alice
ageField := v.FieldByName("Age")
fmt.Println(ageField.Int()) // 30
}
Setting struct fields:
type Person struct {
Name string
Age int
}
func main() {
p := Person{"Alice", 30}
// Must pass pointer to modify
v := reflect.ValueOf(&p).Elem()
// Set field by name
v.FieldByName("Name").SetString("Bob")
v.FieldByName("Age").SetInt(25)
fmt.Println(p) // {Bob 25}
}
Exported vs unexported fields:
type Person struct {
Name string // Exported
age int // Unexported
}
func main() {
p := Person{"Alice", 30}
v := reflect.ValueOf(&p).Elem()
// Can read unexported field
ageField := v.FieldByName("age")
fmt.Println(ageField.Int()) // 30
// ❌ Cannot set unexported field
// ageField.SetInt(25) // Panic: reflect: reflect.Value.SetInt using value obtained using unexported field
}
Struct Tags
Struct tags are metadata attached to struct fields:
type Person struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"min=0,max=150"`
Email string `json:"email,omitempty" validate:"email"`
}
func main() {
t := reflect.TypeOf(Person{})
// Get field by index
field := t.Field(0)
// Access the tag
tag := field.Tag
// Get specific tag values
jsonTag := tag.Get("json")
validateTag := tag.Get("validate")
fmt.Println("JSON tag:", jsonTag) // name
fmt.Println("Validate tag:", validateTag) // required
}
Iterating over all fields and tags:
type User struct {
ID int `json:"id" db:"user_id"`
Username string `json:"username" db:"username"`
Email string `json:"email" db:"email"`
}
func printStructTags(i interface{}) {
t := reflect.TypeOf(i)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
dbTag := field.Tag.Get("db")
fmt.Printf("Field: %s, JSON: %s, DB: %s\n",
field.Name, jsonTag, dbTag)
}
}
func main() {
printStructTags(User{})
}
// Output:
// Field: ID, JSON: id, DB: user_id
// Field: Username, JSON: username, DB: username
// Field: Email, JSON: email, DB: email
Parsing tag options:
type Person struct {
Name string `json:"name,omitempty"`
}
func main() {
t := reflect.TypeOf(Person{})
field := t.Field(0)
tag := field.Tag.Get("json")
// tag is "name,omitempty"
// Need to parse manually
parts := strings.Split(tag, ",")
name := parts[0] // "name"
options := parts[1:] // ["omitempty"]
fmt.Println("Name:", name)
fmt.Println("Options:", options)
}
Lookup() method for checking tag existence:
type Person struct {
Name string `json:"name"`
Age int
}
func main() {
t := reflect.TypeOf(Person{})
// First field has json tag
field1 := t.Field(0)
if value, ok := field1.Tag.Lookup("json"); ok {
fmt.Println("Has json tag:", value) // Has json tag: name
}
// Second field doesn't have json tag
field2 := t.Field(1)
if _, ok := field2.Tag.Lookup("json"); !ok {
fmt.Println("No json tag") // No json tag
}
}
Working with Slices and Arrays
Inspecting slices:
func main() {
slice := []int{1, 2, 3, 4, 5}
v := reflect.ValueOf(slice)
fmt.Println("Kind:", v.Kind()) // slice
fmt.Println("Length:", v.Len()) // 5
fmt.Println("Capacity:", v.Cap()) // 5
// Access elements
for i := 0; i < v.Len(); i++ {
elem := v.Index(i)
fmt.Println(elem.Int())
}
}
Modifying slice elements:
func main() {
slice := []int{1, 2, 3, 4, 5}
v := reflect.ValueOf(&slice).Elem()
// Modify element at index 2
v.Index(2).SetInt(100)
fmt.Println(slice) // [1 2 100 4 5]
}
Creating new slices:
func main() {
// Create slice of int with length 5
sliceType := reflect.TypeOf([]int{})
newSlice := reflect.MakeSlice(sliceType, 5, 10)
// Set values
for i := 0; i < newSlice.Len(); i++ {
newSlice.Index(i).SetInt(int64(i * 2))
}
// Convert back to []int
result := newSlice.Interface().([]int)
fmt.Println(result) // [0 2 4 6 8]
}
Appending to slices:
func main() {
slice := []int{1, 2, 3}
v := reflect.ValueOf(slice)
// Append new value
newValue := reflect.ValueOf(4)
newSlice := reflect.Append(v, newValue)
result := newSlice.Interface().([]int)
fmt.Println(result) // [1 2 3 4]
}
Working with Maps
Inspecting maps:
func main() {
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
v := reflect.ValueOf(m)
fmt.Println("Kind:", v.Kind()) // map
fmt.Println("Length:", v.Len()) // 3
// Iterate over map
iter := v.MapRange()
for iter.Next() {
key := iter.Key()
value := iter.Value()
fmt.Printf("%s: %d\n", key.String(), value.Int())
}
}
Getting and setting map values:
func main() {
m := map[string]int{"a": 1, "b": 2}
v := reflect.ValueOf(m)
// Get value by key
key := reflect.ValueOf("a")
value := v.MapIndex(key)
fmt.Println(value.Int()) // 1
// Set value
newKey := reflect.ValueOf("c")
newValue := reflect.ValueOf(3)
v.SetMapIndex(newKey, newValue)
fmt.Println(m) // map[a:1 b:2 c:3]
// Delete key (set to zero Value)
v.SetMapIndex(key, reflect.Value{})
fmt.Println(m) // map[b:2 c:3]
}
type Person struct {
Name string
Age int
}
func main() {
// Get the type
personType := reflect.TypeOf(Person{})
// Create new instance (returns pointer)
newPerson := reflect.New(personType)
// newPerson is *Person, use Elem() to get Person
elem := newPerson.Elem()
// Set fields
elem.FieldByName("Name").SetString("Alice")
elem.FieldByName("Age").SetInt(30)
// Convert back
result := newPerson.Interface().(*Person)
fmt.Println(*result) // {Alice 30}
}
Creating zero values:
func main() {
// Create zero value of int
intType := reflect.TypeOf(0)
zeroInt := reflect.Zero(intType)
fmt.Println(zeroInt.Int()) // 0
// Create zero value of string
stringType := reflect.TypeOf("")
zeroString := reflect.Zero(stringType)
fmt.Println(zeroString.String()) // ""
// Create zero value of struct
type Person struct {
Name string
Age int
}
personType := reflect.TypeOf(Person{})
zeroPerson := reflect.Zero(personType)
fmt.Println(zeroPerson.Interface()) // { 0}
}
Creating pointers:
func main() {
var x int = 42
xType := reflect.TypeOf(x)
// Create pointer type
ptrType := reflect.PtrTo(xType)
fmt.Println(ptrType) // *int
// Create new pointer
ptr := reflect.New(xType) // Returns reflect.Value of *int
ptr.Elem().SetInt(100)
result := ptr.Interface().(*int)
fmt.Println(*result) // 100
}
Type Comparison and Conversion
Comparing types:
func main() {
var x int = 42
var y int = 100
var z float64 = 3.14
tx := reflect.TypeOf(x)
ty := reflect.TypeOf(y)
tz := reflect.TypeOf(z)
fmt.Println(tx == ty) // true (both int)
fmt.Println(tx == tz) // false (int vs float64)
}
Checking convertibility:
func main() {
var x int = 42
vx := reflect.ValueOf(x)
float64Type := reflect.TypeOf(float64(0))
// Check if convertible
if vx.Type().ConvertibleTo(float64Type) {
fmt.Println("Can convert int to float64")
// Perform conversion
converted := vx.Convert(float64Type)
fmt.Println(converted.Float()) // 42.0
}
}
Checking assignability:
func main() {
intType := reflect.TypeOf(0)
float64Type := reflect.TypeOf(0.0)
// Can you assign int to float64 variable?
fmt.Println(intType.AssignableTo(float64Type)) // false
// But int is convertible to float64
fmt.Println(intType.ConvertibleTo(float64Type)) // true
}
Common Use Cases
Use Case 1: JSON encoding/decoding (simplified)
// Simplified version of how encoding/json works
func structToMap(obj interface{}) map[string]interface{} {
result := make(map[string]interface{})
v := reflect.ValueOf(obj)
t := reflect.TypeOf(obj)
for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
// Get json tag or use field name
name := field.Tag.Get("json")
if name == "" {
name = field.Name
}
result[name] = value.Interface()
}
return result
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
func main() {
p := Person{"Alice", 30, "alice@example.com"}
m := structToMap(p)
fmt.Println(m)
// map[age:30 email:alice@example.com name:Alice]
}
Use Case 2: Validation framework
func validate(obj interface{}) []error {
var errors []error
v := reflect.ValueOf(obj)
t := reflect.TypeOf(obj)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
// Check "required" tag
if field.Tag.Get("required") == "true" {
if value.IsZero() {
errors = append(errors,
fmt.Errorf("field %s is required", field.Name))
}
}
// Check "min" tag for numbers
if minStr := field.Tag.Get("min"); minStr != "" {
if value.Kind() == reflect.Int {
min, _ := strconv.Atoi(minStr)
if value.Int() < int64(min) {
errors = append(errors,
fmt.Errorf("field %s must be >= %d", field.Name, min))
}
}
}
}
return errors
}
type User struct {
Name string `required:"true"`
Age int `required:"true" min:"18"`
}
func main() {
u := User{Name: "", Age: 15}
errs := validate(u)
for _, err := range errs {
fmt.Println(err)
}
// field Name is required
// field Age must be >= 18
}
Use Case 3: Generic deep copy
func deepCopy(obj interface{}) interface{} {
original := reflect.ValueOf(obj)
// Create new value of same type
copy := reflect.New(original.Type()).Elem()
copyRecursive(original, copy)
return copy.Interface()
}
func copyRecursive(src, dst reflect.Value) {
switch src.Kind() {
case reflect.Struct:
for i := 0; i < src.NumField(); i++ {
copyRecursive(src.Field(i), dst.Field(i))
}
case reflect.Slice:
dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Cap()))
for i := 0; i < src.Len(); i++ {
copyRecursive(src.Index(i), dst.Index(i))
}
default:
dst.Set(src)
}
}
type Person struct {
Name string
Friends []string
}
func main() {
p1 := Person{
Name: "Alice",
Friends: []string{"Bob", "Charlie"},
}
p2 := deepCopy(p1).(Person)
p2.Name = "Bob"
p2.Friends[0] = "Dave"
fmt.Println(p1) // {Alice [Bob Charlie]}
fmt.Println(p2) // {Bob [Dave Charlie]}
}
Use Case 4: ORM-style database mapping
// Simplified ORM: generate SELECT query from struct
func generateSelect(obj interface{}) string {
t := reflect.TypeOf(obj)
var columns []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Get column name from db tag
column := field.Tag.Get("db")
if column == "" {
column = field.Name
}
columns = append(columns, column)
}
tableName := strings.ToLower(t.Name()) + "s"
return fmt.Sprintf("SELECT %s FROM %s",
strings.Join(columns, ", "), tableName)
}
type User struct {
ID int `db:"user_id"`
Username string `db:"username"`
Email string `db:"email"`
}
func main() {
query := generateSelect(User{})
fmt.Println(query)
// SELECT user_id, username, email FROM users
}
Performance Considerations
Reflection is slow: It's typically 10-100x slower than direct access.
// Benchmark comparison
func BenchmarkDirect(b *testing.B) {
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 30}
for i := 0; i < b.N; i++ {
_ = p.Name // Direct access: ~1 ns/op
}
}
func BenchmarkReflection(b *testing.B) {
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 30}
v := reflect.ValueOf(p)
for i := 0; i < b.N; i++ {
_ = v.FieldByName("Name").String() // Reflection: ~100 ns/op
}
}
Reflection is not type-safe: Errors happen at runtime, not compile time.
// ❌ This compiles but panics at runtime
v := reflect.ValueOf(42)
v.SetString("hello") // Panic: reflect: call of reflect.Value.SetString on int Value
Tips for better performance:
Cache reflect.Type and reflect.Value when possible
Use field indices instead of FieldByName in loops
Avoid reflection in hot paths
Consider code generation as alternative
// ✓ Better: Cache field lookups
type Person struct {
Name string
Age int
}
var (
personType = reflect.TypeOf(Person{})
nameIndex int
ageIndex int
)
func init() {
// Find field indices once
for i := 0; i < personType.NumField(); i++ {
switch personType.Field(i).Name {
case "Name":
nameIndex = i
case "Age":
ageIndex = i
}
}
}
func processMany(people []Person) {
for _, p := range people {
v := reflect.ValueOf(p)
// Use cached indices instead of FieldByName
name := v.Field(nameIndex).String()
age := v.Field(ageIndex).Int()
_ = name
_ = age
}
}
Common Mistakes and How to Avoid Them
Mistake 1: Not checking if value is settable
// ❌ Wrong: trying to set unaddressable value
var x int = 42
v := reflect.ValueOf(x)
v.SetInt(100) // Panic!
// ✓ Correct: pass pointer
v2 := reflect.ValueOf(&x).Elem()
if v2.CanSet() {
v2.SetInt(100)
}
Mistake 2: Not checking if value is valid
type Person struct {
Name string
}
// ❌ Wrong: accessing invalid value
p := Person{}
v := reflect.ValueOf(p)
age := v.FieldByName("Age") // Field doesn't exist!
fmt.Println(age.Int()) // Panic: call of reflect.Value.Int on zero Value
// ✓ Correct: check IsValid()
if age.IsValid() {
fmt.Println(age.Int())
} else {
fmt.Println("Field does not exist")
}
Mistake 3: Wrong type in Set operations
// ❌ Wrong: type mismatch
var x int = 42
v := reflect.ValueOf(&x).Elem()
v.SetFloat(3.14) // Panic: SetFloat on int Value
// ✓ Correct: match types
v.SetInt(100)
Mistake 4: Forgetting to use Elem() with pointers
type Person struct {
Name string
}
// ❌ Wrong: operating on pointer type
p := &Person{}
v := reflect.ValueOf(p)
// v.NumField() // Panic: call of reflect.Value.NumField on ptr Value
// ✓ Correct: use Elem() to dereference
v2 := reflect.ValueOf(p).Elem()
fmt.Println(v2.NumField()) // Works!
Mistake 6: Not handling all types in Kind() switch
// ❌ Wrong: incomplete handling
func process(v reflect.Value) {
switch v.Kind() {
case reflect.Int:
fmt.Println(v.Int())
case reflect.String:
fmt.Println(v.String())
// What about other types? Will fail silently
}
}
// ✓ Correct: handle all cases
func process(v reflect.Value) {
switch v.Kind() {
case reflect.Int:
fmt.Println(v.Int())
case reflect.String:
fmt.Println(v.String())
default:
fmt.Println("Unsupported type:", v.Kind())
}
}
Best Practices
1. Use reflection sparingly
// Only use reflection when necessary:
// - Building generic libraries (JSON, ORM)
// - Working with unknown types
// - Code generation isn't practical
2. Always check CanSet() before setting
v := reflect.ValueOf(&x).Elem()
if v.CanSet() {
v.SetInt(100)
}
3. Always check IsValid() before accessing
field := v.FieldByName("Name")
if field.IsValid() {
// Safe to use
}
4. Cache Type and Value lookups when possible
// Cache at package level or init()
var personType = reflect.TypeOf(Person{})
5. Document that your function uses reflection
// DeepCopy creates a deep copy of the given value using reflection.
// It works with structs, slices, maps, and primitive types.
// Performance: O(n) where n is the total number of values to copy.
func DeepCopy(obj interface{}) interface{} { }
// Instead of reflection:
// - Use interfaces for polymorphism
// - Use code generation (go generate)
// - Use generics (Go 1.18+)
// - Use type switches for known types
Do you know the types at compile time?
├─ YES: Don't use reflection
│ └─ Use interfaces, type assertions, or generics
└─ NO: Types are truly dynamic?
├─ YES: Is performance critical?
│ ├─ YES: Consider code generation instead
│ └─ NO: Reflection might be appropriate
└─ NO: Rethink your design
File Handling in Go
File Basics
Go provides the os and io packages for file operations.
Files are represented by the *os.File type.
Always close files when done using defer file.Close()
Mistake 3: Not checking if file exists before operations
// ❌ Wrong: assuming file exists
os.Remove("file.txt") // Errors if doesn't exist
// ✓ Correct: check first or ignore specific error
err := os.Remove("file.txt")
if err != nil && !os.IsNotExist(err) {
return err
}
Mistake 4: Not using buffered I/O for large files
// ❌ Wrong: reading large file into memory
data, err := os.ReadFile("large.txt") // May OOM!
// ✓ Correct: use buffered reading
file, err := os.Open("large.txt")
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
// Process line by line
}
// ❌ Wrong: manual path construction
path := "dir/" + "subdir/" + "file.txt" // Wrong on Windows!
// ✓ Correct: use filepath.Join
path := filepath.Join("dir", "subdir", "file.txt") // OS-independent
Mistake 7: Not handling concurrent file access
// ❌ Wrong: concurrent writes without synchronization
for i := 0; i < 10; i++ {
go func(n int) {
file.WriteString(fmt.Sprintf("%d\n", n)) // Race condition!
}(i)
}
// ✓ Correct: use synchronization
var mu sync.Mutex
for i := 0; i < 10; i++ {
go func(n int) {
mu.Lock()
file.WriteString(fmt.Sprintf("%d\n", n))
mu.Unlock()
}(i)
}
Best Practices
1. Always defer file.Close() immediately after opening
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer file.Close() // Right after error check
2. Check for specific error types
if os.IsNotExist(err) {
// File doesn't exist
}
if os.IsPermission(err) {
// Permission denied
}
3. Use buffered I/O for better performance
// For reading
scanner := bufio.NewScanner(file)
// For writing
writer := bufio.NewWriter(file)
defer writer.Flush()