Back to shazow.net

Neither self nor this: Receivers in Go

When getting started with Go, there is a strong temptation to bring baggage from your previous language. It’s a heuristic which is usually helpful, but sometimes counter-productive and inevitably results in regret.

Go does not have classes and objects, but it does have types that we can make many instances of. Further, we can attach methods to these types and they kind-of start looking like the classes we’re used to. When we attach a method to a type, the receiver is the instance of the type for which it was called.

Choosing the name of a receiver is not always a trivial task. Should we be lazy and name them all the same (like this or self)? Or treat them not unlike local variables by abbreviating the type (like srv to a Server type )? Or maybe something even more nuanced?

And what are the consequences? How will our code suffer if we choose one approach over the other? Let’s explore.

Quick refresher on Go structs

type Server struct {
    ...
}

func (srv *Server) Close() error {
    ...
}

func (srv Server) Name() string {
    ...
}

Every time I start writing somekind of server, it starts out looking like this. It’s a type that holds information about our server, like a net.Listener socket, and it has a Close() method that shuts down the server. Easy enough.

The receiver of the Close() method is the (srv *Server) part. This says that inside of the Close() method declaration, the scope will have a srv variable that is a reference to the instance of the Server that it’s being called on. That is:

myServer := &Server{}
myServer.Close()

In this case, the srv that is referenced inside of the myServer.Close() is effectively the same variable as myServer. They’re both references to the same Server instance.

Facts about Go methods and receivers

While we can call a method on a type instance and get the receiver implicitly, it can also be called explicitly:

myServer := &Server{}
Server.Name(myServer) // same as myServer.Name()
(*Server).Close(&myServer) // same as myServer.Close()

These functions can be passed around as references just like any other function, with the implicit receiver or without:

withReceiver := myServer.Name
without := Server.Name

Receivers can be passed by reference or passed by value.

func (byValue Server) Hello() { ... }
func (byReference *Server) Bye() { ... }

This is to illustrate that struct methods in Go are merely thin sugar over traditional C-style struct helper declarations. An equivalent C method might look like this:

void server_close(server *srv) { ... }

Go helps by namespacing the methods and implicitly passing the receiver when called on an instance, but otherwise there is very little magic going on.

In other languages where this and self is a thing (Python, Ruby, JavaScript, and so on) it’s a much more complicated situation. These are not vanilla local variables wearing fancy pants. The thing we might expect this to represent inside of a method could actually represent something very different once inheritance or metaclasses had their way. In effect, it might not make any sense to give contextual names like srv rather than self in Python, but it definitely makes sense in Go.

Naming the receiver

As we write idiomatic Go code, it’s common to use the first letter or a short abbreviation as the name of the receiver. If the name of the struct is Server, we’ll usually see s or srv or even server. All of these are fine—short is convenient, but it’s more about uniquely identifying the variable in a consistent way.

Why not self or this? Coming from languages like Python, or Ruby, or JavaScript, it’s tempting to do something like:

func (this *Server) Close() error {
    ...
}

That’s one less decision to make every time we declare a struct. All of our methods could use the same receiver. Any time we see this in the code, we’ll know that we’re talking about the receiver, not some random local variable. It will be GREAT!.. or will it?

What if we refactor the code and this is no longer referring to the same thing as before? And are we giving up valuable semantic meaning?

Reshaping our code

Eventually we’ll need to refactor some of our code: Take a chunk of code that is already functional and put it in another context where it allows for more flexibility towards a higher-level goal.

For example, consider moving pieces of code between levels abstractions or from higher levels of abstraction into a lower level. Imagine taking this snippet from a higher-level container like Room which holds groups of users in a server, and moving it up or down one level:

func (this *Room) Announce() {
    srv := this.Server()
    for _, c := range srv.Clients() {
        // Send announcement to all clients about a new room
        c.Send(srv.RenderAnnouncement(this))
    }
}

// Moved between...

func (this *Server) AddRoom(room *Room) {
    for _, c := range this.Clients() {
        // Send announcement to all clients about a new room
        c.Send(this.RenderAnnouncement(room))
    }
}

When using this, there is confusion about whether we’re referring to the server or the room as we’re moving the code between.

-       c.Send(this.RenderAnnouncement(room))
+       c.Send(srv.RenderAnnouncement(this))

Refactoring this kind of code produce some bugs that the compiler will hopefully catch (or maybe not, if the interfaces happen to be compatible). Even bugs aside, having to edit all the little innards does make moving code around more tedious.

Moving across levels of abstraction is a great example of when consistently well-named receivers make a huge difference:

func (room *Room) Announce() {
    srv := room.Server()
    for _, c := range srv.Clients() {
        // Send announcement to all clients about a new room
        c.Send(srv.RenderAnnouncement(room))
    }
}

// Moved between...

func (srv *Server) AddRoom(room *Room) {
    for _, c := range srv.Clients() {
        // Send announcement to all clients about a new room
        c.Send(srv.RenderAnnouncement(room))
    }
}

This is a great little pattern to keep everything working despite moving between layers of abstraction. Note how the inner code stays identical and all we’re doing is sometimes adding a little extra context outside of it.

As projects mature, this kind of refactoring happens surprisingly often. We’re talking about just one line in this example, but the same applies for larger chunks too.

The suggested strategy for naming Go receivers is the same strategy for naming normal local variables. If they’re named similarly, then these code blocks can be moved wholesale between layers of abstraction with minimal hassle and helps us avoid careless bugs.

By naming receivers as this or self, we’re actually making receivers special in a way that is counter-productive. Imagine naming every local variable with the same name, all the time, regardless of what it represents? A scary thought.

Advanced naming technique

If we agree that contextually named receivers are meaningful, then maybe we can utilize this opportunity for an even greater advantage.

What if we named our receivers based on the interface that they’re implementing (if any)? Let’s say we add io.Writer and io.Reader interfaces to our Server:

func (w *Server) Write(p []byte) (n int, err error) {
    // Send p to all clients
}

func (r *Server) Read(p []byte) (n int, err error) {
    // Receive data from all clients
}

Maybe we also want to add the http.Handler interface to provide a dashboard for our server.

func (handler *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Render dashboard
}

There are a few benefits to doing it this way:

Name of the Receiver

Carefully naming our receivers can have lots of tangible benefits, especially as our project grows and code gets moved around. It can make our inner method code much more readable without needing to be aware of which struct it’s embedded into. It can even add an opportunity to indicate higher-level layout of our struct’s interface implementation.

Picking a fixed name for all receivers like self can have negative effects like mixing up context when code gets moved around. It removes a decision during writing, but the cost creeps up when we go back to read the code or refactor it.

Go forth and give your receivers the names they deserve.

Back to shazow.net