Golang Use a pointer receiver to define a function for a struct

eye-catch Golang

Do you recognize that there are two ways to define a method for a struct?

func (m StructName) FuncName(){
    // do something
}

func (m *StructName) FuncName(){
    // do something
}

The difference between the two methods is whether it has a pointer. It’s important to know the difference. Let’s check it in this post.

Sponsored links

How to change the original variable’s value

Let’s consider this case first. If we have a Slice/Array and want to do the same procedure to add elements, it should be written in a function not to write the same code twice. Let’s define the two functions here.

func addDataToArrayWithPointer(list *[]int) {
    if list != nil {
        *list = append(*list, 1, 2, 3)
    }
}

func addDataToArray(list []int) {
    list = append(list, 1, 2, 3)
}

Only the first function changes the original value.

dataList := []int{99}
addDataToArray(dataList)
fmt.Println(dataList) // [99]
addDataToArrayWithPointer(&dataList)
fmt.Println(dataList) // 99 1 2 3

The difference is whether the parameter is a pointer or not. If a pointer is passed to the function, the referenced data of the address changes. Therefore, the original variable is updated.

When a parameter is specified without a pointer, it means that the copied value is used in it. Hence, the original list is not updated in this example above.

The point of updating the original value is to pass a pointer (address).

Sponsored links

Pass a pointer for a struct

Let’s change the parameter to a struct. Define a struct and corresponding functions to update the internal data.

type Sensor struct {
    Value    int
    DataList []int
}

func updateDataForPointer(current *Sensor, newValue int) {
    if current != nil {
        current.Value = newValue
    }

}

func updateDataForValue(current Sensor, newValue int) {
    current.Value = newValue
}

This behaves the same way as an array/slice.

s2 := Sensor{Value: 99}
updateDataForValue(s2, 999)
fmt.Println(s2.Value) // 99
updateDataForPointer(&s2, 88)
fmt.Println(s2.Value) // 88

Even though 999 is specified for updateDataForValue(), it’s not reflected in the original value. Again, it’s because a copied struct is used in the function.

Define a method with pointer or without pointer

We already know the difference between with and without a pointer. This difference also applies to method definitions.

type Sensor struct {
    Value    int
    DataList []int
}

func (m *Sensor) UpdateValueWithPointer(value int) {
    m.Value = value
}

func (m Sensor) UpdateValue(value int) {
    m.Value = value
}

We can already understand that only a function with a pointer can update the original variable.

s := Sensor{Value: 1}
fmt.Println(s.Value) // 1
s.UpdateValue(2)
fmt.Println(s.Value) // 1
s.UpdateValueWithPointer(3)
fmt.Println(s.Value) // 3

This syntax (m *StructName) is called a pointer receiver.

When should a function without pointer is defined

Then, we should know when we should define a function without a pointer. It’s a good example if we define an alias type and need to have a function for it.

type aliasInt int32

func (m aliasInt) Square() aliasInt {
    return m * m
}

value := aliasInt(5)
fmt.Println(value)          // 5
fmt.Println(value.Square()) // 25

Enum is also a good example.

type MyEnum int8

const (
    _ MyEnum = iota
    Sleeping
    Running
    Idling
)

func (e MyEnum) String() string {
    switch e {
    case Sleeping:
        return "Sleeping"
    case Running:
        return "Running"
    case Idling:
        return "Idling"
    default:
        return "Unknown"
    }
}

If String() is not defined for the enum, it shows an integer which is not readable. The function is not a pointer receiver.

var myEnum MyEnum
fmt.Println(myEnum) // Unknown
myEnum = Sleeping
fmt.Println(myEnum) // Sleeping
myEnum = Running
fmt.Println(myEnum) // Running

But, always use a pointer receiver when one of the functions for a struct uses a pointer receiver for consistency.

Using a pointer receiver is 50 times faster ?!

We should know about the performance too. As mentioned above, the parameter is copied if it’s not a pointer receiver. If the struct is big, it affects the performance too.

Let’s take a benchmark here.

import "testing"

type manyInt64 struct {
    prop0 int64
    prop1 int64
    prop2 int64
    prop3 int64
    prop4 int64
    prop5 int64
    prop6 int64
    prop7 int64
    prop8 int64
    prop9 int64
}

type dataHolder struct {
    prop0 manyInt64
    prop1 manyInt64
    prop2 manyInt64
    prop3 manyInt64
    prop4 manyInt64
    prop5 manyInt64
    prop6 manyInt64
    prop7 manyInt64
    prop8 manyInt64
    prop9 manyInt64
}

type smallData struct {
    data int32
}

func (m smallData) ValueGetter() int32 {
    return m.data
}

func (m *smallData) PointerGetter() int32 {
    return m.data
}

func (m dataHolder) ValueProp0() manyInt64 {
    return m.prop0
}

func (m *dataHolder) PointerProp0() manyInt64 {
    return m.prop0
}

func (m dataHolder) ValueProp9() manyInt64 {
    return m.prop9
}

func (m *dataHolder) PointerProp9() manyInt64 {
    return m.prop9
}

func BenchmarkSmallStructValueGetter(b *testing.B) {
    var data smallData
    for i := 0; i < b.N; i++ {
        _ = data.ValueGetter()
    }
}

func BenchmarkSmallStructPointerGetter(b *testing.B) {
    var data smallData
    for i := 0; i < b.N; i++ {
        _ = data.PointerGetter()
    }
}

func BenchmarkValueGetter0(b *testing.B) {
    var data dataHolder
    for i := 0; i < b.N; i++ {
        _ = data.ValueProp0()
    }
}

func BenchmarkPointerGetter0(b *testing.B) {
    var data dataHolder
    for i := 0; i < b.N; i++ {
        _ = data.PointerProp0()
    }
}

func BenchmarkValueGetter9(b *testing.B) {
    var data dataHolder
    for i := 0; i < b.N; i++ {
        _ = data.ValueProp9()
    }
}

func BenchmarkPointerGetter9(b *testing.B) {
    var data dataHolder
    for i := 0; i < b.N; i++ {
        _ = data.PointerProp9()
    }
}

// $ go test ./benchmark -bench Getter -benchmem
// goos: linux
// goarch: amd64
// pkg: play-with-go-lang/benchmark
// cpu: Intel(R) Core(TM) i7-9850H CPU @ 2.60GHz
// BenchmarkSmallStructValueGetter-12              1000000000               0.2555 ns/op          0 B/op          0 allocs/op
// BenchmarkSmallStructPointerGetter-12            1000000000               0.2419 ns/op          0 B/op          0 allocs/op
// BenchmarkValueGetter0-12                        88152307                12.98 ns/op            0 B/op          0 allocs/op
// BenchmarkPointerGetter0-12                      1000000000               0.4835 ns/op          0 B/op          0 allocs/op
// BenchmarkValueGetter9-12                        89456086                12.48 ns/op            0 B/op          0 allocs/op
// BenchmarkPointerGetter9-12                      1000000000               0.2505 ns/op          0 B/op          0 allocs/op
// PASS
// ok      play-with-go-lang/benchmark     3.667s

When the struct is small enough, the difference is very small. However, when it’s a big struct, a pointer receiver is about 50 times faster than a value reference. It takes time to copy the whole struct. That’s why we have a big difference here.

If it’s necessary to define a getter function, it should always be a pointer receiver for the performance.

Clone my GitHub repository if you want to try it on your PC.

Comments

Copied title and URL