Skip to main content

Pointers and Structs

Go has pointers, but no pointer arithmetic. Structs are the way to create complex data types.

Pointers

A pointer holds the memory address of a value. Think of it as a reference to where data lives in memory, rather than the data itself. Key Syntax:
  • The type *T is a pointer to a T value. Its zero value is nil.
  • The & operator generates a pointer to its operand (“address of”).
  • The * operator denotes the pointer’s underlying value (“dereferencing”).
func main() {
    i, j := 42, 2701

    p := &i         // p is a pointer to i (p stores i's memory address)
    fmt.Println(*p) // read i through the pointer (prints 42)
    *p = 21         // set i through the pointer (i is now 21)
    fmt.Println(i)  // see the new value of i (prints 21)

    p = &j         // point to j instead
    *p = *p / 37   // divide j through the pointer
    fmt.Println(j) // see the new value of j (prints 73)
}
Why Use Pointers?
  1. Modify Function Arguments: Pass a pointer to allow a function to modify the original value.
  2. Avoid Copying Large Structs: Passing a pointer is more efficient than copying a large struct.
  3. Share Data: Multiple parts of your program can reference the same data.
No Pointer Arithmetic: Unlike C, Go has no pointer arithmetic. You cannot do p++ to move to the next memory address. This prevents common memory bugs.

Structs

A struct is a collection of fields.
type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    v.X = 4
    fmt.Println(v.X)
}

Pointers to Structs

Struct fields can be accessed through a struct pointer.
v := Vertex{1, 2}
p := &v
p.X = 1e9 // Equivalent to (*p).X
To access the field X of a struct when we have the struct pointer p we could write (*p).X. However, that notation is cumbersome, so the language permits us instead to write just p.X, without the explicit dereference.

Struct Literals

You can list just a subset of fields by using the Name: syntax. (And the order of named fields is irrelevant.)
var (
    v1 = Vertex{1, 2}  // has type Vertex
    v2 = Vertex{X: 1}  // Y:0 is implicit
    v3 = Vertex{}      // X:0 and Y:0
    p  = &Vertex{1, 2} // has type *Vertex
)

Methods

Go does not have classes. However, you can define methods on types. A method is a function with a special receiver argument.
type Vertex struct {
    X, Y float64
}

// Abs method has a receiver of type Vertex named v
func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

Pointer Receivers

You can declare methods with pointer receivers. This means the receiver type has the literal syntax *T for some type T. (Also, T cannot itself be a pointer such as *int.) Methods with pointer receivers can modify the value to which the receiver points (as Scale does here). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.
func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v.Abs()) // Output: 50
}

Choosing a Value or Pointer Receiver

There are two reasons to use a pointer receiver:
  1. Modify the receiver: The method needs to modify the value that the receiver points to.
  2. Performance: To avoid copying the value on each method call. This is more efficient if the struct is large.
Rule of Thumb: If in doubt, use a pointer receiver.