blue gopher with sunglasses giving two thumbs up
Created using OpenAI’s DALL·E 2

Features of Golang that I think are pretty neat

Gavin Killough
CodeX
Published in
10 min readMay 22, 2023

--

One of the things many Gophers, including myself, love about Go is its simplicity. With only twenty five reserved words, few repetitive language features (e.g. for-loop is the only type of loop), and syntax-light statements, Go minimizes the number of characters per line, while maintaining an exceptional level of readability. These qualities, along with its familiar C-style syntax make it an easy language to learn and quickly become productive within its ecosystem. Despite how lightweight and barebones it might feel, Go packs plenty of powerful and convenient features into its modest footprint. Some of these features are unique to Go, others are borrowed or inspired by other languages; regardless of where they come from, the following are a few of my favorites.

Structural Typing

Duck Typing gets its name from an old quote roughly paraphrased to, if it walks like a duck and it quacks like a duck, it’s probably a duck–a concept likely familiar to Python and Ruby developers. Go supports a similar concept called Structural Typing, which, put in terms of ducks, can be thought of as, if it walks like a duck, quacks like a duck, and compiles, then it’s an honorary duck. The way this manifests in Go is within interface semantics: any function or method which has the same structure (i.e. parameters and return types) as an interface “implements” that interface and can be used anywhere an instance of the interface is requested. The difference between the two is Structural Typing relies on compile-time — rather than runtime — compatibility checking, providing more safety than Duck Typing and lending developers confidence in their implementations before ever running the code.

A convenient side effect of Structural Typing is library cross-compatibility. Many libraries use interfaces with predictable (or at least common) signatures, meaning other libraries offering functions or methods with identical signatures can play nicely together without any external dependency defining said interfaces. To give an example, the most commonly used interfaces in the entirety of Go are likely io.Reader and io.Writer. Their signatures both demand a slice — Go’s dynamic array — of bytes, and return an int and an error. Any type, from any library, exposing a Read or Write method conforming to that structure can be used anywhere the corresponding interfaces are demanded. In fact, if I created a new interface Reader2 in my library and its Read method had the same signature, I could replace all references to io.Reader with Reader2 without breaking a single API contract.

By reducing the need for excessive layers of adapters between different modules, Structural Typing sets Go apart from many of its statically typed, object oriented language alternatives.

The _ (Blank) Identifier

Theorized to be inspired by a pattern-matching convention in Haskell, the blank identifier, _, in Go is a write-only placeholder used for explicitly ignoring a value. To those unfamiliar with the language, there are likely two questions front-of-mind upon learning of this feature. Firstly, what is the use-case for it? Secondly, why would it even be necessary?

I’ll answer the latter question first as it informs the answer to the former. The reason the blank identifier is necessary in Go is primarily because Go does not permit (i.e. won’t compile with) unused imports or unused local variables. This is an intentional choice from the Golang maintainers to increase code readability and cleanliness. Providing the blank identifier gives developers a way to explicitly express they don’t care about an identifier without bloating their code with a named, but unused one. Now let’s look at some examples of how to use it.

Go allows zero or more variables to be returned by a function, and in some cases, the caller of the function cares about fewer of the variables than are returned. Take for instance this function signature:

func getStatus() (code int, message string)

As a caller of this function, I may only care about the status code, while the error message is uninteresting to my use-case. Rather than create a dead variable for the returned message, I can use the blank identifier to indicate I don’t intend to consume it:

code , _ := getStatus()

A few of the many other ways to use the blank identifier include:

  • Importing a package for its side-effects
    Example: Importing a database driver needed only at runtime
  • Ignoring an unneeded parameter when implementing an interface
    Example: Creating HTTP-handling middleware which logs the request, but has no interest in accessing the also-provided response writer
  • Ignoring the index value of a range
    Example: for _, elem := range mySlice { // ...

One last example of this nifty blank identifier uses it to perform compile-time assertions of interface implementations. With structural typing, it’s sometimes important to ensure the implementation of an interface cannot later introduce a backwards incompatible change. The blank identifier can help solve this problem. For example:

var _ MyInterface = (*MyImplementation)(nil)

Here we are casting nil to a MyImplementation pointer, then assigning it to a global variable of type MyInterface. This code will only compile if and only if MyImplementation actually implements MyInterface. By using the blank identifier as the variable “name”, no allocations need occur and no “throwaway variable” need exist, ensuring we never accidentally break the interface contract nor leak unintended information.

As you can see, this single character is an incredibly versatile feature of Go, and I think it’s pretty neat.

Goroutines

No article about Go would be complete without mentioning goroutines and their important counterpart, channels. As one of the most popular topics in the Go community, there is more than enough information about goroutines as it is, so I’ll keep my thoughts brief.

The ability to spin off a concurrent process in two keystrokes might seem dangerous, but goroutines are highly performant; and with channels providing a mechanism to safely communicate among them, the risk of abuse is negligible compared to alternative concurrency models in most languages. Couple that with Go’s sync package–part of its standard library–and its capabilities in this domain far exceed those of its analogs while being much easier to use.

// Hey you!
go learnGo()

The Standard Library

One thing which immediately stood out to me in my early days with Go was how much I could do with just the standard library. Any time I searched for an answer or tutorial while working on my first few projects, there always seemed to be at least one straightforward way to accomplish my goals with the standard library. Even when a dependency was suggested, it was usually one of Golang’s tertiary or experimental libraries. The only reason such libraries aren’t already part of the standard library is because Go guarantees backward compatibility in every new version, and these additional tools aren’t at a point the Golang maintainers are ready to commit to a state of permanence.

Go’s out-of-the-box tooling stood in stark contrast to the languages I was previously most familiar with: Java, Javascript, and Python to name a few. Maintaining a webserver in vanilla Java is excruciating. Creating a new front-end in Javascript is unthinkable without the likes of React, NPM, Babel, Webpack, and handfuls of other tools. Data analytics in Python wouldn’t even be a consideration if not for Pandas and Numpy. This isn’t to say the external libraries of those languages are a bad thing, but they often come with additional costs and tradeoffs before any productive code can be written.

Contrast that with Go. Want to spin up an HTTP server? One line of standard Go. Need to support TLS? Add a line to load up your certificate and one or two more for preconfiguration and you’re done. Json support? Image manipulation? SQL connections? Easy. Compression, encryption, HTML templating, webassembly, and more are all right there. And even when the need or desire arises to reach for a third-party library, many module maintainers choose to use pure standard Go, resulting in the need for minimal–or even zero–transitive dependencies.

The benefits of minimizing third party dependencies are numerous. From managing license compliance and security vulnerabilities, to tightly coupling yourself to an ecosystem that may not serve the long-term needs of your app, using third-party dependencies can be both risky, and constraining. Again, there’s nothing inherently wrong with these external tools, but Go gives you the choice to defer committing to them, or forgo them altogether without requiring significant boilerplate in new projects to achieve what they offer. I love this about Go, and it’s what makes me reach for Go first whenever I need to solve a new problem.

One Tool To Rule Them All

…maybe most importantly, you have a default — a baseline — which means you can start more quickly and establish your project without needing to make additional decisions…

This next section was supposed to be an addendum to the last one rather than completely standalone, but it turns out I had a lot to say. Just like the amazing standard library of Go, you needn’t venture far, or really, anywhere else, to do all of the operations which other languages need two or three tools to even start to consider. To continue the contrast, let’s look at Java, JavaScript and Python again.

To manage dependencies, build, test, reformat and bundle code in each of the aforementioned languages in any efficient way requires downloading an additional tool and learning its syntax and nuances. In the Java ecosystem you’ll need to get familiar with Maven or Gradle, then learn how to navigate and configure the artifact repositories, then create a few dozen lines of a boilerplate script, and only then can you actually do anything with your Java source code. Javascript has a similar song and dance with NPM and perhaps Yarn as well. Python has a dozen and one ways to manage dependencies and virtual environments. Once you have these tools, don’t forget to download the corresponding version switchers as you juggle several versions of your several tools over several projects.

In Go, you have a few choices for managing and building, but you have a great default option built-in. go get your dependencies and occasionally tweak the versions in your go.mod file. go build or go install your project and you’ll have an executable ready at lightning speed. go fmt your source code to silence the majority of pedantic disagreements about style. Yes, you’ll still have to learn the commands and understand some of the semantics (maybe even set an environment variable like GOPRIVATE*gasp*), but like everything in Go these tools are designed to be easy, intuitive and minimalistic. Furthermore, and maybe most importantly, you have a default — a baseline — which means you can start more quickly and establish your project without needing to make additional decisions or configure five things before doing something productive.

Admittedly, this standard tooling wasn’t always available in Go, and like many languages that came before, it took years of growing pains to get where it is today. Also, to be fair to other languages, most developers who work in them every day have almost certainly streamlined these processes to be fast for themselves. What continues to set Go apart for me is how the extraneous parts stay out of the way until you absolutely need them, and when you do, they are serviceable if you stick with them, but lightweight enough to exchange if you need something else.

Defer Statements

Saving the best for last, defer might just be my favorite feature of the bunch. The defer keyword, followed by a function call pushes said call to a stack of functions, each guaranteed to run when the current function exits — regardless of how. This makes it a great way to recover from panics, clean up open resources, or handle other easily-forgettable tasks.

In normal procedural code, clean-up activities usually happen at the end of a function, regardless of how long the function is. Keeping logically coupled code in a different “physical” part of a file makes it more difficult to refactor the code later or abstract it away when necessary. Greater distance between the instigating operation and its eventual housekeeping also makes it easier to forget the latter part altogether; the bugs that come out of situations like this can be notoriously tough to track down. Lots of procedural languages attempt to solve this using a “try-with” concept, typically requiring additional nesting of code and a dedicated scope. The maintainers of Go give us defer to tackle such cases without any of the extra nesting, but also with the power to extend beyond leaky resources.

Here’s a simplified example of defer in action:

// error handling omitted for brevity
resp, _ := http.Get("https://httpbin.org/get")
defer resp.Body.Close()

bodyBytes, _ := ioutil.ReadAll(resp.Body)
fmt.Println("HTTP Response Body:", string(bodyBytes))

As soon as we have a response, we immediately can defer closing the body lest we forget to do it later which would cause a resource leak. This strategy creates a convenient, maintainable proximity between the lifecycle actions of the resource stream (open — performed by http.Get— and close).

One neat way I’ve seen defer used is to guarantee rollback on failed database transactions. The following code snippet is my rough memory of a utility created by a former coworker to do just that:

// error handling omitted for brevity
func DoInTransaction(db *sql.DB, fn func(*sql.Tx) error) {
committed := false
tx, _ := db.Begin()
defer func() {
if !committed {
tx.Rollback()
}
}()

err := fn(tx)
if err == nil {
tx.Commit()
committed = true
}
}

Code like this still empowers you to take fine-grained control over your transaction management, but defer provides a safety mechanism to minimize the risk of doing so.

The usage of defer needn’t be limited to cases like this. How about a concurrency safe data structure?

type ConcurrentMap[K comparable, V any] struct {
cMap map[K]V
mux sync.Mutex
}

func Put[K comparable, V any](cm *ConcurrentMap[K, V], k K, v V) {
cm.mux.Lock()
defer cm.mux.Unlock()

cm.cMap[k] = v
}

This is a basic example, but this pattern can extend to any more complex data structures in which failing to unlock the mutex in a given method would be devastating.

If you’re anything like me, the possibilities are already running through your mind of all the creative idioms defer could enable.

Go may not have the same aesthetic as some other high-level languages, but there’s something to be said for the procedural style and how concise and tight each line can be. The limited feature-set exposed by Go delivers practicality and simplicity in the resulting code, delicately balancing the (usually) conflicting forces of cleverness and maintainability. Even if you don’t find these concepts as neat as I do, I hope you can appreciate their utility within Go as a language, and maybe even garner some inspiration from them.

--

--

Gavin Killough
CodeX
Writer for

Gavin is a software developer who is always looking for ways to improve code quality and build expressive APIs.