How type embedding intersects with Pointer and Value method receivers in Go

How type embedding intersects with Pointer and Value method receivers in Go

I recently encountered a subtlety in Go language in how methods exposed by an embedded type are resolved vs. those with the same name defined by the container types depending on whether they are defined on Pointer or Value receivers. A quick trip to the language documentation however helped me understand subtlety and fix the unexpected result I was getting. It left me with a greater appreciation for how different features of the Go Language are designed to interact in cleanly defined manners.

Specifically, I wanted to embed time.Time into a type called ISODate which would have a json.Marshaler and json.Unmarshaler defined to handle dates specified in the format 2016-01-02. As a convenience, I wanted to implement the Stringer interface on ISODate to return dates in the same format, over-riding how time.Time handles it. The code snippet below shows the Stringer portion.

1
2
3
4
5
6
7
type ISODate struct {
	time.Time
}

func (t *ISODate) String() string {
	return t.Format("2006-01-02")
}

By of force of habit I had defined the Stringer with a pointer receiver. However, when I tried to use the String() method above, I saw the time.Time.String formatting of 2006-01-02 15:04:05.999999999 -0700 MST fomat instead of the 2006-01-02 fomrat that I wanted. Digging into the problem a bit, I found that calling String() on &ISODate produced the correct formatting. Clearly I was missing something in my understanding of Pointer and Value receivers for methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
	"fmt"
	"time"
)

type ISODate struct {
	time.Time
}

func (t *ISODate) String() string {
	return t.Format("2006-01-02")
}

func main() {
	t := ISODate{time.Date(2006, 1, 2, 3, 4, 5, 7, time.UTC)}
	fmt.Println(t)
	fmt.Println(&t)
}

Output:
{2006-01-02 03:04:05.000000007 +0000 UTC}
2006-01-02

Two of the real gems in the Go language documentation are the Language Specification which is remarkably compact and readable for a technical document and the Effective Go article which contains somewhat friendlier explanations of the concepts.

Sure enought, in Effective Go’s section on Pointer vs Value receivers, I found the following explanation:

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.

I had defined my ISODate.String() method with a pointer receiver in the code above and so clearly the second fmt.Println(&t) called it as expected. However, where did the first fmt.Println(t) output come from?

Looking at the code for time.Time.String() we see that the String() function is defined on a value receiver. Since time.Time is embedded in ISODate and its methods are therefore promoted, it turns out that ISODate in this case had two String() methods - one with a pointer receiver and the other with a value receiver.

1
2
3
4
5
// String returns the time formatted using the format string
//	"2006-01-02 15:04:05.999999999 -0700 MST"
func (t Time) String() string {
    return t.Format("2006-01-02 15:04:05.999999999 -0700 MST")
}

The fix was simply to define the String() method on a value receiver so that the underlying String() of the time.Time could be overridden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"fmt"
	"time"
)

type ISODate struct {
	time.Time
}

func (t ISODate) String() string {
	return t.Format("2006-01-02")
}

func main() {
	t := ISODate{time.Date(2006, 1, 2, 3, 4, 5, 7, time.UTC)}
	fmt.Println(t)
	fmt.Println(&t)
}

Output:
2006-01-02
2006-01-02

Srinath
Srinath Curiosity driven innovator in data science & evidence based marketing. Programmer for over 25 years. Multi-instrumentalist.