TL;DR
Click for spoiler
The following three Go method expressions are actually equivalent!
time.Now().String() // 😎
time.Time.String(time.Now()) // 🤔
fmt.Stringer.String(time.Now()) // 🤯
mind == "blown"
I learned something new today about invoking methods in Go. This came about because of an interesting little bit of code I was reviewing with a coworker who is new to Go. They had written some code which, quite frankly, blew my mind because it didn't appear to be valid syntax based on my experience (yet it was clearly compiling and executing on the coworker's machine). Here's a simplified version of the code:
func Foo(v any) {
pointer := reflect.ValueOf(v)
if pointer.Kind() != reflect.Ptr {
return
}
actualType := reflect.Type.Elem(pointer.Type()) // <- WAT!
fmt.Println(actualType)
}
What really threw me off was the line right before the fmt.Println
. I knew that reflect.Type
is an interface definition, and that Elem()
is a method defined with the Type
interface, but the Go compiler in my head threw up all sorts of errors that went something like this:
"Hold it right there! You can't just directly invoke methods on interface definitions! (Can you?)"
...
"Wait a minute, even if you could invoke methods on interface definitions, the
Elem()
func is niladic--it doesn't receive any parameters!"
...
"Hold up, why have I never seen anything like this? How is this code even compiling, let alone executing!"
...
Ok, so after some research we learned that
- you actually can invoke methods on interface definitions, if you pass a valid receiver, because
- the receiver is actually just a glorified function/method argument (it's just listed/defined separate from the other parameters), and
- this alternate syntax even allows you derive a function with an explicit pointer receiver!
And this has all been in the official spec forever:
If M is in the method set of type T, T.M is a function that is callable as a regular function with the same arguments as M prefixed by an additional argument that is the receiver of the method. (emphasis mine)
A Simple Example
Let's break this concept down with an example that doesn't involve reflection.
Consider the following Go type definition:
type Point struct { x, y int }
Now, a trivial method:
func (p Point) String() string {
return fmt.Sprintf("(%d,%d)", p.x, p.y)
}
Suppose now that we had an instance of our type:
func main() {
p := Point{x: 1, y: 2}
...
}
Now suppose you wanted to print the instance instance by calling the String
method defined previously:
fmt.Println(p.String())
This is all pretty standard, but did you know that you could accomplish the same method invocation in this way?
fmt.Println(Point.String(p))
Conversely, did you know that you can get to it via the fmt.Stringer
interface (because our Point
implements fmt.Stringer
)?
fmt.Println(fmt.Stringer.String(p))
And that is the equivalent of the line that blew my mind from the original code snippet above:
actualType := reflect.Type.Elem(pointer.Type())
Hmm
In case you're having trouble accepting that all of this code even compiles, here's a Go Playground--have at it.
I'm really not sure why this alternate syntax exists, but I guess it's interesting, maybe because it hints at the underlying representation of types and methods. But what I'd like to know is whether it facilitates any scenario or solves some problem that the more traditional method expression syntax doesn't provide for?