I've written JavaScript and TypeScript for years, mostly on the frontend. Recently I started learning Go (also called Golang), and it turned out to be much less scary than I expected.
A lot of what you know from JS/TS carries straight over to Go. Some things are different, and a few will surprise you. This post puts the three languages side by side, so you can see what's the same and what's new.
If you can write TypeScript, you can learn Go. Let's go 🚀
Why would a frontend dev learn Go?#
- Fast backends and APIs. Go is popular for web servers, microservices, and CLIs.
- Simple language. Go has very few keywords. There's usually one obvious way to do something.
- Tooling you already use. esbuild (the bundler Vite uses under the hood) is written in Go. So are Docker, Kubernetes, and Hugo.
- One binary.
go buildgives you a single file you can run anywhere. Nonode_modules😅
The big picture#
| Topic | JavaScript | TypeScript | Go |
|---|---|---|---|
| Types | Dynamic | Static (checked at build, erased at runtime) | Static (checked at compile, real at runtime) |
| How it runs | Interpreted / JIT (Node, browser) | Compiled to JS, then same as JS | Compiled to a native binary |
| Package manager | npm / pnpm / yarn | npm / pnpm / yarn | Built in: go mod |
| Formatter | Prettier / Biome | Prettier / Biome | Built in: go fmt |
| Semicolons | Optional | Optional | Not needed |
| Classes | Yes | Yes | No (uses structs + methods) |
| Error handling | try / catch |
try / catch |
Errors are returned as values |
| Async | Promise, async/await |
Promise, async/await |
Goroutines and channels |
null / undefined |
Both | Both | Only nil |
Don't worry if some rows look strange. We'll walk through each one.
1. Hello World#
JavaScript / TypeScript
console.log("Hello, World!");Go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}What's different
- Every Go file starts with
package. A runnable program always usespackage main. - The program starts at
func main(). Think of it like the entry file in your bundler config. fmtis a built-in package for printing.fmt.Println≈console.log.
Run it with:
go run main.go2. Variables#
JavaScript
let name = "Fachalik";
const age = 25;TypeScript
let name: string = "Fachalik";
const age: number = 25;Go
var name string = "Fachalik" // full form
var city = "Jakarta" // type is inferred
age := 25 // short form, only inside functions
const pi = 3.14What's the same
- Type inference works like TypeScript. Go figures out the type from the value.
constmeans the value can't change.
What's different
- The type comes after the name, like TS, but without the colon:
name string, notname: string. :=is the shortcut you'll use most. It means "declare and assign".- Go's
constonly works for simple values (numbers, strings, booleans). You can't doconst user = { ... }. - Unused variables are a compile error. Go won't build if you declare something and never use it. Annoying at first, but it keeps code clean.
Zero values instead of undefined#
In JS, a variable you didn't assign is undefined. In Go, every type has a zero value:
var count int // 0
var title string // "" (empty string)
var done bool // falseThis means you never get "cannot read property of undefined" from a plain number or string. 🎉
3. Types#
| TypeScript | Go |
|---|---|
string |
string |
number |
int, int64, float64, ... |
boolean |
bool |
string[] |
[]string (a slice) |
Record<string, number> |
map[string]int |
interface User { ... } / type User = { ... } |
type User struct { ... } |
any / unknown |
any (alias of interface{}) |
The biggest change: Go has separate types for whole numbers and decimals. int is for whole numbers, float64 is for decimals. You can't add them together without converting:
a := 10 // int
b := 2.5 // float64
sum := float64(a) + b // must convert explicitlyTypeScript would just let you do a + b. Go makes you say what you mean.
4. Functions#
JavaScript
function add(a, b) {
return a + b;
}
const multiply = (a, b) => a * b;TypeScript
function add(a: number, b: number): number {
return a + b;
}Go
func add(a int, b int) int {
return a + b
}
// same types can be grouped
func multiply(a, b int) int {
return a * b
}What's the same: parameters with types, return type, return. It reads almost like TypeScript.
What's different
- The keyword is
func, and the return type has no colon. - There are no arrow functions. But you can still store functions in variables and pass them around:
greet := func(name string) string {
return "Hi, " + name
}
fmt.Println(greet("Gopher"))Multiple return values#
This one is new and you'll love it. A Go function can return more than one value:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
result, err := divide(10, 2)It feels like array destructuring in JS (const [result, err] = divide(10, 2)), but it's built into the language.
5. Objects → Structs#
Go has no classes. Instead you use a struct (the shape of the data) and attach methods to it.
TypeScript
class User {
constructor(
public name: string,
public age: number,
) {}
greet() {
return `Hi, I'm ${this.name}`;
}
}
const user = new User("Fachalik", 25);
user.greet();Go
type User struct {
Name string
Age int
}
func (u User) Greet() string {
return "Hi, I'm " + u.Name
}
user := User{Name: "Fachalik", Age: 25}
user.Greet()What's different
- The
(u User)before the method name is called a receiver. It's likethis, but you give it a name. - No
new, no constructor. You just writeUser{...}. By convention, people write a function likeNewUser(...)when they need setup logic. - Capital letter = public, lowercase = private.
Namecan be used from other packages;namecan't. Noexportkeyword, noprivatekeyword.
Interfaces (the familiar part)#
Good news: Go interfaces feel a lot like TypeScript's structural typing. You don't write implements. If a type has the right methods, it fits the interface automatically.
type Greeter interface {
Greet() string
}
func sayHello(g Greeter) {
fmt.Println(g.Greet())
}
sayHello(user) // works, because User has Greet()That's the same "if it looks like a duck" idea you already know from TS.
6. Arrays, slices, and maps#
JavaScript / TypeScript
const fruits: string[] = ["apple", "banana"];
fruits.push("mango");
const scores: Record<string, number> = { alice: 90 };
scores.bob = 80;Go
fruits := []string{"apple", "banana"}
fruits = append(fruits, "mango") // note: you reassign
scores := map[string]int{"alice": 90}
scores["bob"] = 80
value, ok := scores["charlie"] // ok is false if the key doesn't exist[]stringis a slice. Use it like a JS array.appendreturns a new slice, so you always writefruits = append(fruits, ...).- The
value, ok :=pattern is how you check if a key exists. Safer than gettingundefined.
No .map(), .filter(), .reduce() ... just for#
This was my biggest "wait, what?" moment. Go doesn't have those array methods built in. You write a loop:
JavaScript
const evens = numbers.filter((n) => n % 2 === 0);Go
var evens []int
for _, n := range numbers {
if n%2 == 0 {
evens = append(evens, n)
}
}It's more lines, but it's very easy to read. (The slices package in the standard library helps with common tasks like slices.Contains and slices.Sort.)
7. Loops and conditions#
Go has only one loop keyword: for. It covers everything.
// classic loop, like for (let i = 0; i < 5; i++)
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// like while (count < 3)
count := 0
for count < 3 {
count++
}
// like for...of / forEach
for index, fruit := range fruits {
fmt.Println(index, fruit)
}
// don't need the index? use _
for _, fruit := range fruits {
fmt.Println(fruit)
}if looks familiar, just no parentheses (but the braces are required):
if age >= 18 {
fmt.Println("adult")
} else {
fmt.Println("minor")
}Other small differences:
- No
===. Go only has==, and it's always strict because types must match. - No ternary
a ? b : c. Use a normalif. switchdoesn't fall through, so you don't needbreakin every case.
8. Error handling: no try/catch#
JavaScript / TypeScript
try {
const data = JSON.parse(text);
} catch (err) {
console.error(err);
}Go
var data map[string]any
err := json.Unmarshal([]byte(text), &data)
if err != nil {
fmt.Println("failed:", err)
return
}In Go, errors are just values that a function returns. You check them right away with if err != nil. You'll write this line a lot.
At first it feels repetitive. After a while, I started to like it: you always know exactly which line can fail, and nothing gets thrown "from somewhere".
9. null / undefined → nil, and pointers#
JS has two "empty" values. Go has one: nil. It's used for pointers, slices, maps, interfaces, and errors.
Go also has pointers. Don't panic, they're simpler than in C. The idea:
- A normal value is a copy.
- A pointer (
*User) is a reference to the original.
func birthday(u *User) {
u.Age++ // changes the original
}
user := User{Name: "Fachalik", Age: 25}
birthday(&user) // & means "give me a pointer to this"
fmt.Println(user.Age) // 26In JS, objects are always passed by reference, so you never think about this. In Go, structs are copied by default, and you use a pointer when you want to change the original. That's really all you need to know to start.
10. Async: Promises vs goroutines#
JavaScript / TypeScript
async function fetchAll() {
const [users, posts] = await Promise.all([getUsers(), getPosts()]);
}Go
func fetchAll() {
var wg sync.WaitGroup
var users []User
var posts []Post
wg.Add(2)
go func() {
defer wg.Done()
users = getUsers()
}()
go func() {
defer wg.Done()
posts = getPosts()
}()
wg.Wait() // like await Promise.all
}go someFunction()starts a goroutine: a very lightweight task that runs at the same time as the rest of your code.sync.WaitGroupwaits for all of them to finish, likePromise.all.deferruns a line when the function ends, like a minifinally.
There's no async / await keyword. In Go, normal code just blocks and waits, and that's fine, because Go runs many goroutines at once. You don't need to "color" functions as async.
For sending data between goroutines, Go has channels. Think of them as a pipe:
ch := make(chan string)
go func() {
ch <- "done!" // send into the pipe
}()
msg := <-ch // wait and receive from the pipe
fmt.Println(msg)11. Packages and imports#
JavaScript / TypeScript
import { readFile } from "fs/promises";
import express from "express";Go
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)| npm world | Go world |
|---|---|
package.json |
go.mod |
pnpm-lock.yaml |
go.sum |
pnpm init |
go mod init my-app |
pnpm add express |
go get github.com/gin-gonic/gin |
node_modules/ |
Shared module cache (not in your project) |
export keyword |
Capital first letter |
Go's standard library is big. You can build a full HTTP server with net/http and no third-party packages:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Go!")
})
http.ListenAndServe(":3000", nil)
}Compare that to setting up Express. Pretty nice.
12. Tooling cheat sheet#
| What you do in JS/TS | In Go |
|---|---|
node index.js / tsx index.ts |
go run . |
tsc / vite build |
go build |
| Prettier / Biome | go fmt ./... |
| ESLint | go vet ./... |
| Jest / Vitest | go test ./... |
Formatting, testing, and linting are all built in. No config files to argue about. 😄
Quick summary#
Things that will feel familiar
- Static types and type inference (like TypeScript)
- Interfaces that match by shape, no
implements(like TypeScript) - Functions as values, closures
if,for,switch, curly braces- Package imports and a module file
Things that are new
- Compiled to a single binary, no runtime like Node
- No classes: structs + methods instead
- Capital letter means exported
- Errors are returned, not thrown
- Multiple return values
- Pointers (
*and&) - Goroutines and channels instead of Promises
- Only one loop:
for - No
.map()/.filter(): use loops - Unused variables and imports don't compile
My tips for getting started#
- Do the official Tour of Go. It runs in the browser and takes an afternoon.
- Rebuild something small you already know, like a todo REST API, with
net/http. - Don't fight
if err != nil. Embrace it. - Let
go fmtformat your code and stop thinking about style. - Read Go by Example when you forget the syntax for something.
I'm still early in my Go journey, but coming from TypeScript has made it way easier than I thought. If you're a frontend developer who's curious about the backend, Go is a great place to start.
Happy coding, fellow gopher! 🐹
