markhuge MH\n s

Understanding Go

published:

signed with 0x683A22F9469CA4EB

tags:

Gopher

A common complaint I hear from people coming to Go from another language, is that Go is “missing features” and requires “a lot of boilerplate”. I’d argue that this is a misconception stemming from the trend of languages becoming more prescriptive and bloated in the name of being “expressive”.

The Go programming language stands out from other modern mainstream languages because of the simplicity of its instruction set. Understanding the why of Go is key to understanding the how.

What’s the point of Go?

The designers of Go created the language with the specific goal of being simple, performant, maintainable and readable. It’s a language optimized for building large, maintainable systems.

Go prioritizes:

  • readability over “expressiveness” (although it can be quite expressive)
  • compute simplicity over syntactic sugar
  • freedom over prescription (in b4 “go fmt is fascism!")

In his talk Simplicity is Complicated, Rob Pike explains these design decisions explicitly:

“What do you want: a language that’s more fun to write in, or easier to work on and maintain? For the most part, the decisions in Go about what went in, were about long term maintenance, particularly in the context of large scale programming.”

Embracing Go’s idioms

To be successful with any language, you need to embrace its idioms.

Go is arguably one of the languages most free from ritual and boilerplate. This freedom can often cause confusion about “the right way™” to do things.

At the time of this writing, Go is at least a second programming language for almost everyone writing it professionally. A common mistake is for people to try and write Go as if it were one of their prior languages.

Composition

I often see devs treating struct as a stand in for class in other languages.

Go is not a hierarchical language. There are no classes or inheritance. Go shares functionality with composition. Composition lets us do things like creating tiny interfaces, and composing them as needed. A great example is the io.ReadWriteCloser interface:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

type ReadWriteCloser interface {
	Reader
	Writer
	Closer
}

There are no chains of inheritance to juggle. We have 3 single-method interfaces that we can pick and choose from to include the functionality we need. By accepting this interface, our code works with files, network sockets, buffers, and anything else that needs a cleanup operation after reading and writing bytes.

Error Handling

This section could easily just be a link to Failure is your Domain by Ben Johnson, but I’ll add my own commentary anyway.

A very common code smell in Go is endless blocks of error checks:

foo, err := checkLogin()
if err != nil {
  return nil, err
}

bar, err := getRecord()
if err != nil {
  return nil, err
}

baz, err := doAnotherThing()
if err != nil {
  return nil, err
}

First, let me acknowledge that this is totally valid and very readable. It’s very easy to see what’s going on. That said, this is returning an error, not error handling.

As Ben states in the opening paragraph of his post:

“Go’s paradox is that error handling is core to the language yet the language doesn’t prescribe how to handle errors. Community efforts have been made to improve and standardize error handling but many miss the centrality of errors within our application’s domain. That is, your errors are as important as your Customer and Order types.

An error also must serve the different goals for each of its consumer roles—the application, the end user, and the operator.”

In many languages, errors are represented by concrete types. Often these have stack traces, methods and other arcana attached to the error type. This usually necessitates the language be very prescriptive about the way errors are handled.

Error consumer roles have very different needs, and there’s generally a lot of jank (often with regex) around massaging the error output into something that each of the roles can consume.

In Go, errors are just values. The error type itself is just a single method interface:

type error interface {
	Error() string
}

In Go, you have the freedom to program your errors to do anything you want them to do. You don’t need to jump through any hoops to parse an error type, wrap it in anything else, or tapdance around extracting the information you need to handle the error. Any type that satisfies the error interface is an error.

For non-trivial programs, you should probably be creating your own error types.

Your error types should be as specific to your application’s domain as possible. If you catch yourself using the same boilerplate errors package in all of your projects, you’re likely missing the point, and should probably just use the errors package.

“Missing Features”

A friend who is a very experienced JavaScript developer, asked if Go slices have a method for checking to see if an index existed.

The example I gave was something like this:

index < len(arr) && index >= 0

You may be thinking “Why isn’t this built into the slice type like it is with lists in other languages?”

How often are you checking to see if index N exists? Is it often enough to justify attaching a method that does this to every slice in every program created in Go? Probably not.

If it’s something your particular use case needs, you could trivially create a custom type with this method:

type MySlice []int

func (m MySlice) IsIndex(index int) bool {
	return index < len(m) && index >= 0
}

Or you could easily do this with a function instead:


func IsIndex(index int, s []int) bool {
	return index < len(s) && index >= 0
}

Maybe it’s not enough to check that an index exists, but that it also has a valid value:

type MySlice []string

// IsIndex returns true if there is a non-zero value
// at index
func (m MySlice) IsIndex(index int) bool {
	if !(index < len(m) && index >= 0) {
		return false
	}

	// Check for zero value
	if m[index] == "" {
		return false
	}

	return true
}

The whole point here is that the simplicity of the language lets you solve this problem however best fits your use case. Go does not prescribe a set of sugar methods that force the programmer to use one pattern or another under the hood. Go gives you control of how your programs work, and makes it very easy to do.

With Go it’s not that you have to implement your own error handling and sugar. You get to control custom behaviors for your specific use cases for free. No parsing canned stack traces or having to factor in arcane under-the-hood performance trade offs of different expressive loop sugar.

As Rob says in Simplicity is Complicated:

“When you have features that add expressiveness, they typically add expense. For instance a lot of people have asked for things like maps and filters to be built into Go, and we’ve said no.

… When the features are there to make life easier and more expressive they tend to generate more [computationally] expensive solutions for problems that have much simpler solutions.”

This is not missing feature cope, it’s the whole point.

Conclusion & References

Hopefully this clears up some of the “why” around Go, and makes the transition less frustrating for people coming in from other languages.

About the Author

Mark Wilkerson

0x683A22F9469CA4EB

R&D engineer @ Twitch. Previously Blizzard, Hightail, Co-Founder @ SpeakUp

Mark is building open source tools to make web3 and decentralized self-ownership easy for regular people.

Live dev streams at Twitch.tv

More Posts