1

I am trying to follow the Go proverb "Make the zero value useful" but run into an issue with private fields on my struct that are by default nil (like map). If the zero value of my struct is to be useful, at some point it needs to initialize the private field. Consider the following situation in which I would like to do away with the constructor for Response, and make the zero value of Response useful. Obviously the field bag (which is a map[string]interface{}) needs to be initialized. I try to initialize the field bag when the getter Bag() is called on Response, which works, and allows for then calling Set() on the bag to set a value. But somehow when the response ends up back in the main function, my value gets lost. I believe the problem is that the initialization that I came up with replaces the pointer on a copy of Response, instead of changing what the pointer points at. So the old pointer still points to nil, but I am not sure. How to initialize the private field correctly without the constructor?

func main() {
    response := getResponse()
    number := response.Bag().Get("NiceNumber")
    fmt.Println(response.Name, "tells me that your nice number today is:", number)
}

func getResponse() (response Response) {
    response = newResponse() // I am trying to do away with this line.
    response.Name = "MyResponse"
    response.Bag().Set("NiceNumber", 100)
    return
}

type Response struct {
    Name string
    bag  Bag
}

func (r Response) Bag() Bag {
    // I have added this piece of code as an attempt to initialize r.bag
    if r.bag == nil {
        r.bag = Bag(make(map[string]interface{}))
    }
    // ---
    return r.bag
}

type Bag map[string]interface{}

func (b Bag) Set(name string, value interface{}) {
    b[name] = value
}

func (b Bag) Get(name string) interface{} {
    return b[name]
}

// This constructor would be deleted if possible.
func newResponse() Response {
    return Response{
        Name: "",
        bag:  Bag(make(map[string]interface{})),
    }
}

Output now is MyResponse tells me that your nice number today is: 100, and when I remove the call to the constructor of Response, the output is MyResponse tells me that your nice number today is: <nil>.

Bazzz
  • 26,427
  • 12
  • 52
  • 69
  • 3
    *"I believe the problem is that the initialization that I came up with replaces the pointer on a copy of Response, instead of changing what the pointer points at."* -- Yes, you're correct. If you change the method do have a pointer receiver instead, i.e. `func (r *Response) Bag() Bag {`, it should get you the result you expect. But keep in mind that, since `Bag()` modifies its receiver, if your code is passing around `Response` and not `*Response`, it will still end up with inconsistent state. – mkopriva Apr 15 '21 at 08:30
  • That will probably work, but it does not help making the zero value of `Response` useful, namely `Response{}` still does not work correctly. Also it changes the signature of the getResponse() function, which I prefer to avoid if at all possible. – Bazzz Apr 15 '21 at 08:36
  • 2
    I think for complex scenarios like this a _constructor_ is the best way to go: https://stackoverflow.com/questions/27553399/golang-how-to-initialize-a-map-field-within-a-struct or as @mkopriva says, change the methods to accept a ptr receiver – Christian Apr 15 '21 at 08:40
  • @mkopriva I just tried it the way you say and it seems to work, but I do not understand why. How can I call the getter `Bag()` on a non-pointer variable of Response? Could you formulate your suggestion into an answer with a bit of explanation, so I can accept? – Bazzz Apr 15 '21 at 08:41
  • @Bazzz the compiler (or runtime? I forget) does that automatically. – mkopriva Apr 15 '21 at 08:42
  • @mkopriva "the compiler (or runtime? I forget) does that automatically" WHAT?? I thought one of Go's slogans is not to do things autoMAGIC. – Bazzz Apr 15 '21 at 08:44
  • @Bazzz see [Calls](https://golang.org/ref/spec#Calls): 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()`**. – mkopriva Apr 15 '21 at 08:47
  • Sometimes sync.Once can help in such tasks. But well, sometimes a constructor functions is the best option. – Volker Apr 15 '21 at 09:05

1 Answers1

1

You can change the Bag() method's receiver to a pointer.

func (r *Response) Bag() Bag {
    if r.bag == nil {
        r.bag = Bag(make(map[string]interface{}))
    }
    return r.bag
}

Note that you can still invoke a pointer-receiver method on a non-pointer value as long as the value is addressable. See the docs on Calls:

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()

mkopriva
  • 35,176
  • 4
  • 57
  • 71