What is that Golang thing, anyways?

Stephen Fox
.debug
Published in
8 min readOct 19, 2017

Searching for “just the right” language.

The Golang mascot, Gopher. Credit wikimedia.org

Golang is a powerful, easy to learn programming language created by engineers at Google that has soared in popularity in recent years. It is so popular, that botnet builders are using it! Today, I am going to discuss the case for Go — and hopefully help you break through some of the language’s eccentricities.

Goldilocks

Several months ago, I set out to create a library that would interoperate with other programming languages without creating a maintenance nightmare. It would need to be easy to learn for new comers, be well supported in development tools such as IDEs and dependency management tools, and be supported on multiple platforms with minimal dependency overhead.

Along the way, I read about Go and the passion that many programmers had for it. Three features in particular caught my eye:

  • A single project can be easily compiled to a single, static executable targeted for popular platforms such as Linux, macOS, and Windows
  • Like C# .NET and Java, Go relies on a runtime. However, a compiled executable does not require the runtime to be present. The executable is less than 10 megabytes on disk. The garbage collector also has a negligible impact on the application’s performance
  • Compartmentalizing code into libraries is easy, and is promoted by the compiler rejecting cyclic dependencies

While these features did not entirely solve my “perfect” library problem, they made a very strong case for exploring the language’s capabilities.

Initial eccentricities

Like all programming languages, Go has a learning curve and its share of non-obvious nuances. During my reading, I encountered both love and hate for many of Go’s qualities. In this section, I will discuss several non-obvious qualities that are especially important for newcomers to understand.

Projects are managed with ‘go get' and the GOPATH

After installing the Go runtime, you will need to create the GOPATH. This is the filesystem location where all of your Go projects should live. There is nothing stopping you from creating Go projects outside of the GOPATH. If you do this, you will quickly discover that many features of the ‘go’ command line tool will not work with your project.

You are expected to use the go command line tool to acquire existing Go projects. Under the hood, Go wraps around your source control management tool of choice. By default, Go assumes that the GOPATH is a directory named ‘go’, which lives in your home directory (~/go). You can change this by creating an environment variable of the same name, whose value is the desired location.

Existing Go projects are acquired using the go get <project-url> command. These projects are added to the GOPATH, in the structure of:

<GOPATH>/src/<repository-host.com>/<your-identity>/<project-name>

For example, running:

go get https://github.com/stephen-fox/power

… would result in the Git project being cloned to:

~/go/src/github.com/stephen-fox/power

Go expects you to follow this convention when you create a new project. What differs in project creation is that you must manually create the directory structure shown above — the go tool will not do that for you. As a result, you must know which source control tool and repository host you are using before you create a project. While this promotes the usage of source control tools, it unfortunately forces developers to bend their workflow to the tool.

Dependencies are managed in packages

A Go package is a self-contained codebase that exposes public functions to a caller. It is synonymous with a library. Members of a package are identified with the package keyword at the top of a .go source file. Public functions are exposed outside of the package by capitalizing the first letter of the function’s name.

For example, if you wanted to write a package that exposed a function which prints “hello world”, then the code would look like this:

// This file would be 'print.go' in:
// <GOPATH>/src/github.com/<your-username>/print
package printimport (
“fmt”
)
func HelloWorld() {
fmt.Println(“hello world”)
}

You can now implement your print package in a Go application by importing it. Simply create a new directory within the project and do the following:

// This file would be 'main.go' in:
// <GOPATH>/src/github.com/<your-username>/print/cli
package mainimport (
"github.com/<your-username>/print"
)
func main() {
print.HelloWorld()
}

Now when you execute go run main.go on the command line, you will see “Hello world”.

This convention is very simple. It also promotes the separation of concerns. Go’s compiler will also stop you from creating cyclic dependencies. For example, if you imported your command line application’s package into the print package, Go would not allow you to compile the resulting code. This is very helpful when writing several libraries for closely related, but different APIs. The end result is reusable code that is completely compartmentalized.

Relative imports are bad

Even though Go really wants you to place your project in the GOPATH, you can still work outside of it using “relative imports”.

Note: if you use relative imports, many of the go command’s features will no longer work with your project. This method of development is only for the Go maintainers themselves.

Regardless, if you were to move the Go project in the earlier print example outside of the GOPATH, you would find that it no longer compiles. This is because Go’s default behavior is to import packages relative to <GOPATH>/src directory. You can work around this by changing:

import ”github.com/<your-username>/print”

in ‘main.go’ to…

import “../print”

Now you can compile your application without putting it in the GOPATH!

Again, this is not the intended way to develop a Go application. I only mention it so that you do not make the same mistake I did initially, and create everything outside of the GOPATH.

Targeting dependencies is simple, but basic

As mentioned previously, Go has a very simple dependency management scheme:

go get a package -> import the package -> write some code -> repeat

This is fantastic for small teams and applications. You may be asking “well, how do I target a specific version of the code?” As of Go version 1.9, the answer is you do not.

Similar to Git Flow, Go assumes that all code in the default branch of a repository is “stable” — tested and deployed to production. As a result, Go does not let you specify a version (i.e., a Git tag) to use with the go get command. You can still do git checkout <tag> in the Go project’s directory, but that is a separate step outside of go get.

There are plans to integrate a tool named “go dep” into Go. At the time of writing this piece, go dep is not included with Go.

The shiny stuff

While Go has its share of nuances, it also has a handful of self-selling features. I will discuss some of my favorites in this section.

Cross compilation

Compiling an application for different operating systems and architectures is fast, and easy to do in Go. This can be accomplished several ways. I have found that the quickest and easiest is using Mitchell Hashimoto’s “gox” command line tool. It wraps the go commands used to build an application for Go’s many supported operating systems. Since the tool is also a Go application, it can be downloaded by executing go get github.com/mitchellh/gox.

Note: You must add <GOPATH>/bin to your PATH variable. If you do not do this, you will need to execute ~/go/bin/gox to use it.

After installing gox, you can compile your application for all supported operating systems by executing:

$ cd <project-directory>
$ gox

You can target a specific operating system as follows:

$ gox -os 'windows'

And if you desire a specific architecture:

$ gox -osarch 'windows/amd64'

A full list of supported operating systems (known as GOOS) and architectures (GOARCH) can be found in the Golang Environment documentation.

Lastly, you can specify an output directory and executable name:

$ gox -output=/tmp/my-app

The resulting artifacts’ names can be supplemented with the OS name and architecture by adding specifiers that gox recognizes:

$ gox -output=/tmp/my-app-{{.OS}}-{{.Arch}}

Interfaces

Interfaces are a common feature in most programming languages. They will be your best friends if you plan to deploy to multiple operating systems. Being able to cleanly separate implementations of your logic is very important for maintainability and readability in those cases. Using interfaces will help prevent unforeseen side effects that tend to result from an unorganized codebase.

Suppose you are creating an application that needs to manage a machine’s power state. A switch statement works relatively well for a single function:

// power.go (revision 1/3)package powerimport (
"runtime"
)
func Shutdown() error {
switch runtime.GOOS {
case "darwin":
// macOS logic.
case "linux":
// Linux logic.
case "windows":
// Windows logic.
}
return nil
}

This code quickly becomes very difficult to read when you substitute the comments for actual code. Of course, you can mitigate this by separating the logic into separate function calls. Adding additional functions to your library will only make things messy, however:

// power.go (revision 2/3)package powerimport (
"runtime"
)
func Restart() error {
switch runtime.GOOS {
case "darwin":
restartDarwin()
case "linux":
restartLinux()
case "windows":
restartWindows()
}
return nil
}
func restartDarwin() {
}
// Other restart functions...func Shutdown() error {
switch runtime.GOOS {
case "darwin":
shutdownDarwin()
case "linux":
shutdownLinux()
case "windows":
shutdownWindows()
}
return nil
}
func shutdownDarwin() {
}
// Other shutdown functions...func Sleep() error {
switch runtime.GOOS {
case "darwin":
sleepDarwin()
case "linux":
sleepLinux()
case "windows":
sleepWindows()
}
return nil
}
func sleepDarwin() {
}
// Other sleep functions...

Instead of constantly checking the operating system, we can create different implementations and provide a single implementation to the caller using an interface. In the following example, the Power interface will act as a contract between the package, and the caller. This means that a Power object will always have the Restart(), Shutdown(), and Sleep() functions. Finally, we can provide the implementations to a caller using a public Get() function:

Full code available at: https://github.com/stephen-fox/power

Another tool for the toolbox

In the previous sections, I covered some of the most attractive features provided by the language. Without a doubt, Go makes a fantastic alternative to scripting languages. I also believe that, given enough effort, a Go library could interoperate with other programming languages using interprocess communication, or bindings.

The language is not perfect of course. Go’s project management tools are not ideal. This might make larger teams skeptical of the language. I still recommend giving Go a shot, even if you are skeptical. There are many other features that I did not discuss here, such as Go’s excellent serialization and multitasking support.

If you do not want to install Go on your system, you can still try it out in your web browser using the Go Playground. You can also find excellent introductions to Go at the Tour of Go and Go by Example.

--

--