Go best practices: readability

I’ve been writing software in Go professionally in big companies for 4 years now, and after all that time I’ve seen a lot of projects and read a lot of articles about software development in Go. On this post I’ll talk about some of the things I consider the best Go practices, most of which can relate to other languages too.

One thing I’ve noticed is that sometimes there’s a gap between academic software and industry software development, but it’s necessary understand and adapt the best practices to write the best software possible.

But what defines the best software possible? On this post I’ll focus on readability, which helps productivity and maintainability, some core characteristics of good software.

Readability

“Programs must be written for people to read, and only incidentally for machines to execute.” Hal Abelson and Gerald Sussman, Structure and Interpretation of Computer Programs

“The most important skill for a programmer is the ability to effectively communicate ideas.” Gastón Jorquera

“Obvious code is important. What you can do in one line you should do in three.” Ukiah Smith

One of the most important characteristics that make me love Go is the simplicity, which really helps with readability, but just using Go isn’t enough to write readable code, there are some things that should be taken into account.

Naming and identifiers

The first way to achieve a good readable code is to write good names.

“Good naming is like a good joke. If you have to explain it, it’s not funny.” Dave Cheney

A good name must be:

  • Concise: must be small and easy to read
  • Descriptive: must describe its purpose (careful not to confuse with contents that are unnecessary)
  • Predictable: it should be possible to tell what something is just by reading its name (being “idiomatic” helps, more on that later)

First of all, some Go code conventions:

  • Use CamelCase
  • Uppercase describes public and lowercase private

A good name uses all language resources and also your code architecture to help describe it.

The length of a name should be proportional to the “distance” of its use

For example:

for _, i := range items {
	fmt.Print(i.Name)
}

The only use of i is one line after its declaration, so it’s ok to write a small name.

var ErrInvalidUser = errors.New("user is invalid")

func MyFunc()error {
	// ...

	return ErrInvalidUser
}

Now ErrInvalidUser is a public exported error that some function returns and can be used anywhere in the code, so it should be more descriptive. Reading it tells you that it’s an error and it’s about the user being invalid.

Package names

Package names should describe what the package is about in a simple way. To help describe that, check out some Go internal packages:

  • math
  • encoding
  • net
  • errors

If, for example, you wanted to find a method to calculate a square root, it’s really easy to guess where that would be. Also it helps readability when a package has a good name like:

import "net/http"

http.StatusOk

Really generic packages should be avoided, they don’t really describe anything and their contents should be in specific packages. Some example of bad package names are: utils or helpers.

Interface names

In general, interface names use suffixes like “er” / “or”, that helps describe the expected behavior. Some good examples are Marshaller, Writer, Reader , just by reading the name you can guess what it does. It’s important to note, though, that this is not mandatory, in some cases such as UserStorager, UserDatabaser the name seems kind of weird, UserStorage or UserService would be more easy to read, still being descriptive.

Function and method names

The best practice for writing function and method names is to think of the name as the whole definition, that helps removing unnecessary information that would make the name verbose and the code less readable. For instance:

func ParseNameStringToParsedNameString(name string) string {}

func (u users) GetUserByName(name string) (*User, error) {}

Note that if you read the whole definition, some parts of the name are describing already known information, we can simplify that to:

func Parse(name string) string {}

func (u users) Get(name string) (*User, error) {}
  • Note

    There’s a rule that even the Go team suggests when writing method names that have types on them like: .ToString() or .ConvertToInt() , they can be simplified to just .String() or .Int().

Be Idiomatic

There is a rule that should be taken into consideration before all others, and that is being idiomatic. Being idiomatic means using patterns that are well known by all the programming community such as:

for i, user := range users {
	fmt.Printf("user index %d", i)
}

In this case, i is used, not only in go, bu in all languages to describe the index while going through a loop. Using another name, as descriptive as you can imagine, could make the code less readable, that is because the reader is so familiar with the i notation and is probably expecting it.

Besides those well known patterns, there are some that the Go community uses that really help to read other people’s code. Some can be found inside the Go source code or on open source code, see one example:

var tt []Thing
for _, t := range tt {
	// ...
}

Here the double first letter identifies that the variable in question is a slice / array of elements.

Comments

“Comments are usually created with the best of intentions, when the author realizes that his or her code isn’t intuitive or obvious. In such cases, comments are like a deodorant masking the smell of fishy code that could be improved.” Comments code smell

The use of comments in code, in most cases, is dispensable. A good, readable code does not need comments to describe anything. If it seems like your code needs a comment, maybe you should think about the way it was written.

In my opinion there are 2 cases that comments are necessary:

  1. Public libraries and clients that will be used by other programmers. They should be used like this:
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error) {}
  1. When you or your team make a decision that is needed but counterintuitive, in that case a comment will help explain why that decision was made. For example:
func myExample(){
	// this needs to be this way because bla bla bla ...
	counter intuitive code
}

How do I make sure all that is being done?

1. Read a lot of Go code

Make sure to always read Go code and open source code written in Go to search for good patterns and ideas to make your code more readable.

2. Use tools

Use linters and IDE plugins to help catch some errors.

3. Code review

Peer code review is one of the key parts of developing not only readable code, but resilient and error-proof code. Although that sounds counterintuitive, it also makes development process faster, because the developers can focus on writing code more when they don’t need to revert and fix production errors. It also accelerates on-boarding process and developer development.

4. Focus on simplicity

Always write the most simple solutions, that helps, not only readability, but maintainability and developer happiness overall.

Read more