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
JavaScriptlet count = 1;
count = 2;
const name = "ada";
let empty; // undefined
var old = true; // avoid
→
Gocount := 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
JavaScripttypeof 42; // "number"
Number("42"); // 42
String(42); // "42"
parseInt("42", 10); // 42
(42).toFixed(2); // "42.00"
1 / 2; // 0.5
→
Goreflect.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
JavaScriptconst who = "world";
`Hello, ${who}!`;
"abc".toUpperCase();
"a,b".split(",");
["a", "b"].join("-");
"abc".includes("b");
"abc".trim().replace("a", "z");
"héllo".length; // 5
→
Gowho := "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
JavaScriptconst xs = [1, 2, 3];
xs.push(4);
xs.length;
xs[0];
xs[1] = 9;
xs.slice(1, 3);
const ys = [...xs];
xs.concat(ys);
→
Goxs := []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
JavaScriptxs.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);
→
Goout := 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
JavaScriptconst 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);
→
Goages := 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
JavaScriptconst user = {
name: "ada",
age: 36,
};
user.name;
JSON.stringify(user);
const back = JSON.parse(text);
→
Gotype 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
JavaScriptfunction 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));
→
Gofunc 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
JavaScriptif (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";
→
Goif 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
JavaScriptswitch (x) {
case 1:
one();
break;
case 2:
case 3:
few();
break;
default:
other();
}
→
Goswitch 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
JavaScript1 == "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
JavaScripttry {
const data = risky();
use(data);
} catch (err) {
console.error(err);
throw new Error("loading failed");
} finally {
cleanup();
}
→
Godefer 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
JavaScriptclass 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();
→
Gotype 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) {}
→
Gotype 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
JavaScriptasync function load() {
const a = await fetchA();
return a;
}
const [x, y] = await Promise.all([p1(), p2()]);
setTimeout(fn, 1000);
→
Goch := 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
JavaScriptawait Promise.race([work(), timeout(1000)]);
const c = new AbortController();
fetch(url, { signal: c.signal });
c.abort();
→
Goselect {
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.