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 fmt
andgo vet
respectively. In JavaScript, you'd use Prettier and ESLint for this. In Go, it's built right into thego
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 inpackage auth
and would contain authentication logic.pkg/post/post.go
would be inpackage post
and would contain post logic.cmd/main.go
would be inpackage 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.
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.