Next: Getting started with Go
April 18, 2021

Getting started with Go pointers

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())
}

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.

type Writer interface {
	Write(p []byte) (n int, err error)
}

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)

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)
}

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)
}

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())
}

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())
}

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{}
	...

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

To leave a comment
please visit dev.to/jldec

(c) Jürgen Leschner