Go tip of the week: 3 Go anti-patterns

One of the Go proberbs says:

Clear is better than clever.

Trying to write clever code always backfires when others have to maintain your code (where “others” include your future self four weeks from now). Yet, I continue to see “clever” coding being suggested as cool trick or advanced programming. Here are three “clever Go tricks” that are, in fact, anti-patterns.

Anti-pattern: Dot imports

How dot imports work

Dot imports let you import a package and use its exported identifiers without prefixing them with the package name.

So if you import fmt as dot import, you can call Println() and friends without the fmt. prefix.

import . "fmt" 

func main() {
	Println("Look ma! no prefix")
}

Why this is an anti-pattern

The identifiers exported from this package appear as if they are defined in the current package. This is bad in two ways:

  1. Readers get confused while searching through source files looking for a definition of the identifier in the current package.
  2. Some packages have drop-in replacements that export the same identifiers. If those drop-in packages are imported through a dot import, readers may, after falling victim to problem #1, start thinking that the identifiers belong to the original package where in fact they belong to the drop-in package.

So if you get asked in your next job interview about the advantages of dot imports, tell them there are none.

Anti-pattern: Chaining

How chaining works

If a method updates its receiver, and nothing can go wrong with this update, the method does not need to return anything. Multiple such methods are called one after the other:

type num int

func (n *num) Add(delta num) {
	*n = delta
}

func (n *num) Mult(m num) {
	*n *= m
}

func main() {
	var n num = 6
	n.Add(4)
	n.Mult(3)
	//...
}

Too boring for cl3vr c0drs! Let's chain the methods together! We only need to have each method return its receiver. Then we can dot-call a second method on the result of the first method:

type num int

func (n *num) Add(delta num) *num {
	*n = delta
	return n
}

func (n *num) Mult(m num) *num {
	*n *= m
	return n
}

func main() {
	var n num = 6
	n.Add(4).Mult(3)
	fmt.Println(n)
}

Whoa, we've saved a line!

Why this is an anti-pattern

Three reasons:

  1. We have not gained anything. Really, chaining does not give us anything we need and don't have already.
  2. Chained methods cannot return errors. So if you have errors, you need to fall back to the normal pattern. Now you have two different styles in your code: chaining and normal.
  3. Go code prefers to keep the reading focus to the left edge of the code. This means short lines, and few nesting levels (ideally only one). Method call chains can easiy travel across the whole screen and even cause a line break that looks ugly and makes the code even harder to read.

Method chaining is unidiomatic Go and should be avoided.

Anti-pattern: Defer as initialization

Pop quiz: What does the following code print?

func mysteriousDefer() func() {
	fmt.Println("f runs")
	return func() {
		fmt.Println("func returned by f runs")
	}
}

func main() {
	defer mysteriousDefer()()
	fmt.Println("main runs")
}

main calls mysteriousDefer() as a deferred function, so everything that mysteriousDefer() prints should be printed after main has done its prints.

However, if you run this code, you'll get the following output:

f runs
main runs
func returned by f runs

So one part of the deferred function runs before main!

Why this is an anti-pattern

This “clever trick” allows things like putting initialization and cleanup into one function.

Did you notice the double parentheses in the defer call?

	defer mysteriousDefer()()

You typically would call a deferred function like so:

	defer mysteriousDefer()

The second pair of parentheses calls mysteriousDefer() before it is evaluated by the defer directive. Then mysteriousDefer() prints its message and returns another function, which is the actual function to be deferred.

This convoluted mess of immediate function calling, function returning and deferring a function is hard to grok, and the double pair of parentheses is really easy to miss when glancing over the code.

What's wrong with writing out an initialization function and a cleanup function?

Conclusion

If you learn a new language, it is just too natural (and completely fine!) to be curious and explore every feature of the language, to see how it works. It is also natural to carry over the patterns that you learned in your previous language. But every language has its specific rules and idioms, for a reason. Patterns taken over from other languages may not work well in Go and only lead to convoluted code that is hard to understand and harder to maintain.

Remember the Go proverb cited above, and strive to deliver clear code. All the readers of your code, including your future self, will thank you.