skies.dev

Golang for JavaScript Developers

16 min read

You're a JavaScript developer interested in learning Go. Great choice! Go, sometimes referred to as Golang, is a performant, statically typed backend programming language. If you've used ESBuild, Turborepo, or Hugo from the JavaScript ecosystem, then you've used tools built in Go.

The goal of this article is to get you up to speed with Go coming from JavaScript. I'll point out in which ways the two languages are similar and how they're different. Let's dive in.

Setting up

First thing you'll need to do is install Go on your system. Once you install Go, you'll be able to access the go tool from your shell. The go tool is powerful. It lets you

  • compile and execute your code with go run
  • run tests with go test
  • install dependencies with go get
  • format and lint your code with go fmt and go vet respectively. In JavaScript, you'd use Prettier and ESLint for this. In Go, it's built right into the go tool.

Simply type go in your shell to see a list of commands available to you.

Exercise: Use the Go tool to print the version of Go you're using.

Let's set up a project.

mkdir hello
cd hello
go mod init HelloWorld

The go mod init command creates a go.mod file, which stores your project's dependencies and their versions. It's similar to using npm init to set up a package.json.

Writing your first Go program

Let's write our first program. In good computer science tradition, we'll start off by writing a "hello world" application.

In Go, all code belongs to a package. All executable programs must have a main function inside a main package.

package main

func main() {
	println("hello world")
}

We can use the go tool to compile and run the program in a single command.

go run hello.go

If you're using an IDE like IntelliJ, you should never have to write package declarations or imports by hand. Simply write the function you want and the IDE will be smart enough to fill in the import for you.

Congratulations! You've written your first Go program.

Project structure

Let's start by getting a bird's eye view of how a Go project is structured. Although it's not enforced, Go has some common idioms for project structure.

Often, Go projects will have all executable programs (i.e. programs with a main function in package main) under a cmd directory, while application code is split across multiple packages under a pkg directory.

A simple blogging application backend might have the following structure.

โ”œโ”€โ”€ go.mod
โ”œโ”€โ”€ cmd
โ”‚  โ””โ”€โ”€ main.go
โ””โ”€โ”€ pkg
    โ”œโ”€โ”€ auth
    โ”‚  โ”œโ”€โ”€ auth.go
    โ”‚  โ””โ”€โ”€ auth_test.go
    โ”œโ”€โ”€ common
    โ”‚  โ”œโ”€โ”€ common.go
    โ”‚  โ””โ”€โ”€ common_test.go
    โ”œโ”€โ”€ post
    โ”‚  โ”œโ”€โ”€ post.go
    โ”‚  โ””โ”€โ”€ post_test.go
    โ””โ”€โ”€ user
        โ”œโ”€โ”€ user.go
        โ””โ”€โ”€ user_test.go

In this example,

  • pkg/auth/auth.go would be in package auth and would contain authentication logic.
  • pkg/post/post.go would be in package post and would contain post logic.
  • cmd/main.go would be in package main and would contain the entry point of the server.

Also notice the tests are colocated with the implementation files. This is a common pattern in Go. Suffixing a file with _test.go tells the go tool which files are tests (when you run go test).

In JavaScript, there are many options to choose from for testing including Jest, Cypress, Vitest, and more. Golang simplifies testing by baking it into the language installation, and it's powerful. In addition to testing your code, you can use go test to run performance benchmarks and create documentation examples.

In a JavaScript project, you might have a separate test for each behavior of a function. If you're testing a function in Go, it's common to test all of the function's behavior as single test. This is often accomplished with table driven tests.

Exporting code

Any maintainable piece of software will have multiple files to encapsulate functionality. In JavaScript, you're used to exporting functions, objects, and literals in the following ways.

// CommonJS
module.exports = {
  // ...
};

// ESM
export const widget = {
  // ...
};

Go has a peculiar way of exporting functions from packages. The way you do it is by capitalizing whatever you're exporting (e.g. a function, object, field, etc).

In the following code snippet, Add is exported and is accessible from outside package common. However, sub is not exported, so it's only accessible from within package common. That is to say Add is a public function while sub is a package-private function.

pkg/mymath/arithmetic.go
package mymath

func Add(x, y int) int {
	return x + y
}

func sub(x, y int) int {
	return x - y
}

So let's say you want to use the Add function from another package. You'd import the package and use the function like so.

pkg/agg/sum.go
package agg

import "MyModule/pkg/mymath"

func Sum(nums []int) int {
	sum := 0
	for _, n := range nums {
		sum = mymath.Add(sum, n)
	}
	return sum
}

Another way to hide function is to define them as function closures. Similar to JavaScript's function closures, Go's anonymous functions can access variables defined in the outer function.

pkg/foo/foo.go
package mymath

func Add2(x int) {
  y := 2
  add := func() {
    return x + y
  }
  return add()
}

Control flow

Go has a few control flow statements that you're probably familiar with coming from JavaScript. You got your if statements.

if x > 0 {
  println("x is positive")
} else if x < 0 {
  println("x is negative")
} else {
  println("x is zero")
}

As you can see, in Go we don't need to use parenthesis around conditions. The same is true for for loops.

for i := 0; i < 10; i++ {
  println(i)
}

In Go, there's no while keyword. We use for loops to create while loop semantics. Also notice, we use := anytime we want to declare a variable, and = to assign a value to a new value variable. Using := to reassign an already declared variable is a compilation error.

i := 0
for i < 10 {
  println(i)
  i = i + 1
}

We'll use the range keyword to iterate over arrays and maps. Similar to JavaScript, Go has continue and break statements.

We're introducing the blank identifier _ here. No, this is not a reference to lodash. It's used to indicate that we don't care about the value of a variable. It's a compilation error to declare a variable and not use it in Go.

for _, n := range nums {
	if n%2 == 0 {
		continue
	}
	if n > 10 {
		break
	}
	println(n)
}

Then there's switch statements. Unlike JavaScript, where you need to use break to exit a switch statement, Go automatically exits a switch statement after the first matching case. However you can use fallthrough to force a switch statement to continue to the next case, but this is seldom used in practice.

switch x {
case 0:
  println("x is zero")
case 1:
  println("x is one")
case 2:
  println("x is two")
  fallthrough
default:
  println("x is not zero or one")
}

This is not an exhaustive list of Go's control flow statements. But this should be enough to understand most control flows in Go.

Data structures

Let's talk about data structures in Go. There are 3 main data structures in Go:

  • Slices
  • Maps
  • Structs

Slices

Slices work in a similar way to JavaScript arrays. We use append to add elements to a slice, and we can get the length of the slice with len.

package main

func main() {
  var nums []int
  for i := 0; i < 10; i++ {
    nums = append(nums, i)
  }
  println(len(nums))
}

Unfortunately, Go doesn't offer a rich set of array methods like JavaScript arrays. As of Go version 1.19, you have to write map, filter, and reduce logic manually using for loops. This may change in a future version because Go added generic types in version 1.18.

Maps

The next data structure commonly used is the map. They work in a similar way to JavaScript's Map.

package main

func main() {
  m := map[string]int{
    "one": 1,
    "two": 2,
    "three": 3,
  }
  println(m["one"])   // 1
  println(m["two"])   // 2
  println(m["three"]) // 3
}

You might've also used JavaScript Sets. In Go, we also use maps when we need sets. Just map the key type to a bool value.

package main

func main() {
  s := map[string]bool{
    "one": true,
    "two": true,
    "three": true,
  }
  println(s["one"])   // true
  println(s["two"])   // true
  println(s["three"]) // true
}

Another thing about maps is they actually return 2 values. The second value is a bool that indicates whether the key exists in the map. If the key doesn't exist, the value is the zero value of the map's value type. The idiomatic name for the second value is ok.


```go
package main

func main() {
  m := map[string]int{
    "one": 1,
    "two": 2,
    "three": 3,
  }
  v, ok := m["four"]
  println(v) // 0
  println(ok) // false
}

Structs

To define custom objects, we use structs, and you can think of them like JavaScript objects. We define the struct type, and then we can create instances of that type.

The same rules about exporting via capitalization apply to structs. So Name and Age are accessible on Person structs outside the package, but govtID is not.

package main

type Person struct {
	Name string
	Age  int
	govtID string
}

func main() {
	p := Person{
		Name: "John",
		Age:  30,
	}
	println(p.Name) // John
	println(p.Age)  // 30
}

We can also define methods on structs. This is similar to JavaScript classes and give behavior to our structs in an object-oriented way.

package main

type Person struct {
  Name string
  Age  int
}

func (p Person) SayHello() {
  println("Hello, my name is", p.Name)
}

func main() {
  p := Person{
    Name: "John",
    Age:  30,
  }
  p.SayHello() // Hello, my name is John
}

Interfaces

Be careful though, Go is not really an object-oriented programming language. For example, it does not support inheritance directly, but you can achieve polymorphism using interfaces.

An interface is a set of methods that a type must implement. If a type implements all the methods in an interface, then it is said to implement that interface.

It's idiomatic to keep Go interfaces small and focused on a single behavior. We usually name the interface after the behavior it represents.

For example, in the standard library, the io.Reader interface has a single method Read, the io.Writer interface has a single method Write, and the fmt.Stringer interface has a single method String.

Of course, this is not a hard-and-fast rule. And you're almost certain to encounter interfaces larger than a single method.

Go supports implicit interfaces, which means you don't need to explicitly declare that a type implements an interface. Structs can implement a method defined by an interface, and it can be substituted for that interface. This is how we achieve polymorphism in Go.

package main

type Speaker interface {
  Speak()
}

type Person struct {
  Name string
}

func (p Person) Speak() {
  println("Hello, my name is", p.Name)
}

type Dog struct {
  Name string
}

func (d Dog) Speak() {
  println("Woof, my name is", d.Name)
}

func main() {
  p := Person{Name: "John"}
  d := Dog{Name: "Fido"}
  var s Speaker
  s = p
  s.Speak() // Hello, my name is John
  s = d
  s.Speak() // Woof, my name is Fido
}

Errors

Golang handles errors in a very different way than JavaScript. In JavaScript, we'll throw Error objects and use try/catch blocks to handle them.

Go does not have try/catch blocks. Instead, we use error values to handle errors. In Go, if a function could return an error, it will return an error value as the last return value and let the caller decide what to do with it.

Here's an example of a program that handles errors. You can see that we use the if err != nil pattern to check for errors, where nil can be thought of like null in JavaScript.

package main

import (
	"encoding/csv"
	"fmt"
	"log"
	"os"
)

func readCsvFile(filePath string) [][]string {
	f, err := os.Open(filePath)
	if err != nil {
		log.Fatal("Unable to read input file "+filePath, err)
	}
	defer f.Close()

	csvReader := csv.NewReader(f)
	records, err := csvReader.ReadAll()
	if err != nil {
		log.Fatal("Unable to parse file as CSV for "+filePath, err)
	}

	return records
}

func main() {
	records := readCsvFile("data.csv")
	fmt.Println(records)
}

Also notice we're introducing the defer keyword. This lets us define logic to be run after the function returns, similar to how we use finally blocks in JavaScript.

There is also a panic function that we can use to stop the program. The panic function should be reserved for non-recoverable errors. panic will stop the program and print a stack trace whereas an error value can be handled by the caller and the program can continue.

In idiomatic Go, functions that panic are usually prefixed with Must. In the following example, although there is randomness involved, each branch behaves the same way. MustCompile will panic if the regular expression is invalid, whereas Compile will return an error value if the regular expression is invalid.

package main

import (
	"fmt"
	"math/rand"
	"os"
	"regexp"
	"strings"
)

func main() {
	expr := strings.TrimSpace(os.Args[1])
	if rand.Intn(2) == 0 {
		_, err := regexp.Compile(expr)
		if err != nil {
			panic(err)
		}
	} else {
		regexp.MustCompile(expr)
	}
	fmt.Println("%s is a valid regexp", expr)
}

At this point you should have a good understanding of the basics of Go. We've looked at control flows and data structures and how they relate to Go. We've also looked at how Go handles errors. In the next section, we'll look at how Go handles concurrency, which is one of the things that make Go special.

Concurrency

In JavaScript, we handle concurrency using promises. If we want to run multiple tasks in parallel, we can use Promise.all to wait for all of them to finish. In Go, we use goroutines and channels to handle concurrency.

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. We can launch a new goroutine using the go keyword. Here's an example of a program that launches two goroutines.

package main

import (
  "fmt"
  "time"
)

func say(s string) {
  for i := 0; i < 5; i++ {
    time.Sleep(100 * time.Millisecond)
    fmt.Println(s)
  }
}

func main() {
  go say("world")
  say("hello")
}

The say function is called twice, once in the main goroutine and once in a new goroutine. When we run this program, we see that the output is interleaved.

hello
world
hello
world
hello
world
hello
world
hello
world

This is because the goroutines are running concurrently. Note the order of the output is non-deterministic, it could be different each time we run the program.

Channels

Channels are a typed conduit through which you can send and receive values with the channel operator, <-. Here's an example of a program that uses channels to pass messages between goroutines.

A similar paradigm in JavaScript related to channels is Web Workers, where we can use postMessage to send messages between threads.

package main

import "fmt"

func sum(s []int, c chan int) {
  sum := 0
  for _, v := range s {
    sum += v
  }
  c <- sum // send sum to c
}

func main() {
  s := []int{7, 2, 8, -9, 4, 0}

  c := make(chan int)
  go sum(s[:len(s)/2], c)
  go sum(s[len(s)/2:], c)
  x, y := <-c, <-c // receive from c

  fmt.Println(x, y, x+y)
}

When we run this program, we see that the output is not interleaved.

-5 17 12

This is because the two goroutines are communicating through the channel.

There's more to the concurrency story than what's covered here. If you're interested in learning more, I recommend reading Go Concurrency Patterns.

Standard library

3rd party libraries are tricky in Go. There's plenty of them, but there's not a ton of 3rd party libraries with a large ecosystem behind them. In JavaScript, you can download a NPM package with over 1 million downloads and be fairly confident that it will be supported for a long time. In Go, sometimes you'll find a library that looks good but only has maybe 100 stars on GitHub, so you'll have to decide if it's worth the risk of bringing into a production app.

To that end, it's not uncommon for a JavaScript project to have many dependencies downloaded from NPM. The Go community however seems generally more averse to bringing in 3rd party libraries and instead prefer to depend on the standard library where possible.

Check out Go's standard library documentation to get a full picture of what's available, but here's a handful of packages and a brief summary to get you started.

  • fmt for getting a printing format strings
  • io for reading and writing data
  • os for interacting with the operating system
  • testing for writing tests
  • time for working with time
  • context for handling cancellation and timeouts
  • strings for working with strings
  • sort for sorting slices
  • math for math functions

Conclusion

In this post, we've looked at the basics of Go coming from JavaScript. We've seen how Go handles control flows and data structures. We've also seen how Go handles errors and concurrency.

You should have enough context to get started with understanding and writing Go programs. If you found this content useful or want to leave a comment, tweet at me @skies_dev.

Resources

Hey, you! ๐Ÿซต

Did you know I created a YouTube channel? I'll be putting out a lot of new content on web development and software engineering so make sure to subscribe.

(clap if you liked the article)

You might also like