Meta trips

Consider functions instead of methods


If you coming from an object oriented programming language you might be inclined create a bunch of methods when working with Go.

In this post I’d like to show how the same can be achieved with simple functions, leading to a better API with smaller packages.

Let’s have a look at a simple method, defined on a struct. BTW: As long as the type is declared within the same package, you can define methods on any type, not just structs.

type Car struct {
	horsePower int
}

func (c *Car) Drive() {
	sound := "whoooaaa..."
	if c.horsePower < 40 {
		sound = "tuck tuck tuck..."
	}
	println(sound)
}

But you could just define it as function, if you like:

type Car struct {
	horsePower int
}

func Drive(c *Car) {
	sound := "whoooaaa..."
	if c.horsePower < 40 {
		sound = "tuck tuck tuck..."
	}
	println(sound)
}

You might wonder why this works, horsePower is a private attribute, after all.

Well Go is bit different from, say, Java, where you probably would have written:

public class Car {        
    private int horsePower;
}

While in this code, horsePower is encapsulated inside the object, in Go you can think of it more in terms of visibility across packages.

So in Go lowercase types, attributes, methods, variables, constants and functions can only be used inside the package in which they are defined. To export them to other packages, the first letter has to be uppercase.

To turn a method into a function, all you have to do is, change the line of the declaration, here from func (c *Car) Drive() { to func Drive(c *Car) {.

Note however that you can have only one Drive function per package. So if for example you wanted to also define a Drive function for the Truck struct, that would not be possible and you would have to resort to function names like DriveCar and DriveTruck.

Also the invocation with (Car{}).Drive() looks prettier and more familiar than DriveCar(&Car{}).

And some small annoyance: While you can invoke methods defined on a pointer of a type via the type directly, you have to make sure to pass a pointer in the case of a function.

Transformation of a method to a function is not always the best thing to do. When the method really belongs to the object (like Drive) you should go with a method.

However, often you want some functions just to support your types: nice comfort or intergration functions that strictly do not belong to the type. Then functions are the way to keep your type lean and clean.

When is it better to use functions instead of methods?

If you are in doubt, I would say, start with a function.

Then as you go along, try to separate core functions from supporting helper functions. Try to minimize the core functions and to refine your library in a way that you could move the helper functions to a different package, without exposing unneccessary details.

As your code matures you should be able to tell, which functions really should be methods. Most if not all of your remaining core function will and should be methods, so transform them.

What is the benefit?

  • If you provide useful optional helpers, the user of your library might want to do this as well. So he needs as much details (attributes, methods) of your objects and you will want to export them.

  • On the other hand, keeping the core package as small as possible, allows a faster path to stabilization and less breakage for further development.

  • The supporting helper packages will not stabilize that faster, but that is ok as they are just helpers.

  • Tell the users that they might expect less breakage from the core package than from the helpers and they can adapt to the situation.

Also this procedure forces you to reason about what is really the core early on, which should result in a better and more decoupled design.

What about the Go package hell, go get and friends?

Since your core package and the helper package(s) all belong together, put them in the same repository, in different directories. This way there would be no dependency hell within your library packages, since the go tool handles getting and updating of packages on a repository level, that means, the whole repo with all subpackages is fetched or updated.

What about interfaces and methods?

Say, we want our Car to have a Brand and to fulfill the fmt.Stringer interface, returning the Brand as comfort for the users of fmt and friends.

Here is what it looks like with a method:

type Car struct {
	Brand string
}

func (c *Car) String() string {
	return c.Brand
}

While it works reasonably well, there is nothing inherent to a car that let it act as a Stringer. It is more of a comfort to be able to have a string representation of a car - that smells like helper.

What about this?

type Stringer Car

func (s Stringer) String() string {
	return Car(s).Brand
}

Ok, it is a bit more work for the user: Instead of Car{Brand: “BMW”} in order to get a Stringer he would now have to use Stringer(Car{Brand: “BMW”}).

But please note, that it is also more explicit.

This way your car might not unintentionally get used as a Stringer without being noticed. That might or might not what you want - that depends on, if the interface in question is a core part of your car (then write a method), or if it is just a nice accessory (like Stringer).

Consider the following function using your library


func TakeAnything(things ...interface{}) {
	for _, thing := range things {
	    switch := thing.(type) {
	        case Stringer:
	        	// do things specific to stringers
	        case Car:
	        	// do things specific to cars
	    }   
	}	
}

If Car itself fulfills the Stringer interface, the type switch is now dependent on not just the types, but also the order of the case statements. It is easy for your user to get it wrong accendentially. He might not expect the car to be a Stringer or he looked it up at a point in time, where your car library did not fulfill the Stringer interface.

And then he did an update - and bam! all of the sudden, Car now is a Stringer and TakeAnything behaves different now.

Therefor it is worth considering adding a type wrapper as mentioned for “nice to have” interface fulfillments and put them into your helper packages.

Summary

Usage of package level functions and type wrappers could provide a good alternative to methods when extending your base types with helpers.

This leads to smaller packages that are more modular and easier to support.

Updates

Rob Pike uses a similar idea. He uses the functions as options for initialization.

type option func(*Foo)

// Option sets the options specified.
func (f *Foo) Option(opts ...option) {
  for _, opt := range opts {
     opt(f)
  }
}

Dave Cheney points out the benefits of such an approach.

comments powered by Disqus