Why were method sets designed like this?

411 views
Skip to first unread message

Alexander Shopov

unread,
Jun 10, 2025, 4:06:42 PMJun 10
to golang-nuts
Does anyone have any idea why method sets were designed like this?

Method sets of pointer types include the methods defined on the type
that the pointer points to (Explicitly pointed out in spec
https://go.dev/ref/spec#Method_sets)

I get the idea that having a pointer means that one can mutate the
value - so in essence it may be useful to have the pointer method set
contain the value method set.
But having a pointer to a value does not guarantee the existence of value.

Let's have a look at this trivial example: https://go.dev/play/p/ztNRPYfoAuk

type num interface {

num() int
}

type example struct{}

func (e example) num() int {
return 42
}

func useNum(n num) {
fmt.Println(n == nil)
v := reflect.ValueOf(n)
fmt.Println(v.Kind() == reflect.Ptr && v.IsNil())
fmt.Println(n.num())
}

func main() {
var e1 example
var e2 *example
useNum(e1)
useNum(e2)
}

useNum has no safe way to call the num() method, even though we are
sure it is safe to call on a nil, in essence it never accesses the
state of the example struct.
However a caller can substitute a value with a pointer. Within the
useNum function we cannot guard with a n != nil since it receives an
interface and it will not be nil.
The only way to guard is with reflection.

This same method is also used in Go standard library:
- https://go.dev/src/fmt/print.go#L592
- https://go.dev/src/log/slog/handler.go#L564

So given this - is there any source on why method sets were designed this way?

Kind regards:
al_shopov

burak serdar

unread,
Jun 10, 2025, 4:32:09 PMJun 10
to Alexander Shopov, golang-nuts
On Tue, Jun 10, 2025 at 2:06 PM Alexander Shopov <a...@kambanaria.org> wrote:
>
> Does anyone have any idea why method sets were designed like this?
>
> Method sets of pointer types include the methods defined on the type
> that the pointer points to (Explicitly pointed out in spec
> https://go.dev/ref/spec#Method_sets)
>
> I get the idea that having a pointer means that one can mutate the
> value - so in essence it may be useful to have the pointer method set
> contain the value method set.

The method set of a type T contains the methods that are declared using T.
The method set of a type *T contains the methods that are declared
using T and the methods that are declared using *T.
This guarantees that an interface containing a value of type T does
not implement the methods that work on *T, because the value contained
in the interface is not addressable.

The fact that an interface may contain a nil value and thus may pass a
nill check but panic with a nil ptr error is one of the annoying
idiosyncrasies of Go interfaces. Method sets are not related to this.
> --
> You received this message because you are subscribed to the Google Groups "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/CAP6f5Mm1eF4XZw5sJMvrP4-sb2DkppoFuAtzbwUcbdXQyP1J8w%40mail.gmail.com.

Axel Wagner

unread,
Jun 10, 2025, 5:13:43 PMJun 10
to Alexander Shopov, golang-nuts
Hi,

assume that method values were not promoted to pointer types.
And pointer receiver methods can obviously not get promoted to value types: If you pass
var v T; f(v)
you don't expect `v` to be modified. But pointer methods would expect that. You'd get hard to find, confusing bugs.
So the method set of a type is exactly the set of methods declared with that receiver type.

Now consider flag.Value.
Obviously, the Set method requires a pointer receiver *T.
If String would have a value receiver T, then neither T nor *T would implement flag.Value: Neither type has both methods.
So String also needs to have a pointer receiver.
But that means, if you do e.g.
var t time.Time; fmt.Println(t)
the String method would not get called - it would format as something like {42,23,0xdeadbeef}.

The promotion of value receiver methods to pointer types means it is possible for a type to mix receiver kinds. Methods that make more sense on a value - like String - can use a value receiver, while methods that require a pointer - like Set - can still use a pointer receiver.
Yes, this means the program will panic if you call String on a nil-pointer. But… well, don't do that, then. It would panic if the receiver was a pointer receiver as well, as soon as it tries to access on of the receivers fields (which it needs to do to stringify).

What is a little bit confusing is, that while pointer receiver methods are not promoted to value types, you can still do
var b strings.Builder; b.WriteString("foo")
despite WriteString having a pointer receiver. That's because the selector expression becomes syntactic sugar for (&b).WriteString under these circumstances.
Personally, I kind of feel this was a mistake, but c'est la vie.

Axel Wagner

unread,
Jun 10, 2025, 5:20:32 PMJun 10
to Alexander Shopov, golang-nuts
Oh and regarding your question about checking for nil: My recommendation is to just not do that. Given that the method has a value receiver, it is clear that calling it on a nil-pointer is a bug. A panic, with traceback, is the most useful thing you can do in that situation anyways, as it allows you to find the bug more quickly.

If you try to guard at every point against someone introducing a bug, the possible state-space of your program will explode. It will become impossible to reason about, as there are no invariants to rely on and everything gets riddled with extra checks and assertions, introducing hard to follow extra control flow. In my experience, that will end up introducing more bugs into your code than your checks prevent. Simpler, more obvious code is easier to test, review and check for problems. And has fewer bugs in practice.

Alexander Shopov

unread,
Jun 10, 2025, 6:01:59 PMJun 10
to golang-nuts
Again - I am interested in the "why they were designed this way".

@Axel Wagner:
> checking for nil: My recommendation is to just not do that
That is the reason why I gave the examples with the standard library.
Within very similar circumstances - a logging framework I need to
ensure that adding a log statement would not cause a problem. We may
argue whose fault a panic within a particular call is - but there are
cases when you do not want a call to cause panic. The fact the
standard library does this - for fmt and logging is a very strong
argument that there are cases where this is necessary. In very similar
circumstances I need to do something similar. Note that a caller may
not be the real cause of the problem as the interface value may have
been passed to them. The same argument applies to them - they have no
safe way to decide whether it is safe to pass the value to the
function expecting the interface.

@Burak Serdar:
> The fact that an interface may contain a nil value... Method sets are not related to this.
I disagree with you on this. See https://go.dev/play/p/N20xLUeaVMz
In this case the receiver is declared on a pointer. The interface
definitely contains a nil value but does not cause a panic.
- A method defined on a type is counted in the method set of the pointer
- Therefore the pointer can be passed as an argument to the function
since it is considered to have the method (and thus fulfil the
interface)
- At the point the interface method is called - the compiler does
dereference to get the value and since it is nil we get a panic
Compare this when the method is on the pointer. Again the pointer
fulfils the interface however at the point of call there is no need
for dereference (at least in this particular method). Therefore there
is no panic.

I think it is exactly the method set logic that causes this. Compare
it with this slightly changed example
https://go.dev/play/p/k1zq6vMWwVI
The method is still on the pointer but we pass a pointer to that
pointer rather than the original pointer. In this case the whole thing
does not compile at all - exactly as the spec says:

@The method set of a pointer to a defined type T (@@where T is neither
a pointer nor an interface@@) is the set of all methods declared with
receiver *T or T.@

We get the increase of method sets on moving from a non-pointer to
pointer. There is no such increase when we go from a pointer to a
pointer to a pointer.

Therefore - why were method sets designed this way? It was totally
possible to not do the automatic inclusion of methods on non pointer
types to the method set of pointers to the types.

Kind regards:
al_shopov

Axel Wagner

unread,
Jun 11, 2025, 12:02:02 AMJun 11
to Alexander Shopov, golang-nuts
On Wed, 11 Jun 2025 at 00:01, Alexander Shopov <a...@kambanaria.org> wrote:
Therefore - why were method sets designed this way? It was totally
possible to not do the automatic inclusion of methods on non pointer
types to the method set of pointers to the types.

What about my explanation was dissatisfying?
It would be possible not to do it, but there would be downsides.
 

Kind regards:
al_shopov

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

roger peppe

unread,
Jun 11, 2025, 1:22:19 AMJun 11
to Alexander Shopov, golang-nuts
On Tue, 10 Jun 2025 at 23:00, Alexander Shopov <a...@kambanaria.org> wrote:
Therefore - why were method sets designed this way? It was totally
possible to not do the automatic inclusion of methods on non pointer
types to the method set of pointers to the types.

Somewhat relevant proposal from a while back: https://github.com/golang/go/issues/18775
This would have mitigated your concerns a bit I think, but was deemed too incompatible to be acceptable.

  cheers,
    rog.

Alexander Shopov

unread,
Jun 11, 2025, 5:19:15 AMJun 11
to golang-nuts, r...@golang.org, Ian Lance Taylor, r...@golang.org, g...@golang.org, k...@golang.org
@Axel Wagner
> What about my explanation was dissatisfying?
Please do not take my comment to sound combative.
You gave me *an* explanation but I was looking for *the* explanation.
I was looking more for a design document, discussion, some piece of
tribal knowledge.
Of course there would be downsides for a different design but I am
looking at the way pro-s and con-s were evaluated.

@Roger Peppe
Thanx. Quite informative - this gives me a pointer on what happens
next and the fact that this behavior is bothersome for other people.

Another data point: the catchPanic workaround in Go Standard lib came
only in https://github.com/golang/go/commit/97a929aac95301b850fb855e8e2fa8cfbe47ef59
This is post go 1.3 (June 2014) but pre 1.4 (Dec 2014). This means
that 4 public releases of Go passed (1.0, 1.1, 1.2, .1.3) before this
was implemented.
Note the review of the patch https://codereview.appspot.com/4640043
and also the comments. It does not sound like a widely known
workaround.

If anyone is interested further in this:
git log -G'ValueOf.*Kind.*Ptr.*IsNil' gets some more details:

1. It is Rob Pike implementing this
2. Another place where you do not want such failures is in the AST
tools: https://github.com/golang/tools/blob/master/go/ast/astutil/rewrite.go#L190
3. Another place json encoding/decoding:
https://github.com/golang/go/blob/master/src/encoding/json/decode.go

Compare with the Golang specification:
1. Spec started on Mar 2, 2008 -
https://github.com/golang/go/commit/18c5b488a3b2e218c0e0cf2a7d4820d9da93a554
by Robert Griesemer
2. First usage of the term is on May 8, 2009 -
https://github.com/golang/go/commit/df46b3342ce54129af59e30ff6d9708347f61c75
by Rob Pike
3. https://github.com/golang/go/commit/533dfd62919ecb69a39973cea32d06a1cb166687
on May 13, and https://github.com/golang/go/commit/56809d0ade51d3bbd653ba9e9b7c54e2f4ec5f66
on May 20 by Robert Griesemer stronly corroborate Rob Pike as
originator of the terminilogy.
For comparison Go 1 was released March 2012.

I am really interested in the background of thinking about method
sets, whether this behavior was thought of initially or it was just an
emerging surprise. Rob has never called this a thing Go got wrong
AFAIK.
Was it programmers misusing the API that were possible to misuse that
caused this implementation?

Kind regards:
al_shopov

Axel Wagner

unread,
Jun 11, 2025, 6:59:44 AMJun 11
to Alexander Shopov, golang-nuts
The terminology was introduced in this commit:
One thing you'll notice is that before this, there was a rule that all methods must have the same receiver type. So it was not possible to declare a mix of pointer- and value-receivers. The rules where changed to allow that and the terminology of method sets was introduced at the same time. Including the promotion from value- to pointer-receiver methods.

So, IMO, the reason is pretty clearly "to allow a mix of pointer- and value receivers".

On Wed, 11 Jun 2025 at 11:19, Alexander Shopov <a...@kambanaria.org> wrote:
@Axel Wagner
> What about my explanation was dissatisfying?
Please do not take my comment to sound combative.
You gave me *an* explanation but I was looking for *the* explanation.
I was looking more for a design document, discussion, some piece of
tribal knowledge.

I was trying to provide tribal knowledge (I've been using Go at least as far back as 2013). But, of course, I was not in the room before the open source release. Which is when this was decided. As I understand it, the process didn't really include any design documents at that time. And any discussion is, if it was written down at all, on Google-internal lists.

So it seems to me, a more satisfying answer can only be provided by the ten or so people working on it at that time and I'll duck out.
 
--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Robert Griesemer

unread,
Jun 12, 2025, 1:18:57 PMJun 12
to Alexander Shopov, golang-nuts, r...@golang.org, Ian Lance Taylor, r...@golang.org, k...@golang.org
The basic idea about interfaces and methods is (this is the pre-generic view point, which is relevant here):

1) An interface defines methods (an interface is a method set)
2) Methods can be attached to (associated with) any type (excluding interfaces which have their own syntax for methods)
3) A value of a type that has all the methods of an interface type can be assigned to a variable of that interface type
4) When a method is called on an interface, the dynamic type of the interface determines which method to call.

Then, there were some explicit syntactic/semantic design decisions:

5) A function becomes a method by specifying its receiver parameter before the method name. The receiver parameter type is the type to which the method is attached.
6) A method can only be attached to a defined (named) type (and that type cannot be an interface).

In this simple-most form, a value x of defined type T can be assigned to a variable v of interface type I if T has all the methods of I. When a method m of I is called,
the type T of the value x stored in v is used to determine the method m of T to call with x as receiver value: so, if x is in v, v.m(...) dynamically
looks up T.m (T is the type of x) and then calls T.m(x, ...). Importantly, the receiver value stored in the interface is simply passed through the method m.
(Recall that an interface is a pair (type, value) where type is the type of the value. Lookup uses the type and method name to find the right method.)

In this scenario, the method set of a type T is simply the methods with receiver type T ("the T methods").

In reality, this is cumbersome because it means that if we want to pass a reference (a pointer) to a data structure, we would have to give that pointer type an explicit name
so that we can attach methods to it.

More generally, even without methods, typically names are given to non-pointer data types T, and then we simply use *T to denote the respective pointer type.
That is, in practice we think of a *T not so much as a distinct type, but simply as a reference to T, and we use *T instead of a specific type name for it.
(This is a long-established coding practice, e.g. in C.)

But we still want to be able to pass receiver values as references (pointers), for efficiency and sharing purposes.
Thus, we allow a receiver type to be of the form *T (an unnamed type) in addition to T, and we say that methods are always associated with the receiver base type (T in this case);
it's not possible to have two methods with the same name with a pointer and non-pointer receiver.
And we also disallow named receiver types T that are pointers (methods cannot be attached to pointers).

Now, given a defined type T, we can have methods

func (x T) m1 ...
func (x *T) m2 ...

with pointer and non-pointer receivers *T and T.

As before, if for each method m of an interface I there is a method m of T with non-pointer receiver T, a value of x can be assigned to an I variable.
Similarly, if for each method m of an interface I there is a method m of T with pointer receiver *T, a pointer to an x (&x) can be assigned to an I variable.

That is, the receiver data again is simply passed through.
Until here there are no changes to the basic mechanism outlined above, except that we allow T and *T types as receivers, where T must be a defined non-pointer and non-interface type.

But given a pointer p (= &x) of type *T, one can always get to the x through an indirection (assuming p is not nil). Therefore, we can include all the methods of T with receiver type T
as part of the method set of *T. Which is why method sets for *T types include the methods with T and *T receiver (and which is why the method names
with T and *T receivers must be distinct).

Given an non-pointer method m

func (x T) m ...

the compiler automatically generates a wrapper method m'

func (p *T) m'(...) { (*p).m(...) }

as needed, which calls the non-pointer method. In this case, if m is called on a nil-pointer p, there will be a run-time panic.
This is true whether m is called directly on p or through an interface (after p was assigned to that interface).

Alternatively, one could call such methods via explicit indirection, as in (*p).m(), but this doesn't work if m is called through an interface.
By including the T methods in the method set of *T, the automatic indirection works also with interfaces; so there's a significant convenience gained here.
On a more abstract level, including both *T and T methods in the method set of *T reflects the view that *T values are just references of T data structures.

Note that it's also possible to call an *T method on a variable x (but not a value) of (non-pointer) T, even though the method set of
T doesn't include the methods with receiver type *T. More precisely, this works if x is addressable (variables are addressable), in which case
the compiler automatically takes the address of x before calling the method. This doesn't work through interfaces (once the x is in the interface,
it's a copy of the original one, and taking that copy's address would lead to a pointer to the wrong object). Which is why in this case the type set
is not augmented.

With both these two "adjustments" to the basic idea outlined at the start, we can write methods with T and *T receivers and use them on T and *T
(references to T) and it usually "just works" (we cannot call a *T method on a T value that is not addressable).
That is, we can concentrate on the data structure T and methods associated with T; whether we use a *T or a T for receivers can be an orthogonal
decision based on whether sharing or efficiency is relevant.

(All this said, I don't know how inconvenient it would be if *T and T method sets were strictly aligned with *T and T types. It would certainly be
simpler.)

I hope this helps.
I am not sure I remember any more design rationale here. Russ Cox might have more to say.

- gri






Ian Lance Taylor

unread,
Jun 12, 2025, 3:41:17 PMJun 12
to Alexander Shopov, golang-nuts, r...@golang.org, r...@golang.org, g...@golang.org, k...@golang.org
On Wed, Jun 11, 2025 at 2:17 AM Alexander Shopov <a...@kambanaria.org> wrote:
>
> You gave me *an* explanation but I was looking for *the* explanation.
> I was looking more for a design document, discussion, some piece of
> tribal knowledge.

Apart from Robert's comprehensive description of how the language
works, I'll just add that the current set of rules evolved over time,
with experience in using the language. The exact way that method sets
worked with pointer types came from experience in writing early
versions of Go. There was no design document. Since it happened before
Go was released externally, there was no public discussion.

Ian
Reply all
Reply to author
Forward
0 new messages