A JavaScript developer tries Go for the first time

Written by Bahaa Zidan

2024-05-18

I’ve been building software for a little over 5 years. And while I had to use languages like Python, Java, or C for certain projects, I wrote the vast majority of my code in JavaScript. This article isn’t going to be a rant against JavaScript. There are enough of these already. I’ll just list a few things I found to be really cool in the Go programming language from my perspective as a web developer. Think of it as my first impressions on the language.

Errors as values

If you’ve done any serious development in JavaScript, you’ll reach the conclusion that it’s almost always better to return errors as normal JS objects instead of “throwing” an instance of the built-in Error class. Throwing should only be used when you want the program to crash.

Golang comes with this out of the box. Errors are returned as values. And you’ll explicitly handle them using if err != nil checks. If you want the program to crash, use the panic keyword. You can also recover from a panic using a recover statement. While this may feel gnarly to write. It’s so easy to read. Which is a good trade-off in my opinion.

defer

A defer statement defers the execution of a function until the surrounding function returns. This is super handy when you’re doing anything that requires a cleanup step. Here’s a function where I open a file to copy its’ content to another file:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

This works, but there is a bug. If the call to os.Create fails, the function will return without closing the source file. This can be easily remedied by putting a call to src.Close before the second return statement, but if the function were more complex the problem might not be so easily noticed and resolved. By introducing defer statements we can ensure that the files are always closed:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

Defer statements allow us to think about closing each file right after opening it, guaranteeing that, regardless of the number of return statements in the function, the files will be closed.

This example is directly from the awesome article: defer, panic, and recover by Andrew Gerrand. It’s a fantastic read and it expands on the rules of defer and how it works in tandem with other unique control flow statements like panic and recover.

Type conversion is explicit

One of the biggest sources of memes on JavaScript is type coercion. JavaScript tries to automatically change the types of your values depending on the operation you’re trying to do. While type coercion can be handy sometimes, it often leads to bugs and confusion among newcomers.

patrick star javascript type coercion

In Go, assignment between items of different types requires an explicit conversion. It will not compile if you’re trying to assign a string to an int or try to add two mismatched types. While this simplicity may make Go a little bit harder to write, it makes it so easy to read and maintain which is a recurring theme.

No break statement needed at the end of every switch case

Here’s a JavaScript switch statement from MDN:

const expr = 'Papayas';
switch (expr) {
  case 'Oranges':
    console.log('Oranges are $0.59 a pound.');
    break;
  case 'Mangoes':
  case 'Papayas':
    console.log('Mangoes and papayas are $2.79 a pound.');
    break;
  default:
    console.log(`Sorry, we are out of ${expr}.`);
}

Here is the same switch written in Go:

expr := "Papayas"
switch expr {
case "Oranges":
	fmt.Println("Oranges are $0.59 a pound.")
case "Mangoes", "Papayas":
	fmt.Println("Mangoes and papayas are $2.79 a pound.")
default:
	fmt.Printf("Sorry, we are out of %s.", expr)
}

Go’s switch is like the one in C, Java, JavaScript, and PHP, except that Go only runs the selected case, not all the cases that follow. In effect, the break statement that is needed at the end of each case in those languages is provided automatically in Go.

Pointers

In Go, function arguments are all passed by value or copied by default. This includes things like arrays and structs. You can explicitly define a function that takes a pointer as an argument. A pointer holds the memory address of a value. Here’s a snippet from the official Go tutorial:

func main() {
	i, j := 42, 2701

	p := &i         // point to i
	fmt.Println(*p) // read i through the pointer
	*p = 21         // set i through the pointer
	fmt.Println(i)  // see the new value of i

	p = &j         // point to j
	*p = *p / 37   // divide j through the pointer
	fmt.Println(j) // see the new value of j
}

Again, this makes Golang so easy to read and maintain.

Simple type system

Don’t get me wrong, I love TypeScript. I can’t imagine myself going back to the dark times of having to console.log everything just to get to know what I’m working with. But the type system might be a little too rich for my taste. I often run into situations where I have to do a lot of type gymnastics when I’m using it. This is especially true when building libraries. A complex type system also leads to very complicated, sometimes unhelpful error messages: very long typescript error

In contrast, Go has a very simple type system. You have the usual primitives like int, string, … etc. You have arrays, slices, and structs. You have interfaces and functions. Generics were later added to the language. That’s it. The compiler is very fast and will display very concise/helpful error messages most of the time.

Only one way to do things

While it may sound restrictive at first, having only one way to do any given thing makes Go a very easy language. Especially for big teams working on big codebases. A junior developer should produce the same code a senior would produce working on the same problem. A simple example for that is loops in Go.

Go has only one looping construct, the for loop.

sum := 0
for i := 0; i < 10; i++ {
	sum += i
}
fmt.Println(sum)

The init and post statements are optional. You can drop the semicolons making for a while loop in Go:

sum := 1
for sum < 1000 {
	sum += sum
}
fmt.Println(sum)

If you omit the loop condition it loops forever, so an infinite loop is compactly expressed.

for {
	// runs forever
}

Still ways to go

I just started learning Go. The only way I can describe it is that Go is Simple. So incredibly simple. When you read a program written in Go you almost always know what it does. The icing on that cake is that Go’s performance is so good. It’s sometimes hard to believe that Go is a garbage-collected programming language. It’s been used to build performance critical software such as the cockroach database and many others.

This is just the beginning for me. I’ll write more about Go as I get more experienced in it. I’m excited to see what I can build using this beautifully simple language. Wish me luck 🍀

Thank you for reading!

🍉