- Introduction to Go
- Go Basics
- Functions and Methods
- Data Structures
- Pointers
- Structs
- Interfaces
- Concurrency
- Error Handling
- Advanced Topics
- Memory Management
- Web Development
- Database Integration
- Testing
- Tools and Commands
- Best Practices and Patterns
- File Operations
Go, also known as Golang, is a statically typed, compiled programming language designed at Google. It provides excellent support for concurrent programming and emphasizes simplicity and efficiency.
Go has specific naming conventions that contribute to code readability:
- Package names: lowercase, single-word names (e.g.,
time
,http
) - Variable names: camelCase for local, PascalCase for exported
- Constant names: PascalCase for exported, all caps with underscores for internal
- Function and method names: camelCase for unexported, PascalCase for exported
- Interface names: PascalCase, often ending with '-er' suffix
- Struct names: PascalCase for exported, camelCase for unexported
Variables in Go are used to store and manipulate data. Here's a comprehensive overview of variables in Go:
-
Basic declaration:
var name string var age int
-
Declaration with initialization:
var name string = "John" var age int = 30
-
Short declaration (type inference):
name := "John" age := 30
-
Multiple declarations:
var ( name string age int isStudent bool )
Variables declared without an explicit initial value are given their zero value:
- Numeric types:
0
- Boolean type:
false
- String type:
""
- Pointer types:
nil
Constants are declared using the const
keyword:
const Pi = 3.14159
const (
StatusOK = 200
StatusNotFound = 404
)
Go requires explicit type conversion:
var x int = 10
var y float64 = float64(x)
- Package-level variables: Declared outside any function
- Local variables: Declared inside a function
- Block-level variables: Declared inside a block (e.g., if statement)
- Use camelCase for variable names
- Exported variables (accessible from other packages) start with an uppercase letter
Inner blocks can declare variables with the same name as outer blocks, shadowing the outer variable:
x := 10
if true {
x := 20 // This x shadows the outer x
fmt.Println(x) // Prints 20
}
fmt.Println(x) // Prints 10
Go does not allow unused variables. The compiler will throw an error if a variable is declared but not used.
The blank identifier _
can be used to ignore values:
x, _ := someFunction() // Ignores the second return value
Additional examples:
This overview covers the essential aspects of variables in Go, including declaration, initialization, scope, naming conventions, and special features like the blank identifier.
Go has several built-in data types:
-
Basic Types:
- Numeric Types:
- Integers: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr
- Floating-point: float32, float64
- Complex: complex64, complex128
- Boolean Type: bool
- String Type: string
- Numeric Types:
-
Composite Types:
- Array: Fixed-length sequence of elements
- Slice: Dynamic-length sequence of elements
- Map: Key-value pairs
- Struct: User-defined type with named fields
-
Other Types:
- Pointer: Stores the memory address of a value
- Function: Functions are first-class citizens
- Interface: Defines a set of method signatures
- Channel: Used for communication between goroutines
Go provides various operators:
- Arithmetic:
+
,-
,*
,/
,%
- Comparison:
==
,!=
,<
,>
,<=
,>=
- Logical:
&&
,||
,!
- Bitwise:
&
,|
,^
,<<
,>>
- Assignment:
=
,+=
,-=
,*=
,/=
, etc. - Address and Pointer:
&
,*
Go supports standard control structures:
-
if-else statements:
if condition { // code } else if anotherCondition { // code } else { // code }
-
for loops:
for i := 0; i < 10; i++ { // code } // while-like loop for condition { // code } // infinite loop for { // code } // range-based loop for index, value := range collection { // code }
-
switch statements:
switch variable { case value1: // code case value2, value3: // code default: // code } // tagless switch switch { case condition1: // code case condition2: // code default: // code }
-
defer statements:
defer function()
Composite literals provide a concise way to create and initialize composite types:
-
Array Literal:
primes := [5]int{2, 3, 5, 7, 11}
-
Slice Literal:
fruits := []string{"apple", "banana", "orange"}
-
Map Literal:
ages := map[string]int{ "Alice": 30, "Bob": 25, }
-
Struct Literal:
type Person struct { Name string Age int } p := Person{Name: "John", Age: 28}
-
Nested Composite Literals:
type Address struct { Street string City string } type Employee struct { Name string Address Address } emp := Employee{ Name: "John Doe", Address: Address{ Street: "123 Main St", City: "Anytown", }, }
Go provides a variety of formatting functions in the fmt
package for printing and string manipulation:
-
Printing Functions:
fmt.Print
: Prints its arguments with their default formats.fmt.Println
: Like Print, but adds spaces between arguments and a newline at the end.fmt.Printf
: Formats according to a format specifier and prints.
-
String Formatting Functions:
fmt.Sprint
: Returns a string containing the default formats of its arguments.fmt.Sprintln
: Like Sprint, but adds spaces between arguments and a newline at the end.fmt.Sprintf
: Returns a string formatted according to a format specifier.
-
Formatted I/O:
fmt.Fprintf
: Formats and writes to a specified io.Writer.fmt.Fscanf
: Scans formatted text from a specified io.Reader.
-
Scanning Functions:
fmt.Scan
: Scans text read from standard input, storing successive space-separated values into successive arguments.fmt.Scanf
: Scans text read from standard input, parsing according to a format string.fmt.Scanln
: Like Scan, but stops scanning at a newline.
-
Formatting Directives: Formatting directives are used with
Printf
,Sprintf
, and related functions to specify how to format values. Here are some common directives:%v
: The value in a default format%+v
: The value in a default format with field names for structs%#v
: A Go-syntax representation of the value%T
: A Go-syntax representation of the type of the value%t
: The word true or false (for boolean values)%d
: Base 10 integer%b
: Base 2 integer%o
: Base 8 integer%x
,%X
: Base 16 integer, with lower-case/upper-case letters for a-f%f
,%F
: Decimal point, no exponent%e
,%E
: Scientific notation%s
: String%q
: Double-quoted string%p
: Pointer address%c
: The character represented by the corresponding Unicode code point
Example usage of formatting directives:
num := 42
pi := 3.14159
name := "Gopher"
fmt.Printf("Integer: %d\n", num)
fmt.Printf("Float: %.2f\n", pi)
fmt.Printf("String: %s\n", name)
fmt.Printf("Boolean: %t\n", true)
fmt.Printf("Value: %v\n", num)
fmt.Printf("Pointer: %p\n", &num)
fmt.Printf("Type: %T\n", pi)
fmt.Printf("Quoted string: %q\n", name)
fmt.Printf("Hex: %#x\n", num)
Output:
Integer: 42
Float: 3.14
String: Gopher
Boolean: true
Value: 42
Pointer: 0xc0000b4008
Type: float64
Quoted string: "Gopher"
Hex: 0x2a
These formatting directives provide fine-grained control over how values are formatted in output strings. They are essential for creating well-formatted, readable output in Go programs.
Functions in Go are declared using the func
keyword:
func add(a, b int) int {
return a + b
}
Multiple return values:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Named return values:
func rectangle(width, height float64) (area, perimeter float64) {
area = width * height
perimeter = 2 * (width + height)
return // naked return
}
Methods are functions associated with a type:
type Rectangle struct {
width, height float64
}
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// Pointer receiver
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
Additional examples:
The init
function is automatically executed before the main function:
func init() {
// Initialization code
}
Characteristics of init
:
- It doesn't take any arguments.
- It doesn't return any values.
- It's optional to define an
init
function. - Multiple
init
functions can be defined within a single package. init
functions are executed in the order they appear in the source file.- They run after all variable declarations in the package have evaluated their initializers.
Variadic functions can accept a variable number of arguments:
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
// Usage
fmt.Println(sum(1, 2, 3)) // Output: 6
fmt.Println(sum(4, 5, 6, 7)) // Output: 22
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(sum(numbers...)) // Output: 15
Anonymous functions:
func() {
fmt.Println("I'm anonymous!")
}()
add := func(a, b int) int {
return a + b
}
Closures:
func counterFactory(start int) func() int {
count := start
return func() int {
count++
return count
}
}
counter1 := counterFactory(0)
counter2 := counterFactory(10)
fmt.Println(counter1()) // Output: 1
fmt.Println(counter1()) // Output: 2
fmt.Println(counter2()) // Output: 11
Additional closure example:
func debitCardFunction(balance int) func(int) int {
return func(withdrawal int) int {
if balance < withdrawal {
return balance
}
balance -= withdrawal
return balance
}
}
debitCard := debitCardFunction(100)
fmt.Println(debitCard(20)) // Output: 80
fmt.Println(debitCard(30)) // Output: 50
Reference: Closure Examples
The defer
keyword postpones the execution of a function until the surrounding function returns:
func example() {
defer fmt.Println("This prints last")
fmt.Println("This prints first")
}
Multiple defers:
Deferred functions are executed in Last-In-First-Out (LIFO) order. When multiple defer statements are used, they are pushed onto a stack and executed in reverse order when the surrounding function returns.
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
In this example, the deferred functions are executed in reverse order of their declaration after the main function body completes.
Arrays in Go are fixed-size sequences of elements of the same type. Key characteristics:
- Declaration and Initialization:
// Declaration with size
var arr [5]int
// Declaration with initialization
arr := [5]int{1, 2, 3, 4, 5}
// Size inference
arr := [...]int{1, 2, 3, 4, 5}
// Partial initialization
arr := [5]int{1, 2} // [1 2 0 0 0]
- Memory Layout:
- Arrays are value types (copying an array creates a new copy)
- Contiguous memory allocation
- Fixed size at compile time
- Size is part of the type ([5]int and [6]int are different types)
arr1 := [3]int{1, 2, 3}
arr2 := arr1 // Creates a copy
arr2[0] = 10 // Doesn't affect arr1
fmt.Println(arr1) // [1 2 3]
fmt.Println(arr2) // [10 2 3]
- Multi-dimensional Arrays:
// 2D array declaration
var matrix [3][4]int
// 2D array initialization
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
Slices are dynamic, flexible views into arrays. They consist of:
- Pointer to underlying array
- Length (len)
- Capacity (cap)
- Creation Methods:
// From array
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:4] // [2 3 4]
// Using make
slice := make([]int, 3, 5) // length 3, capacity 5
// Literal syntax
slice := []int{1, 2, 3}
// Zero value
var slice []int // nil slice
- Slice Header Structure:
type SliceHeader struct {
Data uintptr // Pointer to the underlying array
Len int // Number of elements in the slice
Cap int // Capacity (maximum number of elements)
}
- Capacity Growth: When appending to a slice beyond its capacity:
- If cap < 1024: new_cap = old_cap * 2
- If cap ≥ 1024: new_cap = old_cap + old_cap/4
slice := make([]int, 0)
fmt.Printf("Len: %d, Cap: %d\n", len(slice), cap(slice))
for i := 0; i < 10; i++ {
slice = append(slice, i)
fmt.Printf("Len: %d, Cap: %d\n", len(slice), cap(slice))
}
// Output shows capacity doubling:
// Len: 0, Cap: 0
// Len: 1, Cap: 1
// Len: 2, Cap: 2
// Len: 3, Cap: 4
// Len: 4, Cap: 4
// Len: 5, Cap: 8
// ...
- Slice Operations and Behavior:
a) Slicing Operation [low:high:max]:
low: starting index (inclusive) high: ending index (exclusive) max: maximum capacity index (exclusive)
arr := [6]int{1, 2, 3, 4, 5, 6}
slice1 := arr[1:4] // [2 3 4], len=3, cap=5
slice2 := arr[1:4:4] // [2 3 4], len=3, cap=3
// Visualization:
// arr: [1 2 3 4 5 6]
// ↑ ↑ ↑
// low high max
b) Sharing Underlying Array:
original := []int{1, 2, 3, 4, 5}
slice1 := original[1:3]
slice2 := original[2:4]
slice1[1] = 10 // Affects original and slice2
fmt.Println(original) // [1 2 10 4 5]
fmt.Println(slice2) // [10 4]
- Common Pitfalls and Solutions:
a) Unexpected Sharing:
// Potential issue
data := []int{1, 2, 3, 4, 5}
slice := data[2:4]
newData := append(slice, 6) // Might modify original data
// Solution: Use full slice expression
slice := data[2:4:4] // Limits capacity
newData := append(slice, 6) // Creates new array
b) Memory Leaks with Large Arrays:
// Potential memory leak
largeArray := [1000000]int{}
slice := largeArray[1:5] // Holds reference to large array
// Solution: Copy needed elements
slice := make([]int, 4)
copy(slice, largeArray[1:5])
- Performance Considerations:
a) Pre-allocation for Known Size:
// Inefficient
var slice []int
for i := 0; i < 10000; i++ {
slice = append(slice, i)
}
// Efficient
slice := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
slice = append(slice, i)
}
b) Copying vs Referencing:
// Full copy (new backing array)
newSlice := make([]int, len(originalSlice))
copy(newSlice, originalSlice)
// Reference (shares backing array)
reference := originalSlice[:]
- Useful Patterns:
a) Stack Operations:
// Push
stack = append(stack, value)
// Pop
n := len(stack) - 1
value := stack[n]
stack = stack[:n]
// Top
value := stack[len(stack)-1]
b) Queue Operations:
// Enqueue
queue = append(queue, value)
// Dequeue (inefficient for large queues)
value := queue[0]
queue = queue[1:]
// Dequeue (efficient)
value := queue[0]
copy(queue, queue[1:])
queue = queue[:len(queue)-1]
- Common Functions and Operations:
// Length and Capacity
len(slice) // Number of elements
cap(slice) // Maximum capacity
// Append
slice = append(slice, 1, 2, 3)
slice = append(slice, otherSlice...)
// Copy
copy(dst, src)
// Clear
slice = slice[:0] // Maintains capacity
slice = nil // Releases memory
// Remove element
slice = append(slice[:i], slice[i+1:]...)
// Filter
filtered := slice[:0]
for _, x := range slice {
if keepElement(x) {
filtered = append(filtered, x)
}
}
These concepts and examples cover the main aspects of arrays and slices in Go, including memory management, capacity growth, common operations, and best practices for various scenarios.
Additional examples:
2D slices example:
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}
Maps are key-value data structures in Go that allow efficient lookup, insertion, and deletion operations. They are implemented as hash tables and provide an unordered collection of key-value pairs. Here's a brief overview of maps:
- Declaration:
map[KeyType]ValueType
- Initialization: Can be done using
make()
or map literals - Operations: Adding, updating, deleting, and retrieving values
- Concurrency: Not safe for concurrent use without additional synchronization
- Performance: Average time complexity of O(1) for basic operations
Key features:
- Keys must be comparable (e.g., numbers, strings, structs with comparable fields)
- Values can be of any type
- Maps automatically grow as needed
- The zero value of a map is nil
In Go, maps are reference types, meaning that when you pass a map to a function, you are passing a reference to the original map, not a copy. This allows modifications made to the map within the function to affect the original map.
Example of modifying a map in a function:
func updateMap(m map[string]int) {
m["banana"] = 5 // Modifies the original map
}
func main() {
m := map[string]int{
"apple": 1,
"banana": 2,
}
updateMap(m)
fmt.Println(m["banana"]) // Output: 5
}
Maps in Go automatically resize when the number of elements exceeds a certain threshold, which is determined by the load factor. When a map grows, Go allocates a new, larger underlying array and rehashes the existing key-value pairs into the new array. This process is handled automatically, and the programmer does not need to manage the resizing manually.
Example of adding elements to a map:
m := make(map[string]int)
for i := 0; i < 100; i++ {
m[fmt.Sprintf("key%d", i)] = i // The map grows as needed
}
m := map[string]int{
"apple": 1,
"banana": 2,
}
// Add or update
m["cherry"] = 3
// Delete
delete(m, "banana")
// Check existence
if value, exists := m["apple"]; exists {
fmt.Println(value) // Output: 1
}
Additional examples:
This example demonstrates various map operations, including declaration, initialization, adding elements, deleting elements, and iteration.
Pointers store the memory address of a value:
x := 10
ptr := &x
fmt.Println(*ptr) // Output: 10
*ptr = 20
fmt.Println(x) // Output: 20
type Person struct {
Name string
Age int
}
func modifyPerson(p *Person) {
p.Name = "Alice"
p.Age = 30
}
person := Person{"Bob", 25}
modifyPerson(&person)
fmt.Println(person) // Output: {Alice 30}
Go provides automatic dereferencing of pointers to structs, but only in specific contexts. This feature is called "implicit dereferencing" and applies exclusively to method receivers. Here's a more detailed explanation:
-
Method Receivers: When a method is defined with a pointer receiver, Go allows you to call that method on either a pointer or a value of the struct type. The compiler automatically handles the dereferencing.
type Person struct { Name string } func (p *Person) SetName(name string) { p.Name = name // p is automatically dereferenced } person := Person{} person.SetName("Alice") // Go automatically takes the address of person
-
Function Parameters: This implicit dereferencing does not apply to regular function parameters. If a function expects a pointer, you must explicitly pass a pointer.
func UpdateName(p *Person, name string) { p.Name = name } person := Person{} UpdateName(&person, "Bob") // Must explicitly take the address of person
-
Compilation Error: If you try to pass a value to a function expecting a pointer (or vice versa) without explicit conversion, you'll get a compilation error:
func UpdateName(p *Person, name string) { p.Name = name } person := Person{} UpdateName(person, "Charlie") // Compilation error: cannot use person (type Person) as type *Person
This distinction is important to understand because it affects how you write and call methods versus functions in Go, and it's a key part of Go's approach to balancing convenience and explicitness in pointer usage.
Additional examples:
Structs in Go are composite types that group together variables under a single name.
type Person struct {
Name string
Age int
}
// Creating and initializing a struct
// Note: Each line in struct initialization must end with a comma,
// including the last line. This makes version control diffs cleaner
// and makes it easier to add/remove fields.
p1 := Person{
Name: "Alice", // comma required
Age: 30, // comma required even on last line
}
// This is harder to maintain:
p2 := Person{
Name: "Bob", // comma required
Age: 25 // no comma makes it harder to add new fields
}
// Multi-field struct with proper formatting:
type Employee struct {
Person: Person{
Name: "John", // comma required
Age: 30, // comma required
}, // comma required
Address: Address{
Street: "123 Main St", // comma required
City: "Anytown", // comma required
}, // comma required
Salary: 50000, // comma required
} // no comma needed for closing brace
Methods can be defined on structs:
func (p Person) Greet() string {
return fmt.Sprintf("Hello, my name is %s and I'm %d years old", p.Name, p.Age)
}
fmt.Println(p1.Greet())
Methods can use pointer receivers to modify the struct:
func (p *Person) Birthday() {
p.Age++
}
p1.Birthday()
fmt.Println(p1.Age) // Increased by 1
Go supports struct embedding for composition:
type Address struct {
Street string
City string
}
type Employee struct {
Person // Embedding Person
Address // Embedding Address
Salary float64
}
emp := Employee{
Person: Person{Name: "John", Age: 30},
Address: Address{Street: "123 Main St", City: "Anytown"},
Salary: 50000,
}
fmt.Println(emp.Name) // Accessing embedded Person field
fmt.Println(emp.Street) // Accessing embedded Address field
You can embed structs or other types without specifying a field name:
type Manager struct {
Employee
Department string
}
mgr := Manager{
Employee: Employee{
Person: Person{Name: "Jane", Age: 35},
Address: Address{Street: "456 Elm St", City: "Othertown"},
Salary: 75000,
},
Department: "IT",
}
Struct tags provide metadata about struct fields:
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"-" validate:"required,min=8"`
}
// Using reflection to access tags
t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Email")
fmt.Println(field.Tag.Get("json")) // Output: email
fmt.Println(field.Tag.Get("validate")) // Output: required,email
You can create one-off structs without defining a new type:
point := struct {
X, Y int
}{10, 20}
Structs are comparable if all their fields are comparable:
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // Output: true
You can use the new
function to create a pointer to a zeroed struct:
p := new(Person)
p.Name = "David"
p.Age = 40
Additional examples:
This file demonstrates various aspects of struct usage in Go, including definition, methods, embedding, and more complex struct compositions.
Interfaces in Go are a fundamental concept that define a set of method signatures. They provide a powerful way to achieve abstraction and polymorphism in Go programs. Unlike some other languages, Go interfaces are implemented implicitly, which allows for a high degree of flexibility and decoupling between packages.
Definition and Implementation: An interface type is defined as a set of method signatures. For example:
type Writer interface {
Write([]byte) (int, error)
}
A type implicitly implements an interface if it defines all the methods specified by that interface. There's no need for explicit declaration of intent to implement an interface:
type FileWriter struct {
// ...
}
In this example, FileWriter
implicitly implements the Writer
interface because it has a Write
method with the correct signature.
Interface Values: An interface value consists of two components: a concrete value and a dynamic type. This allows for runtime polymorphism:
var w Writer
w = FileWriter{} // w now holds a FileWriter value
Empty Interface:
The empty interface interface{}
is satisfied by all types, as it has no methods. It's often used to handle values of unknown type:
func PrintAnything(v interface{}) {
fmt.Println(v)
}
Type Assertions and Type Switches: Go provides mechanisms to work with the concrete types behind interfaces:
Type Assertions allow you to extract the underlying value from an interface:
fw, ok := w.(FileWriter)
if ok {
// w holds a FileWriter
}
Type Switches determine the type of an interface value:
switch v := w.(type) {
case FileWriter:
fmt.Println("FileWriter:", v)
case *BufferedWriter:
fmt.Println("BufferedWriter:", v)
default:
fmt.Println("Unknown type")
}
Interface Composition: Interfaces can be composed of other interfaces, allowing for more complex abstractions:
type ReadWriter interface {
Reader
Writer
}
Best Practices:
- Keep interfaces small and focused on a single responsibility.
- Use interfaces to define behavior, not data.
- Accept interfaces, return concrete types in function signatures.
- Use the
io.Reader
andio.Writer
interfaces when dealing with streams of data.
Additional examples:
Interfaces play a crucial role in Go's design philosophy, enabling loose coupling between components and facilitating easier testing and maintenance of code. They are extensively used in the standard library and are a key feature for writing flexible and reusable Go code.
The Stringer interface is used for custom string representations of types. It's particularly useful for printing custom types in a human-readable format.
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)
}
p := Person{Name: "Alice", Age: 30}
fmt.Println(p) // Output: Alice (30 years old)
By implementing the String() method, we provide a custom string representation for the Person struct. This is automatically used when the struct is printed or converted to a string.
Go provides multiple ways to sort collections, including sort.Sort
, sort.Slice
, and slices.Sort
. Each method has its own use cases and advantages.
The sort.Interface
is used for custom sorting of collections. It requires implementing three methods: Len()
, Less()
, and Swap()
.
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
type Person struct {
Name string
Age 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] }
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Sort(ByAge(people))
fmt.Println(people)
This example demonstrates how to implement custom sorting for a slice of Person
structs based on their age using sort.Sort
.
sort.Slice
is a more convenient way to sort slices, introduced in Go 1.8. It doesn't require implementing the sort.Interface
:
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Println(people)
In Go 1.21, the slices
package was introduced, providing a type-safe and more efficient sorting method:
import "slices"
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
slices.SortFunc(people, func(a, b Person) int {
return a.Age - b.Age
})
fmt.Println(people)
-
sort.Sort
:- Requires implementing
sort.Interface
- Useful for custom types with complex sorting logic
- More verbose but offers full control over sorting behavior
- Requires implementing
-
sort.Slice
:- More convenient for one-off sorting operations
- Doesn't require implementing
sort.Interface
- Less type-safe (uses
interface{}
internally) - Slightly less performant than
sort.Sort
-
slices.Sort
/slices.SortFunc
:- Type-safe and generally more efficient
- Available only in Go 1.21+
- Preferred method for simple sorting operations in newer Go versions
Choose the method that best fits your use case:
- Use
sort.Sort
for custom types with complex sorting logic or when you need to reuse the sorting logic. - Use
sort.Slice
for quick, one-off sorting operations in older Go versions. - Use
slices.Sort
orslices.SortFunc
for efficient, type-safe sorting in Go 1.21+.
This overview covers the main sorting methods in Go, their differences, and appropriate use cases. Is there any specific aspect of sorting in Go you'd like me to elaborate on?
Additional examples:
Goroutines are lightweight threads managed by the Go runtime. They are more efficient than OS threads and allow for concurrent execution of functions. Here's a brief comparison:
Goroutines vs OS threads vs threads in other languages:
- Goroutines: Managed by Go runtime, lightweight, can run many concurrently
- OS threads: Managed by the operating system, heavier, limited by system resources
- Threads in other languages: Often map directly to OS threads, heavier than goroutines
Goroutines are lightweight because:
-
They have a smaller stack size (starting at 2KB)
-
They are multiplexed onto a small number of OS threads: This means that many goroutines can run on a single OS thread. The Go runtime scheduler manages this multiplexing, allowing it to run thousands or even millions of goroutines on just a few OS threads. This is more efficient than creating a new OS thread for each concurrent task, as OS threads are more resource-intensive.
-
Context switching between goroutines is faster than OS threads: Context switching is the process of storing the state of a thread or goroutine so that it can be resumed later, and then switching to run another thread or goroutine. For goroutines, this process is managed by the Go runtime and is much faster than OS-level context switches for several reasons:
- Goroutine state is smaller and simpler than full thread state, so it's quicker to save and restore.
- The Go scheduler doesn't need to switch to kernel mode to perform a context switch, which is a time-consuming operation for OS threads.
- The Go scheduler can make more intelligent decisions about when to switch contexts, as it has more information about the goroutines it's managing than the OS has about threads.
These factors contribute to making goroutines much more lightweight and efficient than OS threads, allowing Go programs to handle high levels of concurrency with relatively low overhead.
func printNumbers() {
for i := 1; i <= 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
go printNumbers()
time.Sleep(time.Second)
Channels are used for communication between goroutines. There are two types of channels in Go:
-
Unbuffered Channels:
- Created with
make(chan T)
- Synchronous: sender blocks until receiver is ready
- Used when you need guaranteed delivery and synchronization
- Created with
-
Buffered Channels:
- Created with
make(chan T, capacity)
- Asynchronous: sender only blocks when buffer is full
- Used when you want to decouple sender and receiver, or manage flow control
- Created with
Differences between buffered and unbuffered channels:
- Unbuffered channels provide immediate handoff and synchronization
- Buffered channels allow for some decoupling and can improve performance in certain scenarios
Usage:
- Use unbuffered channels when you need strict synchronization between goroutines
- Use buffered channels when you want to allow some slack between sender and receiver, or to implement a semaphore-like behavior
Example:
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println(value)
// Buffered channel
bufCh := make(chan int, 2)
bufCh <- 1
bufCh <- 2
fmt.Println(<-bufCh, <-bufCh)
// Closing channels
close(ch)
Additional examples:
- Channel Operations and Patterns
- Concurrency Patterns
- Odd even
- Odd even using channels and wg
- Odd even using just channels
- Odd even using wait group
This file demonstrates various channel operations, including creating channels, sending and receiving values, and using channels for communication between goroutines. It also shows how to use channels to implement a simple worker pool pattern.
The select statement is used to work with multiple channels:
select {
case msg1, ok := <-ch1:
if !ok {
fmt.Println("ch1 is closed")
} else {
fmt.Println("Received from ch1:", msg1)
}
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
case ch3 <- 42:
fmt.Println("Sent to ch3")
default:
fmt.Println("No channel operations ready")
}
Key points about select with closed channels:
- When receiving from a channel, you can use the two-value form of channel receive to check if the channel is closed.
- For a closed channel, the receive operation
<-ch
returns immediately with the zero value of the channel's type andok
set tofalse
. - The select statement will choose a closed channel if no other cases are ready, allowing you to detect and handle closed channels.
- Sending on a closed channel will cause a panic, so it's important to ensure a channel is open before sending.
Example with a closed channel:
ch := make(chan int)
close(ch)
select {
case val, ok := <-ch:
if !ok {
fmt.Println("Channel is closed")
} else {
fmt.Println("Received:", val)
}
default:
fmt.Println("No value received")
}
// Output: Channel is closed
This behavior allows for graceful handling of channel closure in concurrent programs.
Go provides several synchronization primitives:
-
sync.Mutex:
var mu sync.Mutex mu.Lock() // Critical section mu.Unlock()
-
sync.RWMutex:
var rwmu sync.RWMutex rwmu.RLock() // Read operations rwmu.RUnlock() rwmu.Lock() // Write operations rwmu.Unlock()
-
sync.WaitGroup:
var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() // Do work }() wg.Wait()
-
sync.Once:
var once sync.Once once.Do(func() { // This will only be executed once })
-
sync.Cond:
var mu sync.Mutex cond := sync.NewCond(&mu) // Wait for condition cond.L.Lock() for !condition { cond.Wait() } cond.L.Unlock() // Signal condition change cond.Signal() // or cond.Broadcast()
Here's a brief overview of some common concurrency patterns in Go:
- Worker Pool Pattern:
func workerPool(numWorkers int, jobs <-chan int, results chan<- int) {
for i := 0; i < numWorkers; i++ {
go func() {
for job := range jobs {
results <- processJob(job)
}
}()
}
}
- Generator Pattern:
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
- Fan-Out, Fan-In Pattern: Distributes work across multiple goroutines (fan-out) and then combines the results (fan-in).
func fanOut(input <-chan int, numWorkers int) []<-chan int {
outputs := make([]<-chan int, numWorkers)
for i := 0; i < numWorkers; i++ {
outputs[i] = worker(input)
}
return outputs
}
func fanIn(channels ...<-chan Result) <-chan Result {
var wg sync.WaitGroup
multiplexed := make(chan Result)
multiplex := func(c <-chan Result) {
defer wg.Done()
for result := range c {
multiplexed <- result
}
}
wg.Add(len(channels))
for _, c := range channels {
go multiplex(c)
}
go func() {
wg.Wait()
close(multiplexed)
}()
return multiplexed
}
- Pipeline Pattern: A series of stages connected by channels, where each stage is a group of goroutines running the same function.
func stage1(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * 2
}
}()
return out
}
func stage2(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range n {
out <- n + 1
}
}()
return out
}
// Usage:
// input := generator(1, 2, 3)
// result := stage2(stage1(input))
-
Context Pattern: Uses the
context
package for managing cancellation, deadlines, and passing request-scoped values across API boundaries and between processes. Key features:- Cancellation: Allows canceling long-running operations
- Deadlines: Sets time limits for operations
- Values: Carries request-scoped data
Basic usage:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() select { case <-ctx.Done(): fmt.Println("Operation cancelled or timed out") return ctx.Err() case result := <-doOperation(ctx): return result }
The context example demonstrates the use of the
context
package for managing goroutine lifecycles and cancellation. It shows how to create a context with a timeout and use it to control multiple goroutines. -
Confinement: Restricts data access to a single goroutine, simplifying concurrent programming and avoiding race conditions. Two types:
- Lexical confinement: Data confined by scope (e.g., local variables).
- Ad-hoc confinement: Data confined by convention (e.g., only one goroutine accesses shared data).
Example: Confinement
This example demonstrates confinement by:
- Each goroutine works on its own slice element (
&result[i]
). - No shared mutable state between goroutines.
result
slice is pre-allocated, avoiding dynamic resizing.
Mutex locks are avoided because:
- Each goroutine writes to a distinct memory location.
- No concurrent access to shared data structures.
Without confinement, if using a shared slice, a mutex would be required.
-
Mutex for shared state: Uses mutual exclusion to protect shared data structures from concurrent access. It can be avoided by using confinement in above example. Examples: Mutex Map, Mutex Example
-
Or-Done pattern: Allows for cancellation of multiple channels simultaneously. A separate re-usable function is created for this pattern. It is useful when you have multiple channels and want to cancel them all when one of them is done. Example: Or Done
Channel Management Best Practices:
-
When to close channels:
- Required: For range loops and signaling completion to multiple goroutines
- Not required: One-time communication, garbage-collected channels, synchronization-only channels
-
Common mistakes to avoid:
// Never write to a closed channel
ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel
// Never close a channel twice
ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel
Examples:
- Context Example
- Generator
- Confinement
- Prime Fan-in Fan-out
- Mutex Map
- Mutex Example
- Prime Pipeline
- Or Done
These patterns demonstrate various techniques for managing concurrency, from distributing work and combining results to protecting shared resources and handling cancellation.
In Go, errors are represented by the built-in error
interface:
type error interface {
Error() string
}
Any type that implements this interface can be used as an error. The most common way to create errors is using the errors.New()
function:
import "errors"
err := errors.New("something went wrong")
Example of using errors:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
You can create custom error types by implementing the error
interface:
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}
func someFunction() error {
return &MyError{Code: 404, Message: "Not Found"}
}
err := someFunction()
if err != nil {
fmt.Println(err) // Output: Error 404: Not Found
}
Panic is used for unrecoverable errors. It stops the normal execution of the current goroutine and begins panicking:
func doSomething() {
panic("something went terribly wrong")
}
func main() {
doSomething()
fmt.Println("This line will not be executed")
}
recover
is used to regain control of a panicking goroutine. It's only useful inside deferred functions:
func mayPanic() {
panic("a problem")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered. Error:\n", r)
}
}()
mayPanic()
fmt.Println("Never reached")
}
defer
statements are often used with panics. Deferred functions are still executed in panic situations:
func riskyFunction() {
defer func() {
fmt.Println("This will always execute")
}()
panic("Oops!")
}
func main() {
riskyFunction()
}
fmt.Errorf
is used to create formatted error messages:
name := "John"
age := 30
err := fmt.Errorf("invalid user: %s (age %d)", name, age)
fmt.Println(err)
// Output: invalid user: John (age 30)
It can also wrap errors using the %w
verb (Go 1.13+):
originalErr := errors.New("database connection failed")
wrappedErr := fmt.Errorf("failed to fetch user data: %w", originalErr)
These are predefined errors used for specific conditions:
import "io"
var ErrEOF = errors.New("EOF")
func readFile() error {
// ...
return io.EOF
}
if err := readFile(); err == io.EOF {
fmt.Println("End of file reached")
}
Useful for handling different types of errors:
type NetworkError struct {
Code int
}
func (e *NetworkError) Error() string {
return fmt.Sprintf("Network error with code: %d", e.Code)
}
func handleError(err error) {
switch e := err.(type) {
case *NetworkError:
fmt.Printf("Network error with code %d\n", e.Code) // Same output as fmt.Println(e)
default:
fmt.Println("Unknown error:", err)
}
}
Go 1.13 introduced errors.Is
and errors.As
to improve error handling, especially when dealing with wrapped errors. These functions make it easier to check for specific error types or values in an error chain.
errors.Is
checks if a specific error value exists anywhere in the error chain.
Syntax:
func Is(err, target error) bool
Use errors.Is
when you want to compare an error to a sentinel error value.
Example:
var ErrNotFound = errors.New("not found")
func fetchItem(id string) (Item, error) {
// ... implementation ...
return Item{}, fmt.Errorf("failed to fetch item: %w", ErrNotFound)
}
func main() {
_, err := fetchItem("123")
if errors.Is(err, ErrNotFound) {
fmt.Println("The item was not found")
} else {
fmt.Println("An unknown error occurred:", err)
}
}
In this example, errors.Is
checks if ErrNotFound
is anywhere in the error chain, even if it's wrapped.
errors.As
finds the first error in the error chain that matches the target type, and if so, sets the target to that error value.
Syntax:
func As(err error, target interface{}) bool
Use errors.As
when you need to check for a specific error type and access its fields or methods.
Example:
type NetworkError struct {
Code int
Message string
}
func (e *NetworkError) Error() string {
return fmt.Sprintf("network error: %s (code: %d)", e.Message, e.Code)
}
func fetchData() error {
// Simulating a network error
return fmt.Errorf("failed to fetch data: %w", &NetworkError{Code: 500, Message: "Internal Server Error"})
}
func main() {
err := fetchData()
var netErr *NetworkError
if errors.As(err, &netErr) {
fmt.Printf("Network error occurred. Code: %d, Message: %s\n", netErr.Code, netErr.Message)
} else {
fmt.Println("An unknown error occurred:", err)
}
}
In this example, errors.As
checks if there's a NetworkError
in the error chain. If found, it sets netErr
to point to that error, allowing access to its fields.
- Works with wrapped errors: Both functions work through entire error chains, not just the topmost error.
- Nil-safety: Unlike type assertions, these functions handle nil errors gracefully.
- Interface satisfaction:
errors.As
can find errors that implement an interface, not just concrete types.
Example demonstrating these benefits:
type CustomError interface {
CustomError() string
}
type MyError struct {
Msg string
}
func (e *MyError) Error() string {
return e.Msg
}
func (e *MyError) CustomError() string {
return "This is a custom error: " + e.Msg
}
func someOperation() error {
return fmt.Errorf("wrapped error: %w", &MyError{Msg: "something went wrong"})
}
func main() {
err := someOperation()
// Using errors.As with an interface
var customErr CustomError
if errors.As(err, &customErr) {
fmt.Println(customErr.CustomError())
}
// Safe with nil errors
var nilErr error
fmt.Println(errors.Is(nilErr, io.EOF)) // false, no panic
// Works through wrapped errors
fmt.Println(errors.Is(err, &MyError{})) // true
}
This expanded explanation and examples demonstrate how errors.Is
and errors.As
provide powerful and flexible error handling capabilities in Go, especially when dealing with error wrapping and custom error types.
Go's idiomatic way of returning errors along with results:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
Errors can have methods beyond Error()
for additional functionality:
type ValidationError struct {
Field string
Err error
}
func (v *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field %s: %v", v.Field, v.Err)
}
func (v *ValidationError) Unwrap() error {
return v.Err
}
For errors that don't need to carry additional information:
const ErrInvalidInput = Error("invalid input provided")
type Error string
func (e Error) Error() string {
return string(e)
}
The context
package can be used to carry deadlines, cancellation signals, and other request-scoped values:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := doSomethingSlowWithContext(ctx)
if err == context.DeadlineExceeded {
fmt.Println("Operation timed out")
}
Pointers are often used in error handling, especially with errors.As
:
var netErr *NetworkError
if errors.As(err, &netErr) {
fmt.Printf("Network error code: %d\n", netErr.Code)
}
fmt.Println(point.X, point.Y)
This allows for type safety, modification of the caller's variable, and flexibility in handling both pointer and non-pointer error types.
Unwrapping errors is the process of accessing the underlying error in a wrapped error chain:
type WrapperError struct {
Msg string
Err error
}
func (w *WrapperError) Error() string {
return fmt.Sprintf("%s: %v", w.Msg, w.Err)
}
func (w *WrapperError) Unwrap() error {
return w.Err
}
// Using errors.Unwrap
originalErr := errors.New("original error")
wrappedErr := fmt.Errorf("wrapped: %w", originalErr)
unwrappedErr := errors.Unwrap(wrappedErr)
fmt.Println(unwrappedErr == originalErr) // true
The Error()
method of an error is called implicitly in several situations:
err := errors.New("something went wrong")
fmt.Println(err) // Calls err.Error() implicitly
fmt.Printf("Error occurred: %v\n", err) // Calls err.Error()
fmt.Printf("Error message: %s\n", err) // Calls err.Error()
fmt.Printf("Quoted error: %q\n", err) // Calls err.Error() and quotes the result
For custom types implementing the error
interface, the same rules apply:
type MyError struct {
Code int
Message string
}
func (e MyError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
err := MyError{Code: 404, Message: "Not Found"}
fmt.Println(err) // Calls err.Error() implicitly
Note that the Error()
method is not called when using reflection-based formatting verbs like %#v
, and if a nil error is printed, it will output <nil>
rather than calling any method.
This comprehensive overview covers the essential aspects of error handling in Go, including creating and using errors, custom error types, panic and recover, error wrapping and unwrapping, and best practices for error handling in various scenarios.
Example references:
Reflection allows inspection of types at runtime:
t := reflect.TypeOf(someValue)
v := reflect.ValueOf(someValue)
if v.Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
fmt.Printf("Field: %s\n", v.Type().Field(i).Name)
}
}
Additional examples:
Go 1.18+ supports generics:
// Basic generic function
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
// Usage
PrintSlice([]int{1, 2, 3})
PrintSlice([]string{"a", "b", "c"})
// Generic function with multiple type parameters
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// Generic struct
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
// Using comparable constraint
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
// Custom type constraint with ~
type Integer interface {
~int | ~int32 | ~int64
}
type Float interface {
~float32 | ~float64
}
// Combined numeric constraint
type Number interface {
Integer | Float
}
// Works with custom integer types
type MyInt int
type MyFloat64 float64
func Sum[T Number](numbers []T) T {
var sum T
for _, n := range numbers {
sum += n
}
return sum
}
// Example usage with custom types
func Example() {
// Works with built-in types
ints := []int{1, 2, 3}
fmt.Println(Sum(ints)) // 6
// Works with custom types too
myInts := []MyInt{MyInt(1), MyInt(2), MyInt(3)}
fmt.Println(Sum(myInts)) // 6
myFloats := []MyFloat64{MyFloat64(1.1), MyFloat64(2.2)}
fmt.Println(Sum(myFloats)) // 3.3
}
// Generic map type
type Cache[K comparable, V any] struct {
data map[K]V
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
data: make(map[K]V),
}
}
Key points about generics in Go:
- Use square brackets
[T any]
for type parameters any
is an alias forinterface{}
comparable
is a built-in constraint that allows==
and!=
operations- Custom type constraints can be defined using interface types
- Multiple type parameters are supported
[K, V any]
- Generic types can be used in structs, interfaces, and methods
- The
~
operator in constraints means "any type with underlying type". For example:~int
matches bothint
and any type defined astype MyInt int
- Without
~
, the constraint would only match the exact type
Additional examples:
The context package is used for cancellation, deadlines, and request-scoped values:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
func (fw FileWriter) Write(data []byte) (int, error) { // Implementation return len(data), nil }
new
and make
are built-in functions for memory allocation, but they serve different purposes:
new
is used to allocate memory for a type and returns a pointer to a zero-initialized value of that type.make
is specifically used to create slices, maps, and channels, and returns an initialized (not zeroed) value of the specified type.
// new returns a pointer to a zero-initialized value
p := new(int)
fmt.Println(*p) // Output: 0
// make is used to create slices, maps, and channels
s := make([]int, 5, 10) // slice with length 5 and capacity 10
m := make(map[string]int) // empty map
ch := make(chan int, 5) // buffered channel with capacity 5
Understanding the difference between new
and make
is crucial for proper memory allocation and initialization in Go programs.
Go uses a sophisticated memory management system with automatic garbage collection. Understanding how memory is managed and collected is crucial for writing efficient Go programs.
- Fast allocation and deallocation
- Memory is automatically freed when function returns
- Size must be known at compile time
- Limited in size (typically a few MB)
- Thread-local (each goroutine has its own stack)
Examples of stack allocation:
func stackExample() {
// These will typically be allocated on the stack
x := 42 // Basic types
y := [3]int{1, 2, 3} // Small arrays
z := struct{ name string }{"John"} // Small structs
}
- Managed by garbage collector
- Flexible size
- Slower than stack allocation
- Shared across goroutines
- Used for values that outlive the function that creates them
Examples of heap allocation:
func heapExample() *int {
// These will typically be allocated on the heap
x := new(int) // Pointer types
y := make([]int, 3) // Slices
z := make(map[string]int) // Maps
return x // Returning address forces heap allocation
}
Go's compiler performs escape analysis to determine whether a value can be allocated on the stack or must be allocated on the heap.
Common scenarios that cause heap allocation:
// 1. Returning addresses of local variables
func createPointer() *int {
x := 42
return &x // x escapes to heap
}
// 2. Storing references in interfaces
func createInterface() interface{} {
x := 42
return x // x escapes to heap due to interface conversion
}
// 3. Slices or maps that might grow
func createSlice() []int {
return make([]int, 0, 10) // Usually heap allocated
}
// 4. Goroutine-accessed variables
func accessFromGoroutine() {
x := 42
go func() {
fmt.Println(x) // x escapes to heap
}()
}
You can see escape analysis details using:
go build -gcflags="-m"
Go uses a concurrent, tri-color mark-and-sweep collector:
-
Mark Phase
- White: Unvisited objects
- Grey: Visited but not scanned
- Black: Visited and scanned
// Objects are initially white root := &Object{} // Grey (visited) root.Next = &Object{} // White (unvisited) // After scanning root, it becomes black // After scanning root.Next, both are black
-
Write Barrier
- Ensures consistency during concurrent marking
- Activated during garbage collection
// Write barrier example (conceptual) if writeBarrier.enabled { writeBarrier(ptr, val) } else { *ptr = val }
-
Sweep Phase
- Reclaims memory from white objects
- Runs concurrently with application
Control garbage collection behavior:
// Force immediate garbage collection
runtime.GC()
// Set GOGC environment variable
// GOGC=50 means GC triggers when heap grows by 50%
os.Setenv("GOGC", "50")
// Get current memory statistics
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
fmt.Printf("Heap size: %d\n", stats.HeapAlloc)
- Object Pooling
var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
func processData() {
buf := pool.Get().([]byte)
defer pool.Put(buf)
// Use buf...
}
- Preallocate Slices
// Better
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i)
}
// Worse (causes multiple reallocations)
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
}
- Avoid Memory Leaks
// Potential memory leak
type Cache struct {
data map[string]*hugeStruct
}
// Better: Add cleanup method
func (c *Cache) Cleanup(maxAge time.Duration) {
for k, v := range c.data {
if time.Since(v.lastAccess) > maxAge {
delete(c.data, k)
}
}
}
Monitor GC performance:
// Enable GC logging
debug.SetGCPercent(100) // Default is 100
debug.SetMemoryLimit(1e9) // Set memory limit (Go 1.19+)
// Get GC statistics
var stats debug.GCStats
debug.ReadGCStats(&stats)
fmt.Printf("Last GC: %v\n", stats.LastGC)
fmt.Printf("Num GC: %d\n", stats.NumGC)
fmt.Printf("Pause Total: %v\n", stats.PauseTotal)
These concepts and examples provide a deeper understanding of Go's memory management and garbage collection system. Understanding these aspects helps in writing more efficient Go programs and troubleshooting memory-related issues.
Creating a basic HTTP server:
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
Reference: Basic HTTP Server
Using the http.Get
function to make HTTP requests:
resp, err := http.Get("https://example.com")
if err != nil {
// Handle error
}
defer resp.Body.Close()
// Process the response
cw := ConsoleWriter{} writeData(cw, []byte("Hello, World!"))
Reference: HTTP Client Example
Implementing custom io.Writer
for logging:
type logWriter struct{}
func (logWriter) Write(bs []byte) (int, error) {
fmt.Println(string(bs))
return len(bs), nil
}
io.Copy(logWriter{}, resp.Body)
Reference: Custom Writer Example
Using http.ServeMux
for routing and handling different HTTP methods:
mux := http.NewServeMux()
mux.HandleFunc("/", mainHandler)
mux.HandleFunc("POST /users", createUserHandler)
mux.HandleFunc("GET /users/{id}", getUserHandler)
http.ListenAndServe(":8080", mux)
Reference: Advanced HTTP Server
Encoding and decoding JSON in HTTP handlers:
// Decoding JSON from request body
var user User
json.NewDecoder(r.Body).Decode(&user)
// Encoding JSON to response
json.NewEncoder(w).Encode(user)
Reference: JSON Handling Example
Extracting path parameters from URLs:
id := r.PathValue("id")
intId, err := strconv.Atoi(id)
Reference: Path Parameters Example
Using the Gorilla Mux router for more flexible routing:
r := mux.NewRouter()
r.HandleFunc("/", homeHandler)
r.HandleFunc("/user/{id}", userHandler)
http.ListenAndServe(":8000", r)
Reference: Gorilla Mux Example
Accessing URL variables in handler functions:
vars := mux.Vars(r)
id := vars["id"]
Reference: Gorilla Mux Variables Example
These examples cover various aspects of web development in Go, including basic and advanced HTTP servers, HTTP clients, custom writers, JSON handling, URL routing, and using third-party routers like Gorilla Mux. Each topic includes a reference to the relevant Go file for more detailed implementation.
Gin is a high-performance HTTP web framework written in Go. It provides a martini-like API with much better performance and features.
go get -u github.com/gin-gonic/gin
import "github.com/gin-gonic/gin"
func main() {
// Create default gin router
r := gin.Default()
// Basic route
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello World",
})
})
// Run on port 8080
r.Run(":8080")
}
// URL Parameters
r.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(200, "Hello %s", name)
})
// Query Parameters
r.GET("/search", func(c *gin.Context) {
query := c.DefaultQuery("q", "default search")
c.String(200, "Search query: %s", query)
})
type User struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
r.POST("/user", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"message": "User created"})
})
// Custom middleware
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// Set example variable
c.Set("example", "12345")
// before request
c.Next()
// after request
latency := time.Since(t)
log.Printf("Latency: %v", latency)
}
}
// Using middleware
r.Use(Logger())
// API versioning
v1 := r.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
v2 := r.Group("/v2")
{
v2.POST("/login", loginEndpointV2)
v2.POST("/submit", submitEndpointV2)
v2.POST("/read", readEndpointV2)
}
r.POST("/upload", func(c *gin.Context) {
file, _ := c.FormFile("file")
// Save file
c.SaveUploadedFile(file, "uploaded/"+file.Filename)
c.String(200, "File %s uploaded!", file.Filename)
})
// Serve single file
r.StaticFile("/favicon.ico", "./resources/favicon.ico")
// Serve directory
r.Static("/assets", "./assets")
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn"`
}
func bookableDate(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
today := time.Now()
if date.Unix() > today.Unix() {
return true
}
}
return false
}
func main() {
r := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
// ... router setup
}
Additional examples:
The Gin framework provides a robust set of features for building web applications in Go, with excellent performance and a simple, expressive API. These examples cover the most common use cases, but Gin offers many more features for building complex web applications.
Middleware in Go is a powerful concept, especially in the context of HTTP servers. It allows you to process requests and responses before they reach your main handler or after they've been processed by your handler.
The basic structure of middleware in Go is a function that takes an http.Handler
and returns an http.Handler
:
func middlewareName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing logic
next.ServeHTTP(w, r)
// Post-processing logic
})
}
-
http.Handler
: An interface with a single method:type Handler interface { ServeHTTP(ResponseWriter, *Request) }
-
http.HandlerFunc
: A type that allows regular functions to be used as HTTP handlers:type HandlerFunc func(ResponseWriter, *Request)
-
next.ServeHTTP(w, r)
: Calls the next handler in the chain.
Here's an example of a simple logging middleware:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Received request: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Println("Finished processing request")
})
}
To use middleware in your Go HTTP server:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
wrappedMux := loggingMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", wrappedMux))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome Home!")
}
Middleware is an excellent place to use and modify the request's context. Here's an example of middleware that adds a request ID to the context:
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := generateRequestID()
ctx := context.WithValue(r.Context(), "requestID", requestID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
You can chain multiple middleware functions:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
wrappedMux := loggingMiddleware(requestIDMiddleware(mux))
log.Fatal(http.ListenAndServe(":8080", wrappedMux))
}
In this setup, the request flows through loggingMiddleware
, then requestIDMiddleware
, before reaching the appropriate handler.
MongoDB integration in Go is primarily done using the official MongoDB Go driver. Here's a comprehensive guide on working with MongoDB in Go applications:
First, install the MongoDB Go driver:
go get go.mongodb.org/mongo-driver/mongo
package main
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func connectDB() (*mongo.Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Connect to MongoDB
client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
if err != nil {
return nil, err
}
// Ping the database
err = client.Ping(ctx, nil)
if err != nil {
return nil, err
}
return client, nil
}
type User struct {
ID primitive.ObjectID `bson:"_id,omitempty"`
Name string `bson:"name"`
Email string `bson:"email"`
Age int `bson:"age"`
CreatedAt time.Time `bson:"created_at"`
}
// Insert One Document
func insertUser(client *mongo.Client, user User) (*mongo.InsertOneResult, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := collection.InsertOne(ctx, user)
if err != nil {
return nil, err
}
return result, nil
}
// Insert Many Documents
func insertManyUsers(client *mongo.Client, users []interface{}) (*mongo.InsertManyResult, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := collection.InsertMany(ctx, users)
if err != nil {
return nil, err
}
return result, nil
}
// Find One Document
func findUser(client *mongo.Client, filter bson.M) (*User, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var user User
err := collection.FindOne(ctx, filter).Decode(&user)
if err != nil {
return nil, err
}
return &user, nil
}
// Find Many Documents
func findUsers(client *mongo.Client, filter bson.M) ([]User, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, filter)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var users []User
if err = cursor.All(ctx, &users); err != nil {
return nil, err
}
return users, nil
}
// Update One Document
func updateUser(client *mongo.Client, filter bson.M, update bson.M) (*mongo.UpdateResult, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := collection.UpdateOne(ctx, filter, update)
if err != nil {
return nil, err
}
return result, nil
}
// Update Many Documents
func updateManyUsers(client *mongo.Client, filter bson.M, update bson.M) (*mongo.UpdateResult, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := collection.UpdateMany(ctx, filter, update)
if err != nil {
return nil, err
}
return result, nil
}
// Delete One Document
func deleteUser(client *mongo.Client, filter bson.M) (*mongo.DeleteResult, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := collection.DeleteOne(ctx, filter)
if err != nil {
return nil, err
}
return result, nil
}
// Delete Many Documents
func deleteManyUsers(client *mongo.Client, filter bson.M) (*mongo.DeleteResult, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := collection.DeleteMany(ctx, filter)
if err != nil {
return nil, err
}
return result, nil
}
func aggregateUsers(client *mongo.Client, pipeline mongo.Pipeline) ([]bson.M, error) {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := collection.Aggregate(ctx, pipeline)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var results []bson.M
if err = cursor.All(ctx, &results); err != nil {
return nil, err
}
return results, nil
}
func createIndex(client *mongo.Client, field string, unique bool) error {
collection := client.Database("testdb").Collection("users")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
model := mongo.IndexModel{
Keys: bson.D{{field, 1}},
Options: options.Index().SetUnique(unique),
}
_, err := collection.Indexes().CreateOne(ctx, model)
return err
}
func main() {
// Connect to MongoDB
client, err := connectDB()
if err != nil {
log.Fatal(err)
}
defer func() {
if err := client.Disconnect(context.Background()); err != nil {
log.Fatal(err)
}
}()
// Create a new user
user := User{
Name: "John Doe",
Email: "[email protected]",
Age: 30,
CreatedAt: time.Now(),
}
// Insert the user
result, err := insertUser(client, user)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted user ID: %v\n", result.InsertedID)
// Find a user
filter := bson.M{"email": "[email protected]"}
foundUser, err := findUser(client, filter)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Found user: %+v\n", foundUser)
}
-
Connection Management:
- Always use context for timeouts
- Properly close connections using defer
- Implement connection pooling for production environments
-
Error Handling:
- Check for specific MongoDB errors using
mongo.IsDuplicateKeyError(err)
- Implement proper retry mechanisms for transient failures
- Use meaningful error wrapping for better debugging
- Check for specific MongoDB errors using
-
Performance Optimization:
- Use appropriate indexes
- Implement pagination for large result sets
- Use projection to limit returned fields
- Consider using bulk operations for multiple documents
-
Security:
- Use connection strings with authentication
- Implement proper access control
- Use TLS for production environments
This MongoDB integration guide provides a solid foundation for working with MongoDB in Go applications, covering basic to advanced operations with proper error handling and best practices.
Writing and running tests:
// math.go
func Add(a, b int) int {
return a + b
}
// math_test.go
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
Basic test commands:
# Run all tests in current package
go test
# Run tests with verbose output
go test -v
# Run specific test
go test -run TestAdd
# Run tests matching a pattern
go test -run "Test[A-Z].*"
# Run tests in all subdirectories
go test ./...
Writing benchmarks:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
Running benchmarks:
# Run all benchmarks
go test -bench=.
# Run specific benchmark
go test -bench=BenchmarkAdd
# Run benchmarks with custom iterations
go test -bench=. -benchtime=5s
# Show memory allocations
go test -bench=. -benchmem
- CPU Profiling:
# Generate CPU profile
go test -cpuprofile=cpu.prof -bench=.
# Analyze with pprof
go tool pprof cpu.prof
# Generate web visualization (requires Graphviz)
go tool pprof -web cpu.prof
- Memory Profiling:
# Generate memory profile
go test -memprofile=mem.prof -bench=.
# Analyze with pprof
go tool pprof mem.prof
- Trace Profiling:
# Generate trace
go test -trace=trace.out -bench=.
# View trace
go tool trace trace.out
Common pprof interactive commands:
top # Show top consumers
web # Open graph visualization
list <func> # Show line-by-line breakdown
peek # Show parent-child relationships
quit # Exit pprof
# Run tests with coverage
go test -cover
# Generate coverage profile
go test -coverprofile=coverage.out
# View coverage in browser
go tool cover -html=coverage.out
# Generate coverage report
go tool cover -func=coverage.out
# Run tests with race detector
go test -race
# Run specific test with race detector
go test -race -run TestAdd
The testify package provides enhanced testing capabilities with assertions, mocking, and suite support.
Installation:
go get github.com/stretchr/testify
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestExample(t *testing.T) {
// Simple assertions
assert.Equal(t, 123, Calculate())
assert.NotEqual(t, 456, Calculate())
assert.True(t, IsValid())
// Collection assertions
assert.Contains(t, []string{"hello", "world"}, "hello")
assert.Len(t, []int{1,2,3}, 3)
// Error assertions
err := SomeFunction()
assert.NoError(t, err)
}
import (
"github.com/stretchr/testify/require"
)
func TestWithRequire(t *testing.T) {
result := Setup()
require.NotNil(t, result) // Test stops here if result is nil
require.Equal(t, expected, result.Value)
}
type MockDB struct {
mock.Mock
}
func (m *MockDB) Get(id string) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
}
func TestWithMock(t *testing.T) {
mockDB := new(MockDB)
mockDB.On("Get", "123").Return("data", nil)
result, err := mockDB.Get("123")
mockDB.AssertExpectations(t)
}
type ExampleTestSuite struct {
suite.Suite
db *Database
}
func (suite *ExampleTestSuite) SetupTest() {
suite.db = NewDatabase()
}
func (suite *ExampleTestSuite) TestExample() {
result := suite.db.Query()
suite.Equal(expected, result)
}
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
Additional examples:
The go
command is the primary tool for managing Go source code:
go run
: Compiles and runs a programgo build
: Compiles packages and dependenciesgo test
: Runs testsgo get
: Downloads and installs packages and dependenciesgo mod init
: Initializes a new modulego mod tidy
: Adds missing and removes unused modules
go fmt
: Formats Go source codegofmt -s
: Simplifies code in addition to formattinggo doc
: Shows documentation for a package or symbolgodoc
: Starts a local documentation server
Additional examples:
This example shows how to read the contents of a file using the ioutil.ReadFile
function. Note that in more recent versions of Go, it's recommended to use os.ReadFile
instead.
- Use packages to organize code
- Follow the standard project layout
- Use meaningful names for packages, types, and functions
- Keep package names short and lowercase
- Use profiling tools (pprof) to identify bottlenecks
- Optimize algorithms and data structures
- Use sync.Pool for frequently allocated and deallocated objects
- Consider using sync.Map for concurrent map access instead of mutex-protected maps
- Use channels for communication, mutexes for state
- Implement the fan-out, fan-in pattern for parallel processing
- Use context for cancellation and timeouts
- Implement worker pools for managing concurrent tasks
This comprehensive guide covers all major aspects of Go programming, from basic syntax to advanced concepts and best practices. It includes detailed explanations, code examples, and practical tips to help developers write efficient and idiomatic Go code.
Go provides robust support for file operations through various packages like os
, io
, io/ioutil
, and bufio
. Here's a comprehensive guide to file operations:
// Method 1: os.ReadFile (Simplest)
content, err := os.ReadFile("small.txt") // Returns []byte
if err != nil {
log.Fatal(err)
}
// Method 2: io.ReadAll
file, err := os.Open("small.txt") // Returns *os.File
if err != nil {
log.Fatal(err)
}
defer file.Close()
content, err := io.ReadAll(file) // Returns []byte
// Method 3: bufio.Scanner (line by line)
file, _ := os.Open("small.txt") // Returns *os.File
defer file.Close()
scanner := bufio.NewScanner(file) // Returns *bufio.Scanner
for scanner.Scan() {
line := scanner.Text() // Returns string
// Process line
}
// Method 1: bufio.Scanner (Memory efficient)
file, _ := os.Open("large.txt") // Returns *os.File
defer file.Close()
scanner := bufio.NewScanner(file) // Returns *bufio.Scanner
for scanner.Scan() {
line := scanner.Text() // Returns string
// Process line
}
// Method 2: Buffered reading
file, _ := os.Open("large.txt") // Returns *os.File
defer file.Close()
buffer := make([]byte, 1024) // Returns []byte
for {
n, err := file.Read(buffer) // Returns (int, error)
if err == io.EOF {
break
}
// Process buffer[:n]
}
// Method 1: Write entire file
data := []byte("Hello, World!")
err := os.WriteFile("file.txt", data, 0644)
// Method 2: Create and write
file, err := os.Create("file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
file.Write([]byte("Hello"))
file, err := os.Create("large.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
writer := bufio.NewWriter(file)
for i := 0; i < 1000; i++ {
writer.WriteString("Line of text\n")
}
writer.Flush()
// Adds text to the end of the file
file, err := os.OpenFile("example.txt", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
_, err = file.WriteString("\nAppended text")
if err != nil {
log.Fatal(err)
}
// Replaces all content with new content
err := os.WriteFile("example.txt", []byte("New content"), 0644)
if err != nil {
log.Fatal(err)
}
// Read the file
content, err := os.ReadFile("example.txt") // Returns []byte
if err != nil {
log.Fatal(err)
}
// Replace text and write back
newContent := strings.Replace(string(content), "old text", "new text", -1)
err = os.WriteFile("example.txt", []byte(newContent), 0644)
if err != nil {
log.Fatal(err)
}
// Create a temporary file
tempFile, err := os.CreateTemp("", "temp-*.txt")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tempFile.Name())
// Open original file
original, err := os.Open("example.txt")
if err != nil {
log.Fatal(err)
}
defer original.Close()
// Process line by line
scanner := bufio.NewScanner(original)
writer := bufio.NewWriter(tempFile)
for scanner.Scan() {
line := scanner.Text()
// Example: Replace "old" with "new" in each line
updatedLine := strings.Replace(line, "old", "new", -1)
writer.WriteString(updatedLine + "\n")
}
writer.Flush()
// Replace original with updated file
os.Rename(tempFile.Name(), "example.txt")
// Reading: Direct Unmarshal
content, _ := os.ReadFile("small.json")
var data interface{}
json.Unmarshal(content, &data)
// Writing: Marshal and Write
data := map[string]string{"name": "John"}
jsonData, _ := json.Marshal(data)
os.WriteFile("output.json", jsonData, 0644)
// Reading: Streaming Decoder
file, _ := os.Open("large.json")
defer file.Close()
decoder := json.NewDecoder(file)
for decoder.More() {
var item interface{}
err := decoder.Decode(&item)
// Process item
}
// Writing: Streaming Encoder
file, _ := os.Create("large.json")
defer file.Close()
encoder := json.NewEncoder(file)
for item := range items {
encoder.Encode(item)
}
// Check if file exists
_, err := os.Stat("file.txt")
if os.IsNotExist(err) {
fmt.Println("File does not exist")
}
// Rename file
err := os.Rename("old.txt", "new.txt")
if err != nil {
log.Fatal(err)
}
// Delete file
err := os.Remove("file.txt")
if err != nil {
log.Fatal(err)
}
- Always close files using
defer file.Close()
- Check for errors after each operation
- Consider memory constraints when choosing a method
- Use buffered operations for large files
- Use appropriate buffer sizes for your use case
- Handle errors appropriately
Reference: File Operations
These examples cover the basic file operations in Go. Remember to handle errors appropriately and close files when you're done with them. The defer
keyword is particularly useful for ensuring files are closed properly.