Variables store values at unique locations in computer memory, and each location has a corresponding memory address. A pointer is a special type of variable that stores the memory address of another variable.
Basics of Pointers
petName := "Misty"
pointer := &petName

fmt.Println(petName)  // Output: Misty
fmt.Println(pointer)  // Output: (memory address, e.g., 0xc000020070)
The Ampersand Operator (&): Used to get the memory address of a variable
The Asterisk Operator (*): Used to access the value stored at the memory address a pointer references
Type Safety and Pointer Assignments
Pointers in Go are type-safe. This means you cannot assign the memory address of one type to a pointer of another type.
var pointer *int
petName := "Misty"
// pointer = &petName // Error: cannot use &petName (type *string) as type *int
Declaring Pointer Variables
To declare a pointer variable, you place the asterisk (*) before the type:
var pointer *string
name := "Misty"
pointer = &name
fmt.Println(name)     // Output: Misty
fmt.Println(*pointer) // Output: Misty
Working with nil and Pointers
Pointers can be compared to nil. A nil pointer indicates that it doesn’t reference any memory location. Types like slices, maps, interfaces, and channels, which internally use pointers, can also be compared to nil.
var ptr *string
fmt.Println(ptr == nil) // Output: true
⚠️ Attempting to dereference a nil pointer causes a runtime panic:
var ptr *int
fmt.Println(*ptr) // panic: runtime error: invalid memory address or nil pointer dereference
Passing by Value vs. Reference
In Go, variables are passed by value. This means that when you pass a variable to a function, a copy of its value is created. Modifying the copy doesn’t affect the original variable.
Example: Passing by Value
func counter(value int) {
  value++
}

func main() {
  value := 0
  counter(value)
  fmt.Println(value) // Output: 0 (unchanged)
}
To modify the original variable, you can pass a pointer instead:
func counterPointer(value *int) {
  *value++
}

func main() {
  value := 0
  counterPointer(&value)
  fmt.Println(value) // Output: 1 (modified)
}
Pointers with Structs
The same “pass by value” principle applies to structs. Here’s an example:
type Point struct {
  X int
  Y int
}

func modifyPoint(p Point) {
  p.X++
  p.Y++
}

func main() {
  pt := Point{X: 1, Y: 2}
  modifyPoint(pt)
  fmt.Println(pt) // Output: {1 2} - original struct remains unchanged
}
To modify the original struct, pass a pointer:
func modifyPointPointer(p *Point) {
  p.X++
  p.Y++
}

func main() {
  pt := Point{X: 1, Y: 2}
  modifyPointPointer(&pt)
  fmt.Println(pt) // Output: {2 3}
}
Memory Management: Stack vs. Heap
Variables in Go are typically allocated in stack memory, which is efficient but limited in scope. When a function returns a pointer to a local variable, Go automatically allocates the variable on the heap, ensuring it remains accessible.
func getRandomName() *string {
  name := "Olivia"
  return &name
}

func main() {
  olivia := getRandomName()
  fmt.Println(*olivia) // Output: Olivia
}
Heap allocation allows variables to persist beyond the scope of the function that created them. However, heap memory can become fragmented and require garbage collection, which Go handles automatically.
The Role of Go’s Garbage Collector
Go’s garbage collector tracks pointers and the values they reference, ensuring that memory is reclaimed when it’s no longer needed. This eliminates the risk of dangling pointers and makes it safe to return pointers from functions.
Practical Use Cases for Pointers
Pointers are particularly useful in scenarios like:
• Avoiding expensive copies of large structs
• Implementing linked data structures like linked lists or trees
• Sharing mutable state across functions
Key takeaways
• Use the & operator to get a variable’s memory address and * to dereference pointers
• Pointers are type-safe and cannot reference mismatched types
• Passing variables by pointer allows you to modify the original value
• Go’s garbage collector ensures safe and efficient memory use