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.