Skip to content

Next Stop Golang

feature

Cover photo by Thanos Pal on Unsplash

Since I embarked on the network automation journey, Python and Python-based utilities have always been the go-to tools for the job. I started, as maybe many of you, with Ansible. Then when dealing with its DSL grew more and more tiresome, I began writing my first Python code. And I was happy. I felt like I was learning something fundamental again, not just another niche technology that would never come in handy elsewhere. Although Python had some limitations in terms of portability and speed, it didn't bother me much.

One day as I was commuting to the office, I tuned in to episode #617 of the PacketPushers Heavy Networking podcast. The episode was called Go Vs. Python For Network Engineers. The show's guest, Darren Parkinson, advocated that network engineers should at least try to learn Go alongside Python if not switching to Go completely. That got me curious, and later I read his blog post to understand his reasoning further. While the arguments in favor of switching to Go seemed valid, I recognized that I still had a lot to learn in Python. Therefore, switching to a new language would likely distract me from my current goals and hinder my progress. I think sticking with Python was a good decision at that moment because I've learned a lot about OOP, design patterns, organizing and shipping Python code, among other things, since then.

But time passed, and my curiosity grew. I also dealt more and more with small Python CLI utilities that I wanted my teammates to use, but the portability was a real pain in the ass. It was ok to run some tools as containers in CI/CD pipelines, but doing that on hosts manually was just too much overhead. I revisited Darren's post and decided that the time had come.

My first exercise was to rewrite a small CLI utility written in Python by my teammate in Go. The utility was pretty simple, somewhere around 200 lines of code. What it essentially did was querying YAML files. It took me maybe one day, and it was lots of fun. As a result, we got a binary without external dependencies, and it ran ~20x faster than the Python prototype. Although speed wasn't an issue in that scenario, it still ended up being a nice little perk.

So, here are some features of Go that I had to wrap my head around while working on that small project.

Formatting

The first thing I noticed was that when I added a package to the imports list and hit save, it instantly disappeared. The thing is, Go is even more strict than Python in terms of code formatting. If you have an unused import in your code, it won't compile. The same goes for the unused variables. The official Go extension for VS Code formats the code on save by default, and when it does, it removes the unused imports. It was annoying at first, but later I learned that I could start using external packages without importing them. Just hit save, and the formatter adds the necessary imports (1).

  1. At least the ones from the standard library.

Managing dependencies

Managing external dependencies is a breeze with Go. You just declare your imports and run go mod tidy. That's it. No need for requirements.txt and venv, or external tools like Poetry.

Naming variables

Go community promotes using short but descriptive variable names. If you need a multi-word name, use camelCase instead of snake_case. Names are case-sensitive, and the case of the first character has a special meaning. For example, if you need a function to be accessible outside of the package (to be exportable), its first character should be uppercase.

Data modeling

In Python, I rely on Pydantic when dealing with external data sources and constructing my own data models. In Go, stucts in combination with struct tags cover my needs nicely.

For example, this is how you can parse a YAML file in Go.

package main

import (
    "fmt"
    "log"

    "gopkg.in/yaml.v3"
)

type Config struct {
    Server string `yaml:"server"`
    Port   int    `yaml:"port"`
}

var data = `---
server: 10.0.0.1
port: 443`

func main() {

    // Create a Config struct to hold the parsed data
    var config Config

    // Unmarshal the YAML data into the Config struct
    err := yaml.Unmarshal([]byte(data), &config)
    if err != nil {
        log.Fatalf("Failed to parse YAML: %v", err)
    }

    // Access the parsed data
    fmt.Println("Server:", config.Server)
    fmt.Println("Port:", config.Port)
}
Server: 10.0.0.1
Port: 443

You can run it in the playground.

Typing

I'm used to type hints in Python. But because they're optional, I have to rely on external tools such as Mypy to ensure I'm doing it right. Go is a statically typed language. This means that everything must have a type. Having types makes your code safer and less error-prone while enabling your IDE to provide accurate code completion and suggestions.

Of course, like everything in this life, static typing has its compromises. Imagine you need to write a function that checks if an item belongs to an array.

Note

Well, in Python you could just use the in operator and be done with it, but that's not the point of this example.

You make a loop to iterate over an array and return true the moment a match is found. Sounds easy, but when everything must have a type, you end up writing a separate function for each type you might need (e.g., an array of ints, an array of strings, etc.). Doesn't look DRY, right? Fortunately, Golang now has generics that are designed to solve this.

Asterisks and ampersands (AKA pointers)

Although the Zen of Python states that explicit is better than implicit, a lot of things in Python are actually implicit. This is true for passing data. Python is a pass by reference language, while Go is pass by value. Let's take a look at the example below.

a = [1,2,3]
b = a
b.append(4)
print(a)

Output:

[1, 2, 3, 4]
package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := []int{}
    b = a
    b = append(b, 4)
    fmt.Println(a)
}

Output:

[1 2 3]

Playground link

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := &a
    *b = append(*b, 4)
    fmt.Println(a)
}

Output:

[1 2 3 4]

Playground link

In Python, b is not a copy of a; it's a pointer to a. That's why when you modify b, you also change a because they're the same object. But that's not obvious when looking at the code. Go, on the other hand, is explicit. It allows you to make a more conscious decision about how to pass the data.

I recommend this article for a more detailed explanation of this topic with some good usage examples.

Error handling

Error handling in Go differs from what you may be used to in Python. There are no try/except clauses. Instead, error is just another return value of a function. This enables handling errors just like any other return values.

Below is an example of idiomatic error handling in Go.

package main

import "fmt"

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("can't divide '%d' by zero", a)
    }
    return a / b, nil
}

func main() {

    r, err := Divide(2, 0)
    if err != nil {
        fmt.Printf("Operation failed: %s\n", err)
        return
    }
    fmt.Printf("Result: %d", r)
}

Playground link

Output:

Operation failed: can't divide '2' by zero

I came across a good article that covers this topic in detail.

Toolbox

Go comes with batteries included. You get a dependency manager, linter, formatter, and other tools, all within the go executable.

As for release management, I use GoReleaser to push binaries to Gitlab.

In addition, some of the tools I employ for Python projects, such as commitizen and pre-commit, are applicable to Go projects too.

Things I didn't touch yet

Because of the simplicity of my first projects, I didn't have a chance to explore concurrency in Go, which is considered one of the main areas where Go truly shines. I'm quite sure I'll tackle it when I need to interact with many network devices simultaneously.

Final thoughts

I like Go. It's simple, opinionated, and fun. It has its drawbacks, of course, but at this point, I find it very applicable to the tasks at hand. It's a powerful tool for tackling network automation problems. I would even argue that it's a better choice as the first language for network engineers than Python because of its simplicity and focus on speed and portability.

However, I am by no means discarding Python. In fact, I hope that my foray into Go will enhance my skills as a Python developer. I believe that by exploring a different language and its unique features, one can gain a fresh perspective and a deeper understanding of programming concepts.

In the end, the choice of programming language ultimately depends on the specific needs and requirements of the project. While Go has become my preferred language for my current tasks, Python remains an indispensable part of my toolkit.

Book recommendations

References and further reading

Comments