Golang for JavaScript Developers
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 fmtandgo vetrespectively. In JavaScript, you'd use Prettier and ESLint for this. In Go, it's built right into thegotool.
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.gowould be inpackage authand would contain authentication logic.pkg/post/post.gowould be inpackage postand would contain post logic.cmd/main.gowould be inpackage mainand 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.
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.
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.
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.