DEV Community

loading...
Cover image for Getting started with Go pointers

Getting started with Go pointers

jldec profile image jldec Originally published at jldec.me Updated on ・3 min read

Golang

This is part 2 of my experience as a new user of Go, focusing on the quirks and gotchas of pointers. For installation, testing, and packages, see Getting started with Go.

If you'd like to follow along, and try out out the code in this article, all you need is the Go playground to run the examples.

Pointers

The shortscale package which I covered last time, uses a string Builder. Here is the example from the Builder docs.

package main

import (
    "fmt"
    "strings"
)

func main() {
    var b strings.Builder
    for i := 3; i >= 1; i-- {
        fmt.Fprintf(&b, "%d...", i)
    }
    b.WriteString("ignition")
    fmt.Println(b.String())
}
Enter fullscreen mode Exit fullscreen mode

Notice that var b is an instance of the Builder. When you run the code, it will output: 3...2...1...ignition.

Pointer receiver methods and interfaces

The first argument to fmt.Fprintf is &b, a pointer to b. This is necessary, because fmt.Fprintf expects an io.Writer interface.

The Builder.Write method matches the io.Writer interface. Notice the pointer syntax in the method receiver after the func keyword.

func (b *Builder) Write(p []byte) (int, error)
Enter fullscreen mode Exit fullscreen mode

I was tempted to replace Fprintf(&b, ...) with Fprintf(b, ...), to make it more consistent with the b.WriteString() and b.String() further down, but doing this causes the compiler to complain:

"cannot use b (type strings.Builder) as type io.Writer in argument to fmt.Fprintf: strings.Builder does not implement io.Writer (Write method has pointer receiver)"

Value vs. pointer function arguments

What if, instead of depending on the Writer interface, we called our own write() function?

func main() {
    var b strings.Builder
    for i := 3; i >= 1; i-- {
        write(b, fmt.Sprintf("%d...", i))
    }
    b.WriteString("ignition")
    fmt.Println(b.String())
}

func write(b strings.Builder, s string) {
    b.WriteString(s)
}
Enter fullscreen mode Exit fullscreen mode

Running the code above in the example sandbox outputs just the word ignition.

The 3 calls to write(b) do not modify the builder declared at the top.

This makes sense, because passing a struct to a function copies the struct value.

To fix this, we have to use a pointer to pass the struct by reference, and we have to invoke the function with write(&b, ...). This works, but it doesn't make the code any more consistent.

func main() {
    var b strings.Builder
    for i := 3; i >= 1; i-- {
        write(&b, fmt.Sprintf("%d...", i))
    }
    b.WriteString("ignition")
    fmt.Println(b.String())
}

func write(b *strings.Builder, s string) {
    b.WriteString(s)
}
Enter fullscreen mode Exit fullscreen mode

Why do the method calls work?

Why are we allowed to use b instead of &b in front of b.WriteString and b.String? This is explained in the tour as well.

"...even though v is a value and not a pointer, the method with the pointer receiver is called automatically. That is, as a convenience, Go interprets the statement v.Scale(5) as (&v).Scale(5) since the Scale method has a pointer receiver."

Start with a pointer

If all this mixing of values and pointers feels inconsistent, why not start with a pointer from the beginning?

The following code will compile just fine, but can you tell what's wrong with it?

func main() {
    var b *strings.Builder
    for i := 3; i >= 1; i-- {
        fmt.Fprintf(b, "%d...", i)
    }
    b.WriteString("ignition")
    fmt.Println(b.String())
}
Enter fullscreen mode Exit fullscreen mode

The declaration above results in a nil pointer panic at run time, because b is uninitialized.

panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4991c7]

Create the Builder with new()

Here is one way to initialize a pointer so that it references a new Builder.

func main() {
    b := new(strings.Builder)
    for i := 3; i >= 1; i-- {
        fmt.Fprintf(b, "%d...", i)
    }
    b.WriteString("ignition")
    fmt.Println(b.String())
}
Enter fullscreen mode Exit fullscreen mode

new(strings.Builder) returns a pointer to a freshly allocated Builder, which we can use for both functions and pointer receiver methods. This is the pattern which I now use in shortscale-go.

An alternative, which does the same thing, is the more explicit struct literal shown below.

func main() {
    b := &strings.Builder{}
    ...
Enter fullscreen mode Exit fullscreen mode

There's no avoiding pointers in Go.
Learn the quirks and the gotchas today.
✨ Keep learning! ✨

Discussion (0)

pic
Editor guide