PONY λ M2 Modula-2

TypeScript.CodeCompared.To/Go

An interactive executable cheatsheet comparing TypeScript and Go

TypeScript 6.0 Go 1.26.2
Hello World & Running
Hello, World
console.log("Hello, World!");
fmt.Println("Hello, World!")
Go is compiled and statically typed ahead of time, where TypeScript transpiles to JavaScript. Printing goes through the fmt package rather than a global console; a real program also declares package main and a func main() entry point.
Formatted output
const name = "Ada"; const age = 36; console.log(`${name} is ${age}`);
name := "Ada" age := 36 fmt.Printf("%s is %d\n", name, age)
Go has no string interpolation. Printf uses C-style verbs — %s for strings, %d for integers, %v for any value — and you must add the trailing \n yourself, unlike Println.
Variables & Types
Variable declaration
let count: number = 3; const label = "items"; console.log(count, label);
var count int = 3 label := "items" fmt.Println(count, label)
Go's := infers the type and declares in one step, like an untyped let. The explicit form is var count int = 3. There is no const/let mutability split — every variable is reassignable, and an unused local variable is a compile error.
Zero values
let total: number; let label: string; console.log(total, label);
var total int var label string fmt.Printf("%d %q\n", total, label)
A declared-but-unassigned variable in Go is never undefined — it takes its type's zero value: 0 for numbers, "" for strings, false for booleans, nil for pointers and slices. This removes a whole category of undefined bugs.
Explicit conversion
const count = 3; const text = String(count); const back = Number(text) + 1; console.log(text, back);
count := 3 text := strconv.Itoa(count) back, _ := strconv.Atoi(text) fmt.Println(text, back+1)
Go never converts between types implicitly — not even int to float64. Numbers and strings cross over through the strconv package, and Atoi returns a value and an error, which you discard here with the blank identifier _.
Strings
String operations
const text = "Hello, World"; console.log(text.toUpperCase()); console.log(text.includes("World")); console.log(text.length);
text := "Hello, World" fmt.Println(strings.ToUpper(text)) fmt.Println(strings.Contains(text, "World")) fmt.Println(len(text))
Strings in Go are immutable byte sequences with no methods of their own — operations live as functions in the strings package. Note that len counts bytes, not characters, so a multi-byte UTF-8 rune counts as more than one.
Runes vs characters
const text = "héllo"; console.log([...text].length); console.log([...text][1]);
text := "héllo" runes := []rune(text) fmt.Println(len(runes)) fmt.Printf("%c\n", runes[1])
To work with characters rather than bytes, convert a string to []rune — a rune is a Unicode code point (an int32). This is the Go counterpart to spreading a string into an array of code-point strings in TypeScript.
Numbers
Sized numeric types
const small = 200; const big = 2 ** 40; console.log(small, big);
var small uint8 = 200 var big int64 = 1 << 40 fmt.Println(small, big)
Where TypeScript has one number, Go has a family of sized integer and float types — int8 through int64, uint8, float32, float64. You pick the width, and overflow wraps rather than losing precision silently.
Integer division
console.log(7 / 2); console.log(Math.floor(7 / 2)); console.log(7 % 2);
fmt.Println(7.0 / 2.0) fmt.Println(7 / 2) fmt.Println(7 % 2)
Division of two integers in Go truncates toward zero — 7 / 2 is 3, not 3.5. To get a float result, at least one operand must be a float, as in 7.0 / 2.0.
Slices & Arrays
Slices
const numbers: number[] = [1, 2, 3]; numbers.push(4); console.log(numbers[0], numbers.length);
numbers := []int{1, 2, 3} numbers = append(numbers, 4) fmt.Println(numbers[0], len(numbers))
Go's growable list is the slice, []int. It has no push method; instead append returns a (possibly relocated) slice that you reassign. Every element must share the declared type — no mixed-type arrays.
Slicing
const items = [10, 20, 30, 40]; console.log(items.slice(1, 3)); console.log(items.at(-1));
items := []int{10, 20, 30, 40} fmt.Println(items[1:3]) fmt.Println(items[len(items)-1])
The slice expression items[1:3] is half-open, like slice(1, 3), but it returns a view that shares the underlying array rather than a copy. Go has no negative indexing, so the last element is items[len(items)-1].
No built-in map/filter
const numbers = [1, 2, 3, 4]; const doubled = numbers.map(n => n * 2); console.log(doubled);
numbers := []int{1, 2, 3, 4} doubled := make([]int, 0, len(numbers)) for _, n := range numbers { doubled = append(doubled, n*2) } fmt.Println(doubled)
Go slices have no map/filter/reduce methods — you write an explicit for...range loop. The range form yields an index and a value; _ discards the index. This verbosity is a deliberate Go preference for explicitness.
Maps
Map basics
const ages: Record<string, number> = { ada: 36 }; ages["bob"] = 40; console.log(ages["ada"]);
ages := map[string]int{"ada": 36} ages["bob"] = 40 fmt.Println(ages["ada"])
A Go map[string]int is a typed hash map — both key and value types are fixed at declaration. Unlike a TypeScript object, it is not an open struct; it is purely a dynamic key/value store.
Checking membership
const ages: Record<string, number> = { ada: 36 }; const value = ages["missing"]; console.log(value ?? "absent");
ages := map[string]int{"ada": 36} value, ok := ages["missing"] if !ok { fmt.Println("absent") } else { fmt.Println(value) }
Indexing a missing key returns the zero value, so you cannot tell "absent" from "present but zero" by the value alone. The "comma ok" idiom — value, ok := m[key] — gives a second boolean telling you whether the key existed.
Control Flow
if with initializer
const text = "42"; const value = Number(text); if (!Number.isNaN(value)) { console.log(value * 2); }
text := "42" if value, err := strconv.Atoi(text); err == nil { fmt.Println(value * 2) }
Go's if can declare a variable scoped to the statement before the condition. Parentheses around the condition are forbidden and the braces are mandatory — there is no single-line braceless if.
The only loop: for
for (let index = 0; index < 3; index++) { console.log(index); } let count = 0; while (count < 2) { count++; } console.log(count);
for index := 0; index < 3; index++ { fmt.Println(index) } count := 0 for count < 2 { count++ } fmt.Println(count)
Go has a single loop keyword, for. The three-clause form is familiar; drop the init and post clauses and it becomes a while; drop everything for an infinite loop. There is no separate while or do...while.
switch without fallthrough
const day = 6; let kind: string; switch (day) { case 0: case 6: kind = "weekend"; break; default: kind = "weekday"; } console.log(kind);
day := 6 var kind string switch day { case 0, 6: kind = "weekend" default: kind = "weekday" } fmt.Println(kind)
Go's switch does not fall through by default, so no break is needed, and one case can list several values. You can also write a tagless switch { case cond: } as a clean replacement for an if/else if chain.
Functions
Function definition
function add(a: number, b: number): number { return a + b; } console.log(add(2, 3));
func add(a, b int) int { return a + b } fmt.Println(add(2, 3))
The type comes after each parameter name, and consecutive same-typed parameters share one annotation (a, b int). The return type follows the parameter list. An explicit return is always required.
Multiple return values
function divmod(a: number, b: number): [number, number] { return [Math.floor(a / b), a % b]; } const [quotient, remainder] = divmod(17, 5); console.log(quotient, remainder);
func divmod(a, b int) (int, int) { return a / b, a % b } quotient, remainder := divmod(17, 5) fmt.Println(quotient, remainder)
Go functions return multiple values natively — no tuple type or array wrapper. This is the foundation of Go's idiomatic error handling, where a function returns (result, error) as two separate values.
defer
function process(): void { try { console.log("working"); } finally { console.log("cleanup"); } } process();
func process() { defer fmt.Println("cleanup") fmt.Println("working") } process()
A deferred call runs when the surrounding function returns, no matter how — the Go answer to finally. Deferred calls run in last-in-first-out order, which keeps resource cleanup right next to the acquisition that needs it.
Structs & Methods
Structs
interface Point { x: number; y: number; } const point: Point = { x: 3, y: 4 }; console.log(point.x, point.y);
type Point struct { X int Y int } point := Point{X: 3, Y: 4} fmt.Println(point.X, point.Y)
A Go struct is the equivalent of a TypeScript interface used as a record, but it is a concrete value type, not just a shape. A capitalized field name (X) is exported (public); a lowercase one is package-private.
Methods on structs
class Point { constructor(public x: number, public y: number) {} distance(): number { return Math.hypot(this.x, this.y); } } console.log(new Point(3, 4).distance());
type Point struct{ X, Y float64 } func (p Point) Distance() float64 { return math.Hypot(p.X, p.Y) } fmt.Println(Point{3, 4}.Distance())
Go has no classes. A method is a plain function with a receiver in parentheses before its name — (p Point) binds p like this. Methods live outside the type definition, decoupling data from behavior.
Pointer receivers
class Counter { value = 0; increment(): void { this.value++; } } const counter = new Counter(); counter.increment(); console.log(counter.value);
type Counter struct{ Value int } func (c *Counter) Increment() { c.Value++ } counter := &Counter{} counter.Increment() fmt.Println(counter.Value)
To mutate the receiver, a method takes a pointer receiver (c *Counter); a value receiver would mutate a copy. Go makes the value-vs-reference distinction explicit, where JavaScript objects are always shared references.
Interfaces
Implicit interfaces
interface Speaker { speak(): string; } class Dog implements Speaker { speak(): string { return "Woof"; } } function announce(s: Speaker): string { return s.speak(); } console.log(announce(new Dog()));
type Speaker interface { Speak() string } type Dog struct{} func (d Dog) Speak() string { return "Woof" } func announce(s Speaker) string { return s.Speak() } fmt.Println(announce(Dog{}))
Go interfaces are satisfied implicitly: Dog has a Speak() string method, so it is a Speaker with no implements clause. This is structural typing like TypeScript's, but enforced by the compiler at the point of use.
Type switches
function describe(value: unknown): string { if (typeof value === "number") return "number"; if (typeof value === "string") return "string"; return "other"; } console.log(describe(42));
func describe(value any) string { switch value.(type) { case int: return "number" case string: return "string" default: return "other" } } fmt.Println(describe(42))
The empty interface any (an alias for interface{}) holds a value of any type, like TypeScript's unknown. A type switch — switch value.(type) — recovers the concrete type at runtime, the Go counterpart of narrowing with typeof.
Error Handling
Errors are values
function parse(text: string): number { const value = Number(text); if (Number.isNaN(value)) throw new Error("bad number"); return value; } try { console.log(parse("abc")); } catch (error) { console.log((error as Error).message); }
func parse(text string) (int, error) { value, err := strconv.Atoi(text) if err != nil { return 0, fmt.Errorf("bad number: %w", err) } return value, nil } if value, err := parse("abc"); err != nil { fmt.Println(err) } else { fmt.Println(value) }
Go has no exceptions for ordinary failures. A function returns an error value alongside its result, and the caller checks if err != nil. The explicit %w verb wraps an underlying error so it can be unwrapped later.
panic & recover
function risky(): void { throw new Error("unexpected"); } try { risky(); } catch (error) { console.log("recovered:", (error as Error).message); }
func safe() { defer func() { if r := recover(); r != nil { fmt.Println("recovered:", r) } }() panic("unexpected") } safe()
panic and recover resemble throw/catch, but Go reserves them for truly unrecoverable bugs, not routine errors. recover only works inside a deferred function — the idiom that stops a panic from crashing the program.
Concurrency
Goroutines
async function work(label: string): Promise<void> { console.log(label); } (async () => { await Promise.all([work("a"), work("b")]); })();
var waitGroup sync.WaitGroup for _, label := range []string{"a", "b"} { waitGroup.Add(1) go func(name string) { defer waitGroup.Done() fmt.Println(name) }(label) } waitGroup.Wait()
The go keyword starts a goroutine — a lightweight thread scheduled by the Go runtime, far cheaper than an OS thread. Unlike await, the call returns immediately; a sync.WaitGroup blocks until all the spawned goroutines finish.
Channels
function produce(): Promise<number> { return Promise.resolve(42); } (async () => { const value = await produce(); console.log(value); })();
results := make(chan int) go func() { results <- 42 }() value := <-results fmt.Println(value)
Channels are typed pipes that goroutines use to communicate and synchronize: results <- 42 sends, <-results receives and blocks until a value arrives. Go's motto is "share memory by communicating," replacing shared-state locking with message passing.
Generics
Generic functions
function mapArray<T, U>(items: T[], fn: (item: T) => U): U[] { return items.map(fn); } console.log(mapArray([1, 2, 3], n => n * 2));
func MapSlice[T any, U any](items []T, fn func(T) U) []U { result := make([]U, len(items)) for i, item := range items { result[i] = fn(item) } return result } fmt.Println(MapSlice([]int{1, 2, 3}, func(n int) int { return n * 2 }))
Go added generics in 1.18 with square-bracket type parameters, much like TypeScript. The constraint any means "any type"; more specific constraints such as comparable or a custom interface limit what operations the body may use.
Type constraints
function max<T extends number | string>(a: T, b: T): T { return a > b ? a : b; } console.log(max(3, 9));
type Ordered interface { ~int | ~float64 | ~string } func Max[T Ordered](a, b T) T { if a > b { return a } return b } fmt.Println(Max(3, 9))
A constraint is an interface listing the permitted types with a union. The ~int tilde means "any type whose underlying type is int," so named types qualify too. This is stricter and more explicit than TypeScript's extends constraint.