Surprise About How Method Receivers Work

259 views
Skip to first unread message

jlfo...@berkeley.edu

unread,
Dec 17, 2017, 3:32:24 PM12/17/17
to golang-nuts


Here's something that someone new to Go might run up against. I know I did.

The trivial program below, derived from "The Go Programming Language"
book, shows two methods that differ only in the way they treat their
receiver. In the first, the receiver is a pointer to a slice, and in the
second, the receiver is a slice. So far so good. What confused me is
that both methods return the same result even when called the same way.
I would have expected only one of the methods to work.

Here's what I believe is the explanation. The book says (slightly
edited) "If the receiver p is a variable of type Path but the method
requires a *Path receiver, we can use this shorthand:

p.pr1()

and the compiler will perform an implicit &p on the variable.". This
explains what happens when pr1() is called.

I'm surprised that Go would include this implicit behavior because type
conversions are generally required to be explicit.

Jon Forrest
-------


package main

import (
         "fmt"
)

type Path []byte

func (p *Path) pr1() {
         fmt.Printf("%s\n", *p)
}

func (p Path) pr2() {
         fmt.Printf("%s\n", p)
}

func main() {
         var p Path

         p = Path("abc")
         p.pr1()

         p = Path("abc")
         p.pr2()
}


Jeff Goldberg

unread,
Dec 18, 2017, 12:03:45 AM12/18/17
to golang-nuts
On Sunday, December 17, 2017 at 2:32:24 PM UTC-6, jlfo...@berkeley.edu wrote:

Here's something that someone new to Go might run up against. I know I did.

Me, too! 

The trivial program below, derived from "The Go Programming Language"
book, shows two methods that differ only in the way they treat their
receiver. In the first, the receiver is a pointer to a slice, and in the
second, the receiver is a slice. So far so good. What confused me is
that both methods return the same result even when called the same way.
I would have expected only one of the methods to work.

I never really figured out what was going on, but I finally just accepted it as "go is doing
what I want it to do, even if I say it wrong." 

Here's what I believe is the explanation. The book says (slightly
edited) "If the receiver p is a variable of type Path but the method
requires a *Path receiver, we can use this shorthand:

p.pr1()

and the compiler will perform an implicit &p on the variable.". This
explains what happens when pr1() is called.

Right. I guess this all works because there are so few things you can do with pointers; so this
isn't going to create odd ambiguities.
 
I'm surprised that Go would include this implicit behavior because type
conversions are generally required to be explicit.

I agree that it seems un-Go-like initially. But the more I think about it, the more it makes sense given
other restrictions on pointers. The only thing you can do with pointers is pass them and
deferences them. You never actually do anything with the value of the pointer itself. A type can have
methods, but a pointer to a type can't. So it's always(?) going to be safe for the compiler to do this.

Of course you don't want people using both forms within a local scope. So that is my guess at
why the this "shortcut" is allowed only via receivers.

I am, of course, speculating wildly. I would be pleased to be set straight by the people who actually
know what is going on.

And I pray to whatever is holy that I never take the habits I'm picking up with this back to C.

-j

matthe...@gmail.com

unread,
Dec 18, 2017, 10:06:10 AM12/18/17
to golang-nuts
With fmt you can print the pointer with %p. 

I like it better than the C way of . for value, and -> for the dereference shortcut instead of (*thing).field. Along with struct embedding I've had cases where C-like types act with object oriented level readability because of this Go feature.

Matt

Marvin Renich

unread,
Dec 18, 2017, 10:21:14 AM12/18/17
to golang-nuts
* jlfo...@berkeley.edu <jlfo...@berkeley.edu> [171217 15:33]:
> Here's what I believe is the explanation. The book says (slightly
> edited) "If the receiver p is a variable of type Path but the method
> requires a *Path receiver, we can use this shorthand:
>
> p.pr1()
>
> and the compiler will perform an implicit &p on the variable.". This
> explains what happens when pr1() is called.
>
> I'm surprised that Go would include this implicit behavior because type
> conversions are generally required to be explicit.
>
> type Path []byte
>
> func (p *Path) pr1() {
> fmt.Printf("%s\n", *p)
> }
>
> func (p Path) pr2() {
> fmt.Printf("%s\n", p)
> }
>
> func main() {
> var p Path
>
> p = Path("abc")
> p.pr1()
>
> p = Path("abc")
> p.pr2()
> }

I think you are looking for this statement in the Go spec:

A method call x.m() is valid if the method set of (the type of) x
contains m and the argument list can be assigned to the parameter list
of m. If x is addressable and &x's method set contains m, x.m() is
shorthand for (&x).m()

This can be found at https://golang.org/ref/spec#Calls near the bottom
of that section. This was clearly an intentional design decision.

...Marvin

jlfo...@berkeley.edu

unread,
Dec 18, 2017, 10:30:52 AM12/18/17
to golang-nuts


On Monday, December 18, 2017 at 7:21:14 AM UTC-8, Marvin Renich wrote:

I think you are looking for this statement in the Go spec:

  A method call x.m() is valid if the method set of (the type of) x
  contains m and the argument list can be assigned to the parameter list
  of m. If x is addressable and &x's method set contains m, x.m() is
  shorthand for (&x).m()

This can be found at https://golang.org/ref/spec#Calls near the bottom
of that section.  This was clearly an intentional design decision.

No doubt, but for someone coming to Go from C, it seems like a step
backwards. I like the "What You See Is What You Get" style of programming
language, which mostly describes Go very well. This is an exception.

Jon


Dave Cheney

unread,
Dec 18, 2017, 3:12:22 PM12/18/17
to golang-nuts
It's true it is an exception, it's one of the few cases where the language adds a pinch of syntactic sugar to make the experience more pleasurable.

I can imagine without this the number one oft repeated feature request would be to _not_ have to write (&t).m() all the time when you just wanted to write t.m(). Compared to the subtle footguns of for range capturing loop variables, the compiler harmonizing method dispatch has always felt to me to be the right compromise of a small amount of magic--the illusion is only broken by map addressablity--in pursuit of Go's other goals of readability and productivity. 

jlfo...@berkeley.edu

unread,
Dec 18, 2017, 4:05:54 PM12/18/17
to golang-nuts


On Monday, December 18, 2017 at 12:12:22 PM UTC-8, Dave Cheney wrote:
It's true it is an exception, it's one of the few cases where the language adds a pinch of syntactic sugar to make the experience more pleasurable.

I'd describe this more as removing a pinch of syntactic sugar. 

I can imagine without this the number one oft repeated feature request would be to _not_ have to write (&t).m() all the time when you just wanted to write t.m().

Maybe so, but you know where that leads. Soon those people will start complaining about the requirement for explicit type conversions too.

Anyway, thanks for confirming my reaction.

Jon

 

matthe...@gmail.com

unread,
Dec 18, 2017, 6:05:19 PM12/18/17
to golang-nuts
Here's a specific example of how this works for me. I have a chess board represented as a 64 element array of points:

type Point struct {
 
*Piece // nil for no piece
 
AbsPoint
}
// Absolute Point represents a specific point on the board.
type
AbsPoint struct {
 
File uint8
 
Rank uint8
}
type Piece struct {
 
Kind
 
Orientation
 
Base  Kind
 
Moved bool `json:"-"`
}
type Kind int

const (
 
King Kind = iota + 1
 
Queen
 
Rook
 
Bishop
 
...

Often I'll be iterating over a subset of points on the board and want to check what kind of piece is there. The pointer shortcut (and struct embedding) definitely cleans up my code.

for _, point := range set {
 
if point.Piece != nil {
   
if (point.Kind == King) && (point.Moved == false) && (point.Orientation == mover) {
     
// do something
     
break
   
}
 
}
}
// instead of
for _, point := range set {
 
if point.Piece != nil {
   
if (*(point.Piece).Kind == King) && (*(point.Piece).Moved == false) && (*(point.Piece).Orientation == mover) {
   
// or if (point.Piece->Kind == King) && (point.Piece->Moved == false) && (point.Piece->Orientation == mover) {
     
// do something
     
break
   
}
 
}
}

Given more pieces than regular chess this kind of logic happens many times and I appreciate the reduced word count. For a newcomer it may be surprising and require a type lookup, but newcomers aren't maintaining the code.

Matt

matthe...@gmail.com

unread,
Dec 18, 2017, 6:07:29 PM12/18/17
to golang-nuts
Well you're talking about method receivers, not struct fields. It's a similar shortcut, but sorry about going off topic with the example.

Matt
Reply all
Reply to author
Forward
0 new messages