6

The Go FAQ answers a question regarding the choice of by-value vs. by-pointer receiver definition in methods. One of the statements in that answer is:

If some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used.

This implies that if I have for a data type a few methods that mutate the data, thus require by-pointer receiver, I should use by-pointer receiver for all the methods defined for that data type.

On the other hand, the "fmt" package invokes the String() method as defined in the Stringer interface by value. If one defines the String() method with a receiver by-pointer it would not be invoked when the associated data type is given as a parameter to fmt.Println (or other fmt formatting methods). This leaves one no choice but to implement the String() method with a receiver by value.

How can one be consistent with the choice of by-value vs. by-pointer, as the FAQ suggests, while fulfilling fmt requirements for the Stringer interface?

EDIT:

In order to emphasize the essence of the problem I mention, consider a case where one has a data type with a set of methods defined with receiver by-value (including String()). Then one wishes to add an additional method that mutates that data type - so he defines it with receiver by-pointer, and (in order to be consistent, per FAQ answer) he also updates all the other methods of that data type to use by-pointer receiver. This change has zero impact on any code that uses the methods of this data type - BUT for invocations of fmt formatting functions (that now require passing a pointer to a variable instead of its value, as before the change). So consistency requirements are only problematic in the context of fmt. The need to adjust the manner one provides a variable to fmt.Println (or similar function) based on the receiver type breaks the capability to easily refactor one's package.

icza
  • 389,944
  • 63
  • 907
  • 827
ElazarR
  • 2,288
  • 1
  • 12
  • 13
  • Please note that that's the FAQ and not the spec, the spec allows for mixed receivers (value/pointer) for a single type, your program will still compile if method A is on a pointer while B on a value. Also *should* is not the same as *must*, I personally consider the statement in the FAQ a general recommendation and not something to be blindly followed. You have a case where it does not make sense? Then do not adhere to it! – mkopriva Jan 17 '18 at 10:04
  • 2
    Wow! You discovered that later code changes might produce unexpected results! Welcome to the club. Test. – Volker Jan 17 '18 at 10:12
  • @mkopriva , My question is about the FAQ consistency with core package "fmt", which requires by-value receiver (assuming one wishes to use "fmt" methods in the "natural" way of providing the variables themeselves and not a reference/pointer to them). As you suggest, I need to ignore the FAQ in this case, but I ask why would the FAQ state such an approach if in the context of a core package it cannot be adhered? – ElazarR Jan 17 '18 at 11:07
  • @Volker, addition of a new method to an API should not break existing code if nothing changed in the existing API. This is not a matter of testing but a design problem. Why would an addition of a new independent method affect existing methods? The recommendation of the FAQ to be consistent with the receiver type (value or ptr) across all methods creates a problem that does not exist otherwise. – ElazarR Jan 17 '18 at 11:13
  • 2
    @ElazarR first off, the `fmt` package does not require by-value afaik; second, "natural"? "natual" by whose opinion? If a variable is already a pointer is this `*v` more "natural" than this `v`? The FAQ states a general recommendation, in my understanding anyway, and it's up to the users of the Go language, core package or not, whether they adhere to the recommendation or not. As far as using `fmt` goes, a client library could easily adhere to it, your case may differ, but that does not mean that "it cannot be adhered" to in general. – mkopriva Jan 17 '18 at 11:26
  • 1
    @ElazarR Adding the method doesn't break anything. Changing the receivers does. Best advice would be: Stop worrying about non-problems in real life. – Volker Jan 17 '18 at 12:57
  • This is a confusing and solid issue, though some try to dismiss it. Given a value, you can in fact invoke either pointer or value receiver methods on it. When passed to fmt functions, only value receivers will be invoked. This boils down to this: https://groups.google.com/g/golang-nuts/c/wnH302gBa4I When passing to fmt.Println(), you're implementing interface{}, later type-asserted to Stringer. The compiler cannot and will not check the implementation you provided for a possible alternative implementation using the pointer type, it does not even know if you passed a pointer or not. – Sean F Oct 04 '21 at 22:08
  • So this is not really a pointer vs value methods issue, this is an interface implementation issue when implementing interface{} in calls to fmt methods. – Sean F Oct 04 '21 at 22:10

3 Answers3

3

If you define your methods with pointer receiver, then you should use and pass pointer values and not non-pointer values. Doing so the passed value does indeed implement Stringer, and the fmt package will have no problem "detecting" and calling your String() method.

Example:

type Person struct {
    Name string
}

func (p *Person) String() string {
    return fmt.Sprintf("Person[%s]", p.Name)
}

func main() {
    p := &Person{Name: "Bob"}
    fmt.Println(p)
}

Output (try it on the Go Playground):

Person[Bob]

If you would pass a value of type Person to fmt.Println() instead of a pointer of type *Person, yes, indeed the Person.String() would not be called. But if all methods of Person has pointer receiver, that's a strong indication that you should use the type and its values as pointers (unless you don't intend its methods to be used).

Yes, you have to know whether you have to use Person or *Person. Deal with it. If you want to write correct and efficient programs, you have to know a lot more than just whether to use pointer or non-pointer values, I don't know why this is a big deal for you. Look it up if you don't know, and if you're lazy, use a pointer as the method set of (the type of) a pointer value contains methods with both pointer and non-pointer receiver.

Also the author of Person may provide you a NewPerson() factory function which you can rely on to return the value of the correct type (e.g. Person if methods have value receivers, and *Person if the methods have pointer receivers), and so you won't have to know which to use.

Answer to later adding a method with pointer receiver to a type which previously only had methods with value receiver:

Yes, as you described in the question, that might not break existing code, yet continuing to use a non-pointer value may not profit from the later added method with pointer receiver.

We might ask: is this a problem? When the type was used, the new method you just added didn't exist. So the original code made no assumption about its existence. So it shouldn't be a problem.

Second consideration: the type only had methods with value receiver, so one could easily assume that by their use, the value was immutable as methods with value receiver cannot alter the value. Code that used the type may have built on this, assuming it was not changed by its methods, so using it from multiple goroutines may have omitted certain synchronization rightfully.

So I do think that adding a new method with pointer receiver to a type that previously only had methods with value receiver should not be "opaque", the person who adds this new method has the responsibility to either modify uses of this type to "switch" to pointers and make sure the code remains safe and correct, or deal with the fact that non-pointer values will not have this new method.

Tips:

If there's a chance that a type may have mutator methods in the future, you should start creating it with methods with pointer receivers. Doing so you avoid later having to go through the process described above.

Another tip could be to hide the type entirely, and only publish interfaces. Doing so, the users of this type don't have to know whether the interface wraps a pointer or not, it just doesn't matter. They receive an interface value, and they call methods of the interface. It's the responsibility of the package author to take care of proper method receivers, and return the appropriate type that implements the interface. The clients don't see this and they don't depend on this. All they see and use is the interface.

icza
  • 389,944
  • 63
  • 907
  • 827
  • But in Go one does not really need to derefence a variable to invoke a method that is defined by-pointer. So even for a data type that has methods by-pointer the user of that data type would never bother to dereference the value (of `Person`, in your example). Only when using "fmt" one is required to recall that for that data type the methods are by-pointer and dereference it. This requires knowing how each data type's methods are implemented - which is not required for plain method invocation (consider the case when the data type is defined in another package). – ElazarR Jan 17 '18 at 09:03
  • This is not a matter of possible or not and I understand that there are workarounds to this situation and one can always check the definition for a package he uses, however these solutions are not really scalable in a big project involving many independent programmers. Please see my clarification edits in the question. – ElazarR Jan 17 '18 at 09:34
  • What do you mean by "modify uses of this type to "switch" to pointers..."? In GO one does not have to change anything in the using code if a receiver of a method was changed from by-value to by-pointer. All the invocations by-value would still compile and run correctly (thanks to the compiler). The problem remains - only "fmt" related invocations require a modification in this case. – ElazarR Jan 17 '18 at 10:59
  • @ElazarR By that I mean if someone used the type by creating a non-pointer value of it, you'd have to change it to be a pointer, e.g. `p := Person{}` to `p := &Person{}`. – icza Jan 17 '18 at 11:08
  • Not really. I can keep using `p.SomeMethod()` whether the method receives a pointer or a value for both p as a pointer (reference) as well as a value variable. GO compiler does not care. Since there is one method for either the object (value) or its pointer the compiler invokes the correct one and there is no need to change the using (caller) code. Only "fmt" functions distinguish between the by-value and the by-pointer cases of the String() method when invoking it for Println and other functions. – ElazarR Jan 17 '18 at 16:50
  • @ElazarR That is a shortcut which is replaced by the compiler, I know that. But that only works if the operand is addressable. For example using my `Person` type from the answer, the following expression gives a compile-time error: `map[int]Person{1: {Name: "Bob"}}[1].String()`, but if you change `Person` to `*Person` it compiles and runs fine:`map[int]*Person{1: {Name: "Bob"}}[1].String()`. For details on this, check [this answer](https://stackoverflow.com/a/44543748/1705598) or this [other answer](https://stackoverflow.com/a/34197367/1705598). – icza Jan 17 '18 at 17:02
  • @ElazarR And stop "beating" the `fmt` package, it is not particular in any way. It uses reflection to check whether the passed values implement the `Stringer` interface, that's all. This technique is used by countless other packages. – icza Jan 17 '18 at 17:04
2

In order to emphasize the essence of the problem I mention, consider a case where one has a data type with a set of methods defined with receiver by-value (including String()). Then one wishes to add an additional method that mutates that data type - so he defines it with receiver by-pointer, and (in order to be consistent, per FAQ answer) he also updates all the other methods of that data type to use by-pointer receiver. This change has zero impact on any code that uses the methods of this data type - BUT for invocations of fmt formatting functions (that now require passing a pointer to a variable instead of its value, as before the change).

This is not true. All interface of it and some of type assertions will be affected as well - that is why fmt is affected. e.g. :

package main

import (
    "fmt"
)

type I interface {
    String() string
}

func (t t) String() string { return "" }

func (p *p) String() string { return "" }

type t struct{}
type p struct{}

func S(i I) {}

func main() {
    fmt.Println("Hello, playground")
    T := t{}
    P := p{}
    _ = P
    S(T)
    //S(P) //fail
}

To understand this from the root, you should know that a pointer method and a value method is different from the very base. However, for convenience, like the omit of ;, golang compiler looks for cases using pointer methods without a pointer and change it back.

As explained here: https://tour.golang.org/methods/6

So back to the orignal question: consistency of pointer methods. If you read the faq more carefully, you will find it is the very last part of considering to use a value or pointer methods. And you can find counter-example in standard lib examples, in container/heap :

// A PriorityQueue implements heap.Interface and holds Items.
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
    // We want Pop to give us the highest, not lowest, priority so we use greater than here.
    return pq[i].priority > pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index = i
    pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    item.index = -1 // for safety
    *pq = old[0 : n-1]
    return item
}

// update modifies the priority and value of an Item in the queue.
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
    item.value = value
    item.priority = priority
    heap.Fix(pq, item.index)
}

So, indeed, as the FAQ say, to determine whether to use a pointer methods, take the following consideration in order:

  1. Does the method need to modify the receiver? If yes, use a pointer. If not, there should be a good reason or it makes confusion.
  2. Efficiency. If the receiver is large, a big struct for instance, it will be much cheaper to use a pointer receiver. However, efficiency is not easy to discuss. If you think it is an issue, profile and/or benchmark it before doint so.
  3. Consistency. If some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used. This, to me, means that if the type shall be used as a pointer (e.g., frequent modify), it should use the method set to mark so. It does not mean one type can only have solely pointer methods or the other way around.
leaf bebop
  • 7,643
  • 2
  • 17
  • 27
  • I fail to understand how your example demonstrates my claim. S() is not a method (i.e., a function with receiver, per GO's jargon) - it is a plain function and as such the type of parameters is explicit. On the other hand, for the String() _methods_, I can invoke `t.String()` as well as `(&t).String()` - both will work. However, fmt.Println(t) works as expected while fmt.Println(&t) does not (and vice-versa for p). – ElazarR Jan 17 '18 at 12:02
  • Anyway, I embrace the example of container/heap as it demonstrates that the 3rd rule (Consistency) is weak. Especially when considering the lack of `const` method modifier in GO (const method, as in C++), I would prefer avoiding passing a pointer to so many methods that do not mutate the data just to be "consistent". This would also save me from the problem of the receiver type for `String()` - I would keep it by-value even if other methods are by-pointer. – ElazarR Jan 17 '18 at 12:06
  • @ElazarR You should notice that `fmt.Println` is a plain function, not a method. The point is, `interface` is influenced (and `S()` does not take a fix type of parameter but an interface, just very simple in this case). – leaf bebop Jan 17 '18 at 12:12
  • I agree that `fmt.Println` is a plain function, however its implementation would invoke the `String()` method defined for the type of its argument. However, since there can be only one version of the `String()` method for a given type (by-value or by-pointer) then when `Println` is given the other "mode" of the argument it would invoke the default `String()` method for that data type (e.g., of a Struct) instead of the customized one. This is not the case for normal method invocation, where one can be sure that the respective method would be invoked for either pointer or a value receiver. – ElazarR Jan 17 '18 at 17:07
  • @ElazarR Nope, that is not how fmt work. `Println` take a `interface{}` and do type assertion. It first try to cast it into a `Stringer` interface, and like the example, it may fail. After failure it use reflect to do some complicated stuff to use the "default" way. It is the same as the example, no magic there. – leaf bebop Jan 17 '18 at 17:13
  • I understand that. My complain is about the "default" way. I expected that just as the compiler knows to convert a method invocation based on a value to a method invocation by pointer for a type just because it has a method defined by-pointer (and vice versa), "fmt" should check both ways too before falling to the "default" String(). – ElazarR Jan 17 '18 at 17:17
  • @ElazarR It would not make sense to do that. You pass a value into a function, golang will make a copy of the value. And a pointer type has the potential to change the value. If the compiler modify it through an interface to access the pointer method, it will cause confusion of behavior. – leaf bebop Jan 17 '18 at 17:21
0

The previous answers here do not address the underlying issue, although the answer from leaf bebop is solid advice.

Given a value, you can in fact invoke either pointer or value receiver methods on it, the compiler will do that for you. However, that does not apply when invoking via interface implementations.

This boils down to this dicussion about interface implementations.

In that discussion the discussion is about implementing interfaces with nil pointers. But the underlying discussion revolves around the same issue: when implementing an interface you must choose the pointer or the value type, and there will be no attempt by the compiler, nor can there be any attempt in golang code, to figure out exactly what type it is, and adjust the interface call accordingly.

So for example, when calling

fmt.Println(object)

you are implementing the arg of type interface{} with object of type X. The fmt code within has no interest in knowing whether the type of object is a pointer type or not. It will not even be able to tell without using reflection. It will simply call String() on whatever type that is.

So if you supplied a value of type X, and there just so happens to be a (*X) String() string method, that does not matter, that method will not be called, it will only type-assert whether that type X implements Stringer, it has no interest if type *X asserts Stringer. Since there is no (X) String() string method, it will move on. It will not attempt to check what X may happen to be, whether it's a pointer type, and if not, whether the associated pointer type implements Stringer, and call that String() method instead.

So this is not really a pointer vs value methods issue, this is an interface implementation issue when implementing interface{} in calls to fmt methods.

Sean F
  • 4,344
  • 16
  • 30