The * , & Operator and Pointers in Golang

What is a Pointer?

A pointer is a type of variable that stores the memory address of another variable. Rather than containing an actual data value, a pointer contains the specific location in memory where the data value is stored.
This property enables efficient manipulation of data and can be especially valuable for tasks that involve dynamic memory allocation, data structures such as linked lists, and performance optimization by avoiding the need to copy large data structures.

Why Use Pointers ?

Pointers are used for several reasons:

  1. Efficiency: Passing large structures (such as arrays or structs) by pointers instead of copying the entire data can improve performance.
  2. Dynamic Memory Allocation: Pointers allow for the creation and management of dynamic data structures like linked lists, trees, and graphs.
  3. Memory Management: Pointers enable low-level memory management, which is crucial for system-level programming.
  4. Flexibility: Pointers provide more flexibility in functions, allowing them to modify variables passed to them.

Basic Pointer Operations

  1. Address-of Operator (&): This operator is used to get the memory address of a variable.
  2. Dereference Operator (*): This operator is used to access or modify the value stored at the memory address that the pointer points to.

How Pointers Work in Memory

  1. When you create a variable, the system allocates a block of memory to store its value. The variable name refers to this memory location. For instance, if you create an integer variable, x, with a value of 42, the system will allocate memory to store 42 and x will refer to that location.

    A pointer variable p stores the memory address of variable x, not the value 42 itself. If you have a pointer variable p will hold the memory address of x, and dereferencing p (using *p) will give you the value stored at that address, which is 42.

Consider the following simple example in Golang:

func main() {
   var x int = 42  // Create an integer variable x with a value of 42
   var p *int = &x // Create a pointer variable p that stores the address of x
   fmt.Println("Value of x:", x)    // Output: 42
   fmt.Println("Address of x:", &x) // Output: address of x: 0xc0000120b0 
   fmt.Println("Value of p:", p)    // Output: value of p: 0xc0000120b0 (same as the address of x)
   fmt.Println("Value at p:", *p)   // Output: 42 (value stored at the memory address p points to)
}

In this example:

  • x is an integer variable with the value 42.
  • p is a pointer to an integer, and it is assigned the address of x using the & operator.
  • &x provides the address of x.
  • *p dereferences the pointer, giving the value stored at the memory address p points to (which is 42).

Understanding pointers is essential for efficient memory management and effective programming, especially in languages like Golang that support low-level memory operations. Pointers provide a powerful tool for optimising performance and enabling advanced data structures and algorithms.

Basic Pointer Operations

1. Address-of Operator (&)

The Address-of Operator (&) retrieves the memory address of a variable, which can be stored in a pointer variable.

Usage:

  • Getting the Address: Use “&” before a variable’s name to obtain its address.
  • Assignment: The address obtained using the “&” operator can be assigned to a pointer variable.

Example:

func main() {
   var num int = 42    // Declare an integer variable num with a value of 42
   var ptr *int = &num // Declare a pointer variable ptr and assign it the address of num
   fmt.Println("Value of num:", num)    // Outputs: 42
   fmt.Println("Address of num:", &num) // Outputs: address of num: 0xc0000120b0
   fmt.Println("Value of ptr:", ptr)     // Outputs: value of ptr : 0xc0000120b0 (same as address of num)
}

For the complete program please visit our GitHub repository

Explaination :

  • var num int = 42 declares an integer variable num with a value of 42.
  • var ptr *int = &num declares a pointer variable ptr of type *int and assigns it the address of num using the & operator.

 

In this example:

 

  • &num returns the address of the variable num.
  • This address is then stored in the pointer variable ptr.

2. Dereference Operator (*)

The Dereference Operator (*) in Go is used to interact with the value stored at the memory address that a pointer variable holds. By dereferencing a pointer, you can access or modify the value at the address the pointer points to. Here’s a detailed breakdown:

Usage of Dereference Operator

  1. Dereferencing: Applying the * operator to a pointer variable retrieves or modifies the value at the address the pointer references.

Pointer Declaration: When declaring a pointer, the * operator indicates the type the pointer will refer to (e.g., *int for a pointer to an integer).

Example :

Let’s take a look at a detailed example:

func main() {
   var a int = 10  // Declare an integer variable a with value 10
   var p *int = &a // Declare a pointer variable p and assign it the address of a
   // Dereference p to access the value at the address it points to
   fmt.Println("Value at pointer p:", *p) // Outputs: 10 (value stored at the address p points to)
   // Modify the value at the address p points to
   *p = 20
   fmt.Println("New value of a:", a) // Outputs: 20 (a has been updated through pointer p)
}

For the complete program please visit our GitHub repository

Explanation:

  • Dereferencing (*p):
    • *p retrieves the value stored at the memory address pointed to by p. Initially, *p returns 10, which is the value of the variable a since p points to a.
  • Modifying via Dereferencing (*p = 20):
    • By assigning 20 to *p, you are directly modifying the value at the address p points to. Since p points to a, this operation updates the value of a to 20.

Understanding dereferencing is crucial for manipulating data through pointers and for implementing complex data structures and algorithms that require direct memory access.

 

Following image can help you to understand the pointer concept : 

Note :

  • pointer ptr stores the memory address value of variable p
  • memory address value of variable p is equal to value of pointer ptr to access the corresponding value of  

Pointer to struct

type EmployeeContact struct {
   Name  string
   Phone string
}
func main() {
   // Create an instance of EmployeeContact
   employee1 := EmployeeContact{"Scalent_Employee_1", "1234567890"}
   // Print the original struct instance
   fmt.Println("Original employee1:", employee1) // Outputs: {Scalent_Employee_1 1234567890}
   // Create a pointer to the struct instance
   employee1Pointer := &employee1
   // Modify the struct fields using the pointer
   employee1Pointer.Phone = "0987654321"
   // Print the modified struct instance
   fmt.Println("Modified employee1:", employee1) // Outputs: {Scalent_Employee_1 0987654321}
}

For the complete program please visit our GitHub repository

In this example:

  1. Pointer Creation and Usage:
    • employee1Pointer is created as a pointer to employee1 using the & operator. This operator retrieves the memory address of employee1, allowing employee1Pointer to reference it.
    • The pointer can then be used to access and modify the fields of employee1 directly.
  2. Modifying Data Through Pointers:
    • The line employee1Pointer.Phone = “0987654321” changes the Phone field of employee1 through the pointer. Since employee1Pointer stores the memory address of  employee1, it can directly access and modify employee1‘s values. Therefore, any changes made through the pointer are reflected directly in employee1.

Pointer receiver in Methods

In Go, methods can be associated with either value receivers or pointer receivers. Understanding the difference between these two types of receivers is crucial for writing effective and idiomatic Go code.

Value Receivers vs. Pointer Receivers

  1. Value Receivers:
    • When a method has a value receiver, the method operates on a copy of the struct.
    • Modifications to the struct within the method do not affect the original struct instance.
  2. Pointer Receivers:
    • When a method has a pointer receiver, the method operates on the original struct, not a copy.
    • Modifications to the struct within the method will affect the original struct instance.

Why Use Pointer Receivers?

Pointer receivers are used when:

  • Modifying the Struct: If you need to modify the fields of the struct within the method, use a pointer receiver.
  • Efficiency: For large structs, passing by pointer avoids copying the entire struct, which can be more efficient

Example Code

Here’s an example to illustrate the use of pointer receivers:

package main
import "fmt"
// Define a struct type
type Rectangle struct {
   Width, Height float64
}
// Method with a value receiver
func (r Rectangle) Area() float64 {
   return r.Width * r.Height
}
// Method with a pointer receiver
func (r *Rectangle) Scale(factor float64) {
   r.Width *= factor
   r.Height *= factor
}
func main() {
   // Create an instance of Rectangle
   rect := Rectangle{Width: 5, Height: 10}
   // Print the original area
   fmt.Println("Original Area:", rect.Area()) // Outputs: Original Area: 50
   // Scale the rectangle
   rect.Scale(2)
   // Print the new area after scaling
   fmt.Println("New Area:", rect.Area()) // Outputs: New Area: 200
}

For the complete program please visit our GitHub repository

Explanation:

  1. Struct Definition:
    • Rectangle struct has fields Width and Height.
  2. Value Receiver Method (Area):
    • Area method calculates the area based on the Width and Height.
    • It has a value receiver (r Rectangle), meaning it works with a copy of the struct.
  3. Pointer Receiver Method (Scale):
    • Scale method scales the dimensions of the rectangle by a given factor.
    • It has a pointer receiver (r *Rectangle), meaning it operates directly on the original struct instance.
  4. Modifications:
    • In main, rect.Scale(2) changes the dimensions of rect because Scale modifies the original struct. The Area method reflects these changes because it uses the updated dimensions.

Value Receiver: Works with a copy of the struct. Use it when you don’t need to modify the struct or when the struct is small and copying is inexpensive.

Pointer Receiver: Works with the original struct. Use it when you need to modify the struct or when the struct is large and you want to avoid copying.

By using pointer receivers, you can modify the struct in place and improve performance, especially with large structs.

Pointer in function

Using pointers in Golang functions can serve various purposes, including modifying the original value passed to the function, preventing unnecessary copying of large data structures, and optimizing performance. This is a detailed explanation of how pointers work in functions:

What does a Pointer in a Function represent?

A pointer in a function is a variable that stores the memory address of another variable. When you pass a pointer to a function, you’re actually passing the address of the variable, not a duplicate of its value. This allows the function to directly alter the original variable.

  1. Modifying Original Data:
    • When you need a function to modify a variable’s value and have that modification affect the variable outside the function, you should use a pointer.
    • Since functions receive duplicates of variables without pointers, any modifications made within the function will not impact the original variable.
  2. Efficiency with Large Data Structures:
    • Copying and passing large data structures by value can be inefficient. This inefficiency can be avoided by passing a pointer to the data structure.
package main
import "fmt"
// Define a struct type
type Rectangle struct {
   Width, Height float64
}
// Function to resize a rectangle by modifying its fields using a pointer
func resizeRectangle(r *Rectangle, width, height float64) {
   r.Width = width
   r.Height = height
}
func main() {
   rect := Rectangle{Width: 5, Height: 10}
   fmt.Println("Before resizing:", rect) // Outputs: Before resizing: {5 10}
   // Pass the address of rect to the function
   resizeRectangle(&rect, 15, 20)
   fmt.Println("After resizing:", rect) // Outputs: After resizing: {15 20}
}

For the complete program, Please visit our GitHub repository

Explanation:

  1. Struct Definition (Rectangle):
    • Rectangle has Width and Height fields.
  2. Function Definition (resizeRectangle):
    • resizeRectangle takes a pointer to a Rectangle (*Rectangle) and modifies its fields.
  3. Passing the Pointer (&rect):
    • In main, &rect passes the address of rect to resizeRectangle.
    • The function updates rect directly.

By using pointers, you can write more efficient and powerful Go code, particularly when dealing with complex data types and large-scale applications.

 

Here are some situations where you should avoid using pointers in Go:

Key Points

    1. Small Structs

    For small structs, the cost of copying is negligible, and using pointers can add unnecessary complexity.

Example:

package main
import "fmt"
type Point struct {
   X, Y int
}
func PrintPoint(p Point) {
   fmt.Println(p.X, p.Y)
}
func main() {
   p := Point{X: 1, Y: 2}
   PrintPoint(p) // No need to pass a pointer here.
}
  1. Primitive Types

Primitive types (like int, float, and string) are inexpensive to copy, so passing by value is usually preferred.

package main
import "fmt"
func Increment(x int) int {
  return x + 1
}
func main() {
  x := 5
  x = Increment(x) // No need to pass a pointer.
  fmt.Println(x)
}

 3 Slices and Maps

Slices and maps are reference types, meaning their underlying data is not copied when passed by value. Therefore, there is generally no need to use pointers to them unless you need to change the reference itself.

package main
import "fmt"
func ModifySlice(s []int) {
   s[0] = 10
}
func main() {
   slice := []int{1, 2, 3}
   ModifySlice(slice) // No need to pass a pointer.
   fmt.Println(slice)
} 
// output : [10 2 3]

Let us understand one more example on the slice

package main
import "fmt"
func main() {
   numbers := []int{10, 20, 30, 40, 50}
   numbersPtr := &numbers
   for i := 0; i < len(*numbersPtr); i++ {
       fmt.Printf("Index: %d, Value: %d\n", i, (*numbersPtr)[i])
       if i == 2 {
           *numbersPtr = append(*numbersPtr, 60, 70, 80)
       }
   }
}

Modification:

When i equals 2, the slice numbers is updated with new values, becoming [10, 20, 30, 40, 50, 60, 70, 80] due to the append operation.

Effect on Iteration:

  • Initially, len(*numbersPtr) is evaluated as 5.
  • After appending, the length increases to 8.
  • As a result, the loop proceeds beyond the initial length, iterating from i = 3 through i = 7, reflecting the updated length of the slice.

Unexpected behavior in the loop was observed: The loop’s termination condition altered due to the evaluation of len(*numbersPtr) at the beginning of each iteration, causing the loop to run for a longer duration than originally anticipated.


Potential errors related to indexing: If the slice had been modified in a way that decreased its length, accessing indices that no longer existed would lead to a runtime panic.

  1. Concurrency

For sharing data between goroutines, use channels or other synchronization methods instead of raw pointers to avoid race conditions.

  1. Simplifying Code

Avoiding pointers can make code more readable and easier to understand, especially for beginners.

Summary

  • Use pointers when you need to modify the original data, avoid copying large data structures, or manage memory explicitly.
  • Avoid pointers when dealing with small structs, primitive types, immutable data, slices, and maps (unless modifying the reference), to simplify code, and prevent memory leaks.

By carefully considering these guidelines, you can write more efficient, readable, and maintainable Go code.