JS → GO

A phrasebook for JavaScript developers

JavaScript
→Go

Look up something you already write every day, and read the Go sentence that means the same thing — plus a note on where the translation stops being literal.

→

Getting a program to run

package main
JavaScript
// hello.js — run: node hello.js

console.log("Hello, world");

// any file can be the entry point,
// and top-level code just runs
→
Go
// hello.go — run: go run hello.go

package main

import "fmt"

func main() {
	fmt.Println("Hello, world")
}

Every Go file belongs to a package. Only package main with a func main() builds into a runnable program. There is no top-level code — statements live inside functions.

Declaring variables

:= and var
JavaScript
let count = 1;
count = 2;

const name = "ada";

let empty;        // undefined
var old = true;   // avoid
→
Go
count := 1        // inside a function
count = 2

const name = "ada"

var empty string  // "" — the zero value
var n int         // 0
var ok bool       // false

:= declares and infers the type in one step, but only inside a function; at package level use var. There is no undefined — every variable starts at its type's zero value. A declared local you never read is a compile error, not a warning.

Types and conversion

strconv
JavaScript
typeof 42;          // "number"

Number("42");       // 42
String(42);         // "42"
parseInt("42", 10); // 42

(42).toFixed(2);    // "42.00"
1 / 2;              // 0.5
→
Go
reflect.TypeOf(42)         // int

n, err := strconv.Atoi("42")
s := strconv.Itoa(42)
f, err := strconv.ParseFloat("4.2", 64)

fmt.Sprintf("%.2f", 42.0)  // "42.00"
1 / 2                      // 0 — integer division
float64(1) / 2             // 0.5

One number becomes int, int64, float64 and friends. Conversion is always explicit — float64(i), int(f) — and never happens behind your back, so mixing an int and a float64 in one expression will not compile. int(2.9) truncates to 2. Parsing returns a value and an error.

Working with strings

strings, fmt
JavaScript
const who = "world";
`Hello, ${who}!`;

"abc".toUpperCase();
"a,b".split(",");
["a", "b"].join("-");
"abc".includes("b");
"abc".trim().replace("a", "z");

"héllo".length;   // 5
→
Go
who := "world"
fmt.Sprintf("Hello, %s!", who)

strings.ToUpper("abc")
strings.Split("a,b", ",")
strings.Join([]string{"a", "b"}, "-")
strings.Contains("abc", "b")
strings.ReplaceAll(strings.TrimSpace(s), "a", "z")

len("héllo")                    // 6 bytes
utf8.RuneCountInString("héllo") // 5

Strings are immutable UTF-8 bytes. len counts bytes; ranging over a string yields runes (code points) with their byte offsets. There are no methods on strings — the verbs live in the strings package. Building a string in a loop wants strings.Builder, not +=.

Lists

slices
JavaScript
const xs = [1, 2, 3];

xs.push(4);
xs.length;
xs[0];
xs[1] = 9;

xs.slice(1, 3);
const ys = [...xs];
xs.concat(ys);
→
Go
xs := []int{1, 2, 3}

xs = append(xs, 4)   // reassign!
len(xs)
xs[0]
xs[1] = 9

xs[1:3]              // a view, shares memory
ys := make([]int, len(xs))
copy(ys, xs)         // a real copy
append(xs, ys...)

append returns a new slice header, so you must assign the result back. Slicing does not copy: xs[1:3] looks into the same underlying array, and writing through it changes the original. [3]int is a fixed-size array and a genuinely different type from the slice []int.

map, filter, reduce

write the loop
JavaScript
xs.map(x => x * 2);

xs.filter(x => x > 1);

xs.reduce((a, b) => a + b, 0);

xs.includes(2);
xs.indexOf(2);
xs.sort((a, b) => a - b);
→
Go
out := make([]int, 0, len(xs))
for _, x := range xs {
	out = append(out, x*2)
}

sum := 0
for _, x := range xs {
	sum += x
}

slices.Contains(xs, 2)
slices.Index(xs, 2)
slices.Sort(xs)

There is no built-in map/filter/reduce, and idiomatic Go writes the loop rather than reaching for a helper. What generics did add is the slices and maps packages, which cover sorting, searching, comparing and cloning.

Key–value lookup

map[K]V
JavaScript
const ages = { ada: 36 };

ages.ada;
ages["ada"];
ages.grace = 45;
delete ages.ada;
"ada" in ages;
Object.keys(ages);

const m = new Map();
m.set("k", 1);
→
Go
ages := map[string]int{"ada": 36}

ages["ada"]
v, ok := ages["ada"]  // ok reports presence
ages["grace"] = 45
delete(ages, "ada")
len(ages)

m := make(map[string]int)
m["k"] = 1            // nil map would panic

Reading a missing key returns the zero value, not undefined, so ages["nobody"] is 0. The comma-ok form is how you tell "absent" from "present and zero". Iteration order is deliberately randomised, and a nil map is fine to read but panics on write — make it first.

Objects with a shape

struct
JavaScript
const user = {
  name: "ada",
  age: 36,
};

user.name;

JSON.stringify(user);
const back = JSON.parse(text);
→
Go
type User struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

u := User{Name: "ada", Age: 36}
u.Name

b, err := json.Marshal(u)
err = json.Unmarshal(text, &back)

The struct is the everyday object: fixed fields, fixed types, checked at compile time. Tags in backticks control JSON names. A field is visible outside its package — and to encoding/json — only if it starts with a capital letter, which is the single most common reason a struct marshals to {}.

Functions

multiple returns
JavaScript
function add(a, b) { return a + b; }
const add2 = (a, b) => a + b;

function greet(name = "you") {}
function sum(...nums) {}

const { a, b } = pair();

const twice = f => x => f(f(x));
→
Go
func add(a, b int) int { return a + b }
add2 := func(a, b int) int { return a + b }

// no default arguments — use an options
// struct or a second constructor
func sum(nums ...int) int { ... }

a, b := pair()   // two return values

func twice(f func(int) int) func(int) int { ... }

No default or named parameters; the usual answers are a config struct or variadic option functions. Returning several values is native, and that is how errors travel. defer f.Close() schedules cleanup to run when the function exits, whichever way it exits.

Copies and pointers

& and *
JavaScript
// the engine decides for you:
// objects by reference, primitives by value

function rename(u) { u.name = "new"; }
rename(user);      // user is changed

let n = 1;
function inc(x) { x++; }
inc(n);            // n is still 1
→
Go
// everything is passed by value.
// you choose when to pass an address.

func rename(u *User) { u.Name = "new" }
rename(&u)         // u is changed

func inc(x *int) { *x++ }
inc(&n)            // n is now 2

p := &u
p.Name = "ada"     // auto-dereferenced

A struct argument is a copy unless you pass a pointer, which you do when the function must mutate the original or the value is large. Slices, maps and channels already hold an internal reference, so they behave the way you expect. There is no pointer arithmetic and there is a garbage collector: this is only & and *.

Branches and loops

one keyword: for
JavaScript
if (x > 1) {} else if (y) {} else {}

for (let i = 0; i < 10; i++) {}
while (ok) {}
do {} while (ok);

for (const x of xs) {}
for (const k in obj) {}

const size = x > 1 ? "big" : "small";
→
Go
if x > 1 { } else if y { } else { }
if v, err := f(); err != nil { }  // scoped init

for i := 0; i < 10; i++ { }
for ok { }                        // while
for { break }                     // infinite

for i, x := range xs { }
for k, v := range m { }

// no ternary — write the if

for is the only loop keyword and covers all four shapes. Conditions take no parentheses, braces are never optional, and the opening brace must sit on the same line. The scoped init form — if v, err := f(); err != nil — keeps v and err from leaking into the surrounding scope.

switch

no break needed
JavaScript
switch (x) {
  case 1:
    one();
    break;
  case 2:
  case 3:
    few();
    break;
  default:
    other();
}
→
Go
switch x {
case 1:
	one()
case 2, 3:
	few()
default:
	other()
}

switch {              // no subject:
case x > 10:          // an if/else chain
	big()
}

Cases break on their own; falling through is opt-in with the fallthrough keyword. One case can list several values, a case can carry an init statement, and a subjectless switch is the idiomatic replacement for a long if/else ladder.

Equality and truthiness

no coercion
JavaScript
1 == "1";        // true
1 === "1";       // false

if (list.length) {}
if (str) {}
if (!user) {}

value ?? fallback
user?.address?.city
→
Go
// == is always strict.
// comparing different types won't compile.

if len(list) > 0 { }
if str != "" { }
if user == nil { }

// no ?? and no ?. — check it
if v, ok := m[k]; ok { use(v) }
if u != nil && u.Address != nil { }

There is no truthiness: if takes a bool and nothing else, so every check is spelled out. Structs and arrays compare with == field by field; slices, maps and functions can only be compared against nil, and reflect.DeepEqual or slices.Equal handles the rest.

Things going wrong

errors are values
JavaScript
try {
  const data = risky();
  use(data);
} catch (err) {
  console.error(err);
  throw new Error("loading failed");
} finally {
  cleanup();
}
→
Go
defer cleanup()

data, err := risky()
if err != nil {
	return fmt.Errorf("loading: %w", err)
}
use(data)

errors.New("boom")
errors.Is(err, os.ErrNotExist)
panic("unrecoverable")   // rare, not control flow

Nothing is thrown. An error is an ordinary value returned next to the result, and the if err != nil block you write every few lines is the idiom, not a sign you are doing it wrong. %w wraps an error so errors.Is and errors.As can look through the chain later.

Classes

methods and embedding
JavaScript
class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  speak() { return `${this.name} barks`; }
}

new Dog("rex").speak();
→
Go
type Animal struct{ Name string }

func NewAnimal(name string) *Animal {
	return &Animal{Name: name}
}

func (a *Animal) Speak() string {
	return a.Name + " makes a sound"
}

type Dog struct{ Animal }  // embedding

func (d *Dog) Speak() string {
	return d.Name + " barks"
}

No classes, no new keyword for your own types, no inheritance. A method is a function with a receiver, and the receiver replaces this — you name it yourself, and it is a pointer whenever the method mutates. Embedding promotes the inner type's fields and methods onto the outer one: composition, not a parent. A constructor is just a function called NewDog.

Duck typing

implicit interfaces
JavaScript
// anything with .speak() will do,
// and you find out at runtime

function speakAll(items) {
  for (const i of items) {
    console.log(i.speak());
  }
}

if (x instanceof Dog) {}
→
Go
type Speaker interface {
	Speak() string
}

func speakAll(items []Speaker) {
	for _, i := range items {
		fmt.Println(i.Speak())
	}
}

if d, ok := x.(*Dog); ok { use(d) }

There is no implements. A type satisfies an interface simply by having the methods, which means you can write an interface for someone else's type after the fact. Keep them small — io.Reader is one method. any holds anything, and you get the concrete type back with an assertion or a switch v := x.(type).

Modules and visibility

go.mod
JavaScript
// greet.js
export function hello() {}
export default thing;

// app.js
import { hello } from "./greet.js";
import thing from "some-pkg";

// package.json, node_modules/
→
Go
// greet/greet.go
package greet

func Hello() {}   // exported: capital H
func local()  {}  // package-private

// main.go
import (
	"fmt"
	"example.com/app/greet"
)

// go.mod, shared module cache

Visibility is spelled with a capital letter rather than an export keyword, so renaming hello to Hello publishes it. A directory is a package and its import path is the module path plus that directory. Unused imports fail the build, which is why everyone runs goimports on save.

Doing things at the same time

goroutines
JavaScript
async function load() {
  const a = await fetchA();
  return a;
}

const [x, y] = await Promise.all([p1(), p2()]);

setTimeout(fn, 1000);
→
Go
ch := make(chan string)
go func() { ch <- fetchA() }()
a := <-ch          // blocks until ready

var wg sync.WaitGroup
wg.Add(2)
go func() { defer wg.Done(); p1() }()
go func() { defer wg.Done(); p2() }()
wg.Wait()

time.Sleep(time.Second)

No event loop, and no coloured functions: any ordinary function becomes concurrent by putting go in front of the call. Channels move values between goroutines, and an unbuffered send blocks until someone receives — that blocking is the synchronisation. If you would rather share memory, take a sync.Mutex. Nothing waits for your goroutines when main returns.

Racing and cancelling

select, context
JavaScript
await Promise.race([work(), timeout(1000)]);

const c = new AbortController();
fetch(url, { signal: c.signal });
c.abort();
→
Go
select {
case v := <-work:
	use(v)
case <-time.After(time.Second):
	return errTimeout
}

ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
req = req.WithContext(ctx)

select waits on several channel operations and proceeds with whichever is ready first, so it covers both Promise.race and a non-blocking poll (with a default case). context carries deadlines and cancellation down a call chain and is, by convention, the first parameter of anything that blocks or touches the network.

Tests

go test
JavaScript
// add.test.js
import { test, expect } from "vitest";

test("adds", () => {
  expect(add(1, 2)).toBe(3);
});

// npx vitest
→
Go
// add_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
	if got := add(1, 2); got != 3 {
		t.Errorf("add(1, 2) = %d, want 3", got)
	}
}

// go test ./...

The runner ships with the toolchain: no framework, no config, no dependency. Files end in _test.go, functions start with Test and take *testing.T. There are no assertion helpers on purpose — you compare values and call t.Errorf with a message that says what you got and what you wanted.

The rest of the toolbox

JavaScriptGo
npm installgo get · go mod tidy
package.jsongo.mod
node_modules/a shared module cache
npm run buildgo build ./...
node app.jsgo run .
prettiergofmt — built in, and not configurable
eslintgo vet · staticcheck
jest · vitestgo test ./...
tscthe compiler — types aren't optional
nodemonair, or just rebuild; it's fast
JSDocgo doc · pkg.go.dev
esbuild + a Dockerfilego build — one static binary

Nothing matches . Try append, goroutine or nil.