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