Newbie question: When to return a pointer?


(Kelly Ellis) #1

I understand the recommendations around using pointers vs. values as method receivers, but what about return values? If the returned type doesn’t need to satisfy another interface (i.e. it’s a plain struct that just contains some data), is it better to return the value or a pointer? It seems somewhat arbitrary, as the caller can just reference/dereference as needed.

A colleague made the following suggestion:

Let’s say you try to create a post via CreatePost but an error occurs. It returns the error and an empty Post object. One could conceive a world in which the error was not checked, the code goes about its business assuming the Post object is valid, and the programmer is spending their time trying to figure out what’s up further down the control flow. It gets worse if you have fields that are pointers.

This is internal code and we make sure to always handle errors, so this wasn’t very convincing to me.

The particular use-case is reading a User from the database:
GetUser(userID string) (api.User, error) vs. GetUser(userID string) (*api.User, error)

I did notice that constructors tend to return pointers.


(Matt Silverlock) #2

In my experience this isn’t always clear-cut, but I usually apply a few rules (assuming structs here):

  • Is the struct reasonably large? Prematurely optimising is rarely good, but if you have a struct with more than a handful of fields and/or fields containing large strings or byte arrays (e.g. a markdown body) the benefits to returning a pointer instead of a copy becomes more apparent.
  • What’s the risk of the returning function mutating the struct (or object) after it returns it? (i.e. some long-running task or job)
  • I nearly always return a pointer from a constructor as the constructor should be run once.

Most of my datastore-related functions return a *model.User (which has ~5 fields). My HTTP handlers return a HTTPError (a copy; 2 fields) as returning a pointer in that case is less useful.


(William Kennedy) #3

I have a bit of a different philosophy. Mine is based on making a decision about the type you are defining. Ask yourself a single question. If someone needs to add or remove something from a value of this type, should I create a new value or mutate the existing value. This idea comes from studying the standard library.

http://www.goinggo.net/2014/12/using-pointers-in-go.html


(Nate Finch) #4

Note that strings are basically slices, which are basically pointers, so copying them is super cheap, regardless of the size of the string.

I like William’s point in his blog post - you very rarely need to worry about the performance. Usually, just use what makes sense otherwise.


(Marcus Olsson) #5

There are already some good answers so I’m just going to add to William’s post from a DDD perspective.

I’ve found that entities and value objects from Domain Driven Design maps pretty nicely into Go. If you are interested in the whole lifecycle of an object as it changes, you’ll likely want to mutate it along the way and pointers would probably be the way to go.

If you’re only interested in the actual value of the object however, then it doesn’t matter which instance you use. The commonly used example is a dollar bill where you don’t care if you switched it for another as long as it’s got the same value.

This also plays nicely into using receivers as well where you probably want pointer receivers on entities and value receivers on value objects (since the latter ones are conceptually immutable).

type Cargo struct {
    TrackingID  string
    Itinerary   Itinerary
    Destination Location
    ...
}

func (c *Cargo) Reroute(l Location) {
    c.Destination = l
}

func NewCargo(id string, origin, dest Location) *Cargo { ... }

type Itinerary struct {
    Legs []Leg
}

func (i Itinerary) Empty() bool { ... }

func NewItinerary(legs []Leg) Itinerary { .... }

Here, the Cargo struct is an entity that will change along the lifetime of the cargo from origin to destination as it gets handled. An itinerary on the other hand is only used as a value, since two Itinerary with the same set of legs would be interchangeable. Itinerary might even have a method func (i Itinerary) AddLeg() Itinerary which has an immutable feel to it.

If you’re writing network infrastructure, this will probably be a bit contrived. At its core though, it basically comes down to what has already been said; if you’re interested in mutating the instance over its lifecycle then pointers would be your choice.


(George Calianu) #6

@kelly, Steve Francia explain here when you should use pointers or values.


(Sean Kelly) #7

I typically boil it down to:

Is the method mutating the original object? Pointer
Is the method intended to return a new copy, with or without changes? Non-pointer.

There’s probably a smarter way to distinguish, but that’s my usual approach


(Kelly Ellis) #8

Are there any thoughts about using a pointer to express nil-ability? Or is that a code smell? Using my same example of GetUser(id string), returning nil would be an indication there’s no such user with that ID. It is semantically different than returning an “empty” User struct, which would have all of its non-pointer fields initialized to their default values. This makes it differentiate between a nonexistent user, vs. a user who’s chosen “” as their username (humor me and pretend that’s an acceptable product design, for illustrative purposes).


(Daniel Skinner) #9

that could be an indication but that relies on the context established by the code written. There’s cases in the standard library and many experimental packages that allow methods of pointer receivers to function even when nil.


(Matt Silverlock) #10

This is certainly pretty common across the std lib - plenty of cases where SomeFunc(arg1, nil) is acceptable - but where a Thing{} with zero-value fields would be ambiguous.

As for using it to indicate “no user for that ID” - that’s probably an API design argument. You could return nil, nil and then require the caller to check if user == nil { // raise 404 } or return ErrUserNotFound and inspect the error within your if err != nil { // check possible error types/strings } block.

(I lean towards the latter, for what it’s worth!)


(Sean Kelly) #11

To add to that last point, you likely would not want to return a value that could be mistaken for a User if one was not actually found. So while I agree that the right approach there is to the return and check the error, I would also say it’s correct to return a nil user (not a 0-value user) to it crystal clear.

That’s how I design my APIs anyways.


(George Calianu) #12

In your particular example is better to use pointers. nil if the user not exist or a pointer to the user structure if found. Also, is not a good practice to return struct type variables.


(Andrey Petrov) #13

Are there any thoughts about using a pointer to express nil-ability? Or is that a code smell?

I found that while it appears to suffice in isolated cases, I’ve generally grown to regret using a nil interface rather than an (interface, error) return tuple to indicate failure or absence.

I suspect this is for a few reasons:

  1. If you always check an error value for failures of expected flow, your code becomes a lot more consistent than when you need to toggle between checking errors and checking value nils. Deviations become meaningful and noticeable.

  2. A nil interface is still a usable construct. In fact, you can call methods on a nil interface as a receiver! Therefore, it’s not always safe to assume that nil == failure/absence, it can have more nuanced meanings in context.

  3. Much of the time I write code, it starts as simple binary conditions (is it there, or is it not?) but it almost always evolves into more complicated scenarios (e.g. it’s there but expired) which might be valuable for me to return a filled interface value but also an error.


(Zev Goldstein) #14

Go has the “comma OK” idiom and it is preferred over returning null in cases like this. I fully agree with the accepted answer from this stack overflow post asking the same question.


(system) closed #15

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.