Output & Running
Hello, World
console.log("Hello, World!"); fmt.Println("Hello, World!") Both
console.log() and fmt.Println() append a trailing newline. Every Go program requires a package main declaration and a func main() entry point — the runner adds this boilerplate automatically.Formatted output
const name = "Alice";
const age = 30;
console.log(`Name: ${name}, Age: ${age}`);
console.log("Pi is approximately", Math.PI.toFixed(4)); name := "Alice"
age := 30
fmt.Printf("Name: %s, Age: %d\n", name, age)
fmt.Printf("Pi is approximately %.4f\n", 3.14159265) Go uses
fmt.Printf() with % format verbs instead of template literals. Common verbs: %s for strings, %d for integers, %f for floats, %v for any value, %T for the type, and %#v for Go syntax representation. Unlike JavaScript's template literals, fmt.Printf() does not add a trailing newline — the explicit \n is required.Build a formatted string
const product = "Widget";
const price = 9.99;
const summary = `${product} costs $${price.toFixed(2)}`;
console.log(summary); product := "Widget"
price := 9.99
summary := fmt.Sprintf("%s costs $%.2f", product, price)
fmt.Println(summary) fmt.Sprintf() builds a formatted string without printing it — the equivalent of using a template literal to construct a string before passing it elsewhere. It returns a string value. The %.2f verb formats a float with exactly two decimal places.Print any value with %v
const point = { x: 3, y: 4 };
const numbers = [1, 2, 3];
console.log(point);
console.log(numbers);
console.log(typeof 42, typeof "hello", typeof true); type Point struct{ X, Y int }
point := Point{3, 4}
numbers := []int{1, 2, 3}
fmt.Printf("%v\n", point)
fmt.Printf("%v\n", numbers)
fmt.Printf("%T %T %T\n", 42, "hello", true) The
%v verb formats any Go value in its default representation — structs as {3 4}, slices as [1 2 3]. %T prints the type name (main.Point, []int, bool). The %#v verb produces Go syntax output useful for debugging.Variables & Types
:= — short variable declaration
// JS: let for mutable, const for immutable binding
let counter = 0;
const greeting = "Hello";
counter = 1;
console.log(counter, greeting); // Go: := infers the type and declares a new variable
counter := 0
greeting := "Hello"
counter = 1 // reassign (no := on reassignment)
fmt.Println(counter, greeting) The
:= operator both declares a variable and assigns its initial value, inferring the type from the right-hand side. It is equivalent to JavaScript's let. On subsequent lines, plain = reassigns without redeclaring. In Go, all declared variables must be used — declaring a variable you never read is a compile error, unlike JavaScript.Explicit type annotations
// JS has no compile-time types; TypeScript adds them optionally
/** @type {string} */
let username = "alice";
/** @type {number} */
let score = 0;
console.log(username, score); // Go: var name type = value (type annotation is explicit)
var username string = "alice"
var score int = 0
fmt.Println(username, score) Go's
var keyword allows explicit type annotation: var name type. Type annotations are required when you want a different type than inference would give — for example, declaring a float64 variable initialized to 0 (which inference would make int). In practice, := with type inference is preferred when the type is obvious from context.Zero values — no undefined
// JS: uninitialized variables are undefined
let counter;
let message;
let active;
console.log(counter); // undefined
console.log(message); // undefined
console.log(active); // undefined // Go: every variable has a well-defined zero value
var counter int
var message string
var active bool
fmt.Println(counter) // 0
fmt.Println(message) // "" (empty string)
fmt.Println(active) // false Go has no
undefined. Every type has a zero value: 0 for numeric types, "" for strings, false for booleans, nil for pointers/slices/maps/channels/interfaces. Variables declared without an explicit initial value always start at their type's zero value. This eliminates an entire class of bugs where JavaScript code fails because a variable was read before being assigned.const — compile-time constants
// JS: const prevents reassignment but is evaluated at runtime
const MAX_RETRIES = 3;
const PI_APPROX = 3.14159;
const APP_NAME = "MyApp";
console.log(MAX_RETRIES, PI_APPROX, APP_NAME); // Go: const is evaluated at compile time
const maxRetries = 3
const piApprox = 3.14159
const appName = "MyApp"
fmt.Println(maxRetries, piApprox, appName) Go's
const values are computed at compile time and can only hold basic types (numbers, strings, booleans). Unlike JavaScript's const, which merely prevents reassignment of the binding, Go constants are true compile-time constants — they can be used in array sizes, type definitions, and other positions where only compile-time values are accepted. Go conventionally uses camelCase for constants, not SCREAMING_SNAKE_CASE.Multiple assignment
// JS: destructuring for multiple assignment
let [first, second] = [1, 2];
console.log(first, second);
// Swap values
[first, second] = [second, first];
console.log(first, second); // Go: multiple assignment with comma
first, second := 1, 2
fmt.Println(first, second)
// Swap values — no temp variable needed
first, second = second, first
fmt.Println(first, second) Go supports multiple assignment in a single statement, which is especially useful for swapping values and for receiving multiple return values from functions. The right-hand side is evaluated fully before any assignment occurs, making
a, b = b, a a correct swap without a temporary variable.Type conversion — always explicit
// JS: implicit coercion is common (and often surprising)
const number = 42;
const text = String(number); // explicit
const asFloat = number + 0.5; // implicit int→float
console.log(text, typeof text);
console.log(asFloat); // Go: every conversion must be explicit — no coercion
number := 42
text := strconv.Itoa(number) // int → string
asFloat := float64(number) + 0.5 // int → float64 (explicit)
fmt.Println(text, asFloat) Go has no implicit type coercion. Mixing
int and float64 in arithmetic is a compile error — one must be explicitly converted first. String-to-number and number-to-string conversions go through the strconv package. This strictness eliminates JavaScript's notorious coercion surprises ("1" + 1 === "11", [] + {} === "[object Object]", etc.).Strings
String literals and raw strings
const normal = "Hello\nWorld"; // escape sequences
const raw = String.raw`Hello\nWorld`; // no escapes
const multiline = `line one
line two`;
console.log(normal);
console.log(raw);
console.log(multiline); normal := "Hello\nWorld" // escape sequences
raw := `Hello\nWorld` // backtick: raw string, no escapes
multiline := `line one
line two`
fmt.Println(normal)
fmt.Println(raw)
fmt.Println(multiline) Go uses backtick-delimited raw string literals (
` + "`" + `...` + "`" + `) where backslash sequences are treated as literal characters. JavaScript uses String.raw or template literals for multiline strings. In Go, backtick strings can span multiple lines and are a common way to embed JSON, SQL, or other content with backslashes.String concatenation and building
// Simple concatenation
const firstName = "Jane";
const lastName = "Doe";
const fullName = firstName + " " + lastName;
console.log(fullName);
// Building many strings efficiently
const parts = ["Go", "is", "fast"];
console.log(parts.join(" ")); // Simple concatenation
firstName := "Jane"
lastName := "Doe"
fullName := firstName + " " + lastName
fmt.Println(fullName)
// Building many strings — strings.Builder avoids repeated allocations
var builder strings.Builder
for index, word := range []string{"Go", "is", "fast"} {
if index > 0 { builder.WriteString(" ") }
builder.WriteString(word)
}
fmt.Println(builder.String()) The
+ operator works for simple concatenation in both languages. For building strings in a loop, Go uses strings.Builder — appending with + in a loop creates a new allocation each iteration, just as in JavaScript. strings.Join() is the direct equivalent of JavaScript's Array.prototype.join() for joining a slice of strings.Common string operations
const sentence = " Hello, Go World! ";
console.log(sentence.trim());
console.log(sentence.trim().toUpperCase());
console.log(sentence.includes("Go"));
console.log(sentence.trim().startsWith("Hello"));
console.log(sentence.trim().replace("Go", "JavaScript"));
console.log("a,b,c".split(",")); sentence := " Hello, Go World! "
fmt.Println(strings.TrimSpace(sentence))
fmt.Println(strings.ToUpper(strings.TrimSpace(sentence)))
fmt.Println(strings.Contains(sentence, "Go"))
fmt.Println(strings.HasPrefix(strings.TrimSpace(sentence), "Hello"))
fmt.Println(strings.Replace(strings.TrimSpace(sentence), "Go", "JavaScript", 1))
fmt.Println(strings.Split("a,b,c", ",")) Go's string operations live in the
strings package as free functions rather than methods on a string value. The pattern is strings.FunctionName(str, args) rather than str.methodName(args). The third argument to strings.Replace() limits how many replacements to make; pass -1 to replace all occurrences (like JavaScript's replaceAll()).String ↔ number conversion
// String to number
const text = "42";
const number = parseInt(text, 10);
const asFloat = parseFloat("3.14");
console.log(number + 1);
console.log(asFloat + 1);
// Number to string
console.log(String(number));
console.log((3.14159).toFixed(2)); // String to number
text := "42"
number, err := strconv.Atoi(text)
if err != nil { fmt.Println("parse error:", err); return }
asFloat, _ := strconv.ParseFloat("3.14", 64)
fmt.Println(number + 1)
fmt.Println(asFloat + 1)
// Number to string
fmt.Println(strconv.Itoa(number))
fmt.Println(fmt.Sprintf("%.2f", 3.14159)) strconv.Atoi() (ASCII to integer) parses a decimal string; it returns (int, error) — the caller must check the error, unlike JavaScript's parseInt() which returns NaN on failure. strconv.ParseFloat() takes a bit size (32 or 64) to control precision. The blank identifier _ discards the error when you are certain the input is valid.Iterating over strings — runes vs bytes
const word = "café";
// JS strings are UTF-16; spread gives Unicode code points
for (const char of word) {
process.stdout.write(char + " ");
}
console.log();
console.log("length (code units):", word.length);
console.log("char count:", [...word].length); word := "café"
// range over a string yields runes (Unicode code points)
for _, character := range word {
fmt.Printf("%c ", character)
}
fmt.Println()
fmt.Println("byte length:", len(word)) // len counts bytes, not chars
fmt.Println("rune count:", len([]rune(word))) // cast to rune slice for char count Go strings are byte slices encoded as UTF-8.
len(s) returns the byte count, not the character count — the same as JavaScript's str.length for ASCII strings but different for multi-byte characters like é (2 bytes in UTF-8). A range loop over a string automatically decodes UTF-8 runes (Unicode code points), similar to iterating with for...of in JavaScript.Numbers
Numeric types — Go has many; JS has one
// JS has one number type (64-bit float) and BigInt
const integer = 42;
const decimal = 3.14;
const large = 9007199254740993n; // BigInt
console.log(integer, decimal, large);
console.log(typeof integer); // "number" — same type as decimal // Go has distinct integer and float types
integer := 42 // int (platform word size, usually 64-bit)
decimal := 3.14 // float64
var small int32 = 100 // explicitly 32-bit integer
var large int64 = 9007199254740993
fmt.Println(integer, decimal, small, large)
fmt.Printf("%T %T %T %T\n", integer, decimal, small, large) JavaScript has a single
number type (IEEE 754 64-bit float) that represents all numbers, plus BigInt for arbitrary precision. Go has separate integer types (int, int8, int16, int32, int64) and float types (float32, float64). Mixing them in arithmetic is a compile error — you must cast explicitly. The plain int type is 64-bit on most modern platforms.Integer division — truncates, never floats
// JS: / always produces a float
console.log(7 / 2); // 3.5
console.log(10 / 3); // 3.333...
// Floor division in JS requires Math.floor
console.log(Math.floor(7 / 2)); // 3
console.log(Math.floor(-7 / 2)); // -4 // Go: integer / integer truncates toward zero
fmt.Println(7 / 2) // 3
fmt.Println(10 / 3) // 3
fmt.Println(-7 / 2) // -3 (truncates toward zero, not floor)
fmt.Println(7 % 2) // 1 (remainder)
// Float division requires at least one float operand
fmt.Println(7.0 / 2) // 3.5 In Go, dividing two integers always produces an integer, truncating toward zero — there is no automatic promotion to float. This matches most other statically-typed languages and is a common source of bugs for JavaScript developers who expect
7 / 2 to equal 3.5. To get a float result, cast one operand to float64 first: float64(7) / 2.Math operations — math package
console.log(Math.abs(-5));
console.log(Math.sqrt(16));
console.log(Math.pow(2, 10));
console.log(Math.floor(3.7));
console.log(Math.ceil(3.2));
console.log(Math.min(3, 1, 4, 1, 5));
console.log(Math.max(3, 1, 4, 1, 5)); fmt.Println(math.Abs(-5))
fmt.Println(math.Sqrt(16))
fmt.Println(math.Pow(2, 10))
fmt.Println(math.Floor(3.7))
fmt.Println(math.Ceil(3.2))
fmt.Println(math.Min(3, 1)) // only 2 arguments
fmt.Println(math.Max(3, 1)) // only 2 arguments Go's
math package mirrors JavaScript's Math object for most operations. A key difference: math.Min() and math.Max() each take exactly two arguments — there is no variadic version. To find the minimum of a slice, use a loop. All math functions operate on float64; pass integer arguments with float64(n) if needed.Slices & Arrays
Slice creation — the dynamic array
// JS: Array is always dynamic (no fixed size)
const numbers = [1, 2, 3, 4, 5];
const empty = [];
const filled = new Array(5).fill(0);
console.log(numbers);
console.log(empty.length);
console.log(filled); // Go: slices are dynamic; arrays have fixed size (rare)
numbers := []int{1, 2, 3, 4, 5}
empty := []int{} // empty slice
filled := make([]int, 5) // length 5, all zeros
fmt.Println(numbers)
fmt.Println(len(empty))
fmt.Println(filled) Go has both arrays (
[5]int, fixed size, rare) and slices ([]int, dynamic, the idiomatic choice). Slices are the Go equivalent of JavaScript arrays. make([]int, length) creates a zero-filled slice of a given length. make([]int, length, capacity) also preallocates backing storage to avoid reallocations during growth.append — growing a slice
const fruits = ["apple", "banana"];
fruits.push("cherry");
fruits.push("date", "elderberry");
console.log(fruits);
console.log(fruits.length); fruits := []string{"apple", "banana"}
fruits = append(fruits, "cherry")
fruits = append(fruits, "date", "elderberry")
fmt.Println(fruits)
fmt.Println(len(fruits)) append() returns a new (or possibly reallocated) slice — the result must always be assigned back. Unlike JavaScript's Array.push() which mutates in place, append() may allocate a new backing array if the current capacity is exceeded. append() accepts variadic elements, so append(sliceA, sliceB...) concatenates two slices (the ... unpacks the second slice).Slice operations — len, range, sub-slicing
const numbers = [10, 20, 30, 40, 50];
console.log(numbers.length);
console.log(numbers.slice(1, 3)); // [20, 30]
console.log(numbers.slice(2)); // [30, 40, 50]
console.log(numbers.indexOf(30));
console.log(numbers.includes(40)); numbers := []int{10, 20, 30, 40, 50}
fmt.Println(len(numbers))
fmt.Println(numbers[1:3]) // [20 30] — same as JS slice(1,3)
fmt.Println(numbers[2:]) // [30 40 50]
// Go has no built-in indexOf; use a loop or slices package
index := -1
for i, value := range numbers {
if value == 30 { index = i; break }
}
fmt.Println(index) Go's slice expression
s[low:high] returns a sub-slice sharing the same underlying array (no copy), whereas JavaScript's Array.slice() always returns a new array. This means mutating the sub-slice also mutates the original. Go's standard library (Go 1.21+) includes a slices package with slices.Index() and slices.Contains().Iterating a slice
const colors = ["red", "green", "blue"];
// for...of — value only
for (const color of colors) {
console.log(color);
}
// forEach — index and value
colors.forEach((color, index) => {
console.log(index, color);
}); colors := []string{"red", "green", "blue"}
// range with value only (blank the index with _)
for _, color := range colors {
fmt.Println(color)
}
// range with index and value
for index, color := range colors {
fmt.Println(index, color)
} Go's
for ... range construct replaces JavaScript's for...of, forEach(), and index-based for loops for iterating slices. range yields two values: the index and a copy of the element. Use _ to discard whichever you don't need. Go has no built-in map(), filter(), or reduce() — these are idiomatic for loops in Go.Sorting slices
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
numbers.sort((a, b) => a - b);
console.log(numbers);
const words = ["banana", "apple", "cherry"];
words.sort();
console.log(words); numbers := []int{3, 1, 4, 1, 5, 9, 2, 6}
sort.Ints(numbers)
fmt.Println(numbers)
words := []string{"banana", "apple", "cherry"}
sort.Strings(words)
fmt.Println(words) The
sort package provides sort.Ints(), sort.Strings(), and sort.Float64s() for the common cases. For custom ordering, sort.Slice(s, func(i, j int) bool { ... }) takes a comparator function — similar to JavaScript's Array.sort() comparator but returning a boolean instead of a number. sort.Slice() sorts in place.Maps & Objects
Map creation and basic operations
// JS: object literal or Map
const scores = { alice: 95, bob: 87, carol: 92 };
console.log(scores["alice"]);
scores["dave"] = 78;
delete scores["bob"];
console.log("alice" in scores);
console.log("bob" in scores); // Go: map[KeyType]ValueType
scores := map[string]int{
"alice": 95,
"bob": 87,
"carol": 92,
}
fmt.Println(scores["alice"])
scores["dave"] = 78
delete(scores, "bob")
_, aliceExists := scores["alice"]
_, bobExists := scores["bob"]
fmt.Println(aliceExists, bobExists) Go maps have typed keys and values —
map[string]int maps string keys to integer values. Looking up a missing key returns the value type's zero value (0 for int) without error — similar to how accessing a missing property in a JavaScript object returns undefined. The two-value form value, ok := m[key] distinguishes "key present with zero value" from "key absent".The comma-ok idiom — check key existence
const population = { tokyo: 13960000, london: 8982000 };
function getPopulation(city) {
const pop = population[city];
if (pop !== undefined) {
return `${city}: ${pop.toLocaleString()}`;
}
return `${city}: not found`;
}
console.log(getPopulation("tokyo"));
console.log(getPopulation("paris")); population := map[string]int{
"tokyo": 13960000,
"london": 8982000,
}
func getPopulation(city string, population map[string]int) string {
if pop, found := population[city]; found {
return fmt.Sprintf("%s: %d", city, pop)
}
return fmt.Sprintf("%s: not found", city)
}
fmt.Println(getPopulation("tokyo", population))
fmt.Println(getPopulation("paris", population)) The comma-ok idiom (
value, ok := map[key]) is Go's idiomatic way to distinguish a missing key from a key whose value happens to be the zero value. It appears throughout Go: type assertions, channel receives, and map lookups all use the same two-value pattern. The if with an init statement (if value, ok := ...; ok { ... }) scopes both variables to the if block.Iterating a map
const config = { host: "localhost", port: "8080", env: "dev" };
for (const [key, value] of Object.entries(config)) {
console.log(`${key}: ${value}`);
}
const keys = Object.keys(config);
const values = Object.values(config);
console.log(keys.sort()); config := map[string]string{
"host": "localhost",
"port": "8080",
"env": "dev",
}
for key, value := range config {
fmt.Printf("%s: %s\n", key, value)
}
// Collect and sort the keys
keys := make([]string, 0, len(config))
for key := range config {
keys = append(keys, key)
}
sort.Strings(keys)
fmt.Println(keys) for key, value := range map iterates all map entries. Critically, Go map iteration order is intentionally randomized — the specification does not define an order. If you need sorted output, collect the keys into a slice, sort the slice, and iterate the slice. This is a deliberate design choice to prevent code from depending on an ordering that could change between Go versions.make — preallocate with known capacity
// JS: no preallocate hint — engine handles it internally
const wordCount = {};
const words = "the quick brown fox jumps over the lazy dog".split(" ");
for (const word of words) {
wordCount[word] = (wordCount[word] || 0) + 1;
}
console.log(Object.entries(wordCount).sort()); words := strings.Fields("the quick brown fox jumps over the lazy dog")
wordCount := make(map[string]int, len(words)) // hint: ~9 unique words
for _, word := range words {
wordCount[word]++
}
// sort keys for deterministic output
keys := make([]string, 0, len(wordCount))
for key := range wordCount { keys = append(keys, key) }
sort.Strings(keys)
for _, key := range keys { fmt.Printf("%s: %d\n", key, wordCount[key]) } make(map[K]V, hint) preallocates internal hash table buckets for approximately hint entries, avoiding rehashing during growth. It is an optimization hint, not a size limit. strings.Fields() splits on any whitespace sequence — the equivalent of JavaScript's str.split(/\s+/).filter(Boolean). Note wordCount[word]++ works because the zero value for int is 0.Control Flow
if / else — no parens, must use braces
const temperature = 22;
if (temperature < 0) {
console.log("freezing");
} else if (temperature < 15) {
console.log("cold");
} else if (temperature < 25) {
console.log("comfortable");
} else {
console.log("hot");
} temperature := 22
if temperature < 0 {
fmt.Println("freezing")
} else if temperature < 15 {
fmt.Println("cold")
} else if temperature < 25 {
fmt.Println("comfortable")
} else {
fmt.Println("hot")
} Go
if requires no parentheses around the condition, and the braces are always required — even for a single-statement body. Both rules are enforced by the compiler and formatter. Omitting braces (as allowed in JavaScript) or adding unnecessary parentheses causes a compile error (the latter triggers gofmt to remove them).if with initializer — scope the result
// JS: must declare outside or use a let in the if
function divide(a, b) {
if (b === 0) return null;
return a / b;
}
const result = divide(10, 2);
if (result !== null) {
console.log("Result:", result);
} func divide(a, b float64) (float64, bool) {
if b == 0 { return 0, false }
return a / b, true
}
if result, ok := divide(10, 2); ok {
fmt.Println("Result:", result)
}
// result and ok are not accessible here — scoped to the if block Go's
if statement supports an optional initialization statement before the condition, separated by a semicolon. Variables declared in the init statement are scoped to the entire if/else if/else chain. This pattern is idiomatic for function calls that return a (value, bool) or (value, error) pair — it keeps the scope of the result tight and avoids polluting the enclosing function's scope.switch — no fallthrough by default
const day = "Wednesday";
switch (day) {
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
console.log("Weekday");
break; // break required to prevent fallthrough
case "Saturday":
case "Sunday":
console.log("Weekend");
break;
default:
console.log("Unknown");
} day := "Wednesday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("Weekday")
case "Saturday", "Sunday":
fmt.Println("Weekend")
default:
fmt.Println("Unknown")
} Go's
switch does not fall through by default — no break statements needed. Multiple values can be listed in a single case separated by commas. To opt into fallthrough for a specific case, use the fallthrough keyword (rare in idiomatic Go). switch can also have no expression (equivalent to switch true), making it a cleaner alternative to a long if/else if chain.for — the only loop in Go
// JS has for, while, do...while, for...of, for...in, forEach
// C-style for loop:
for (let i = 0; i < 5; i++) {
process.stdout.write(i + " ");
}
console.log();
// while equivalent:
let count = 10;
while (count > 0) {
process.stdout.write(count + " ");
count -= 3;
}
console.log(); // Go has only one loop keyword: for
// C-style for loop:
for i := 0; i < 5; i++ {
fmt.Print(i, " ")
}
fmt.Println()
// while equivalent (condition-only for):
count := 10
for count > 0 {
fmt.Print(count, " ")
count -= 3
}
fmt.Println() Go has only one loop keyword:
for. It covers all three JavaScript loop styles: the C-style three-clause for, the condition-only for (equivalent to while), and the infinite for {} loop (equivalent to while(true)). There is no while, do...while, or until keyword. The for ... range form handles iteration over slices, maps, strings, and channels.defer — run cleanup at function return
// JS: try/finally for guaranteed cleanup
function processFile() {
const resource = { name: "file.txt", open: true };
try {
console.log("using:", resource.name);
// ... work with resource ...
} finally {
resource.open = false;
console.log("closed:", resource.name);
}
}
processFile(); func processFile() {
resource := "file.txt"
fmt.Println("opened:", resource)
defer fmt.Println("closed:", resource) // runs when processFile returns
fmt.Println("using:", resource)
// ... work with resource ...
}
processFile() defer schedules a function call to execute when the surrounding function returns — whether by reaching the end, a return statement, or a panic. This is Go's primary mechanism for resource cleanup (closing files, releasing locks, etc.) and is cleaner than JavaScript's try/finally because the cleanup code appears immediately after the resource is opened, not at the bottom of the function. Multiple defer calls run in last-in-first-out order.Functions
Function declaration
function greet(name, greeting = "Hello") {
return `${greeting}, ${name}!`;
}
function add(a, b) {
return a + b;
}
console.log(greet("Alice"));
console.log(greet("Bob", "Hi"));
console.log(add(3, 4)); func greet(name, greeting string) string {
return fmt.Sprintf("%s, %s!", greeting, name)
}
func add(a, b int) int {
return a + b
}
fmt.Println(greet("Alice", "Hello"))
fmt.Println(greet("Bob", "Hi"))
fmt.Println(add(3, 4)) Go functions use
func name(params) returnType syntax with types after parameter names. When multiple consecutive parameters share a type, they can be grouped: func add(a, b int). Go does not have default parameter values — use function overloads with different names or an options struct for optional parameters. All parameters are passed by value; to modify the caller's variable, pass a pointer.Multiple return values
// JS: return an array or object for multiple values
function minMax(numbers) {
return [Math.min(...numbers), Math.max(...numbers)];
}
const [minimum, maximum] = minMax([3, 1, 4, 1, 5, 9]);
console.log(minimum, maximum); func minMax(numbers []int) (int, int) {
minimum, maximum := numbers[0], numbers[0]
for _, number := range numbers[1:] {
if number < minimum { minimum = number }
if number > maximum { maximum = number }
}
return minimum, maximum
}
minimum, maximum := minMax([]int{3, 1, 4, 1, 5, 9})
fmt.Println(minimum, maximum) Go functions can return multiple values as a first-class language feature — no wrapping in an array or object needed. The return types are listed in parentheses. Multiple return values are especially common for the
(result, error) pattern: almost every function that can fail returns its result and an error as the second value. Ignoring the error with _ is allowed but a code smell.Variadic functions
function sum(...numbers) {
return numbers.reduce((total, number) => total + number, 0);
}
console.log(sum(1, 2, 3));
console.log(sum(10, 20, 30, 40));
const values = [1, 2, 3, 4, 5];
console.log(sum(...values)); // spread func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
fmt.Println(sum(1, 2, 3))
fmt.Println(sum(10, 20, 30, 40))
values := []int{1, 2, 3, 4, 5}
fmt.Println(sum(values...)) // unpack slice with ... Go variadic functions use
...Type for the last parameter. Inside the function, the parameter is a regular slice. Calling a variadic function with a slice uses the slice... syntax to unpack it — similar to JavaScript's spread operator. Only the last parameter can be variadic. The built-in append() and fmt.Println() are variadic functions.Named return values
// JS: no named returns; use destructuring at the call site
function divmod(dividend, divisor) {
return {
quotient: Math.trunc(dividend / divisor),
remainder: dividend % divisor,
};
}
const { quotient, remainder } = divmod(17, 5);
console.log(quotient, remainder); func divmod(dividend, divisor int) (quotient, remainder int) {
quotient = dividend / divisor
remainder = dividend % divisor
return // "naked return" — returns named values
}
quotient, remainder := divmod(17, 5)
fmt.Println(quotient, remainder) Named return values serve as documentation and can be used with a bare
return statement that returns the current values of all named results. They are especially useful in short functions or when the same variable needs to be assigned in multiple branches. Named return parameters are initialized to their zero values on function entry.Closures
Closures — capture outer variables
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3 func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3 Go closures capture variables from the enclosing scope by reference, just like JavaScript closures. The
func() int return type declares that makeCounter() returns a function that takes no arguments and returns an int. Functions are first-class values in Go: they can be assigned to variables, passed as arguments, and returned from other functions.Functions as values — callbacks and higher-order functions
function applyTwice(func_, value) {
return func_(func_(value));
}
const double = x => x * 2;
const addTen = x => x + 10;
console.log(applyTwice(double, 3)); // 12
console.log(applyTwice(addTen, 5)); // 25 func applyTwice(transform func(int) int, value int) int {
return transform(transform(value))
}
double := func(number int) int { return number * 2 }
addTen := func(number int) int { return number + 10 }
fmt.Println(applyTwice(double, 3)) // 12
fmt.Println(applyTwice(addTen, 5)) // 25 Function types in Go are written as
func(paramTypes) returnType. A function parameter of type func(int) int accepts any function that takes an int and returns an int. Anonymous functions (function literals) are the Go equivalent of arrow functions, though they use func and full syntax rather than =>.Closure capture in loops — the classic trap
// JS trap: var captures the loop variable by reference
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(() => console.log(i)); // all capture the same i
}
funcs.forEach(f => f()); // prints 3, 3, 3 — not 0, 1, 2
// Fix: use let (block-scoped) or an IIFE
const fixed = [];
for (let j = 0; j < 3; j++) {
fixed.push(() => console.log(j));
}
fixed.forEach(f => f()); // 0, 1, 2 // Same trap in Go: loop variable captured by reference
funcs := []func(){}
for i := 0; i < 3; i++ {
captured := i // shadow i with a new variable each iteration
funcs = append(funcs, func() { fmt.Println(captured) })
}
for _, function_ := range funcs {
function_() // 0, 1, 2
} Go has the same closure-in-loop trap: a closure captures the loop variable itself, not its value at the time of capture. The fix in Go is the same as the pre-
let JavaScript fix: copy the value into a new variable inside the loop body. As of Go 1.22, for range loops create a new variable per iteration (like JavaScript's let), eliminating the trap for for ... range — but classic three-clause for loops still share the variable.Structs & Methods
Structs — named fields, no classes
// JS: use a class or plain object
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const alice = new Person("Alice", 30);
console.log(alice.name, alice.age);
console.log(alice); type Person struct {
Name string
Age int
}
alice := Person{Name: "Alice", Age: 30}
fmt.Println(alice.Name, alice.Age)
fmt.Printf("%+v\n", alice) // {Name:Alice Age:30} Go has no classes — structs with methods fill that role. Structs are defined with
type Name struct { ... } and instantiated as values (not objects on the heap by default). %+v in fmt.Printf() prints field names alongside values, useful for debugging. Exported fields start with an uppercase letter; unexported fields start with lowercase and are only accessible within the same package.Methods — functions with a receiver
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() { return this.width * this.height; }
perimeter() { return 2 * (this.width + this.height); }
describe() { return `${this.width}×${this.height} rect`; }
}
const rect = new Rectangle(5, 3);
console.log(rect.area());
console.log(rect.perimeter());
console.log(rect.describe()); type Rectangle struct { Width, Height float64 }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (r Rectangle) Describe() string {
return fmt.Sprintf("%.0f×%.0f rect", r.Width, r.Height)
}
rect := Rectangle{Width: 5, Height: 3}
fmt.Println(rect.Area())
fmt.Println(rect.Perimeter())
fmt.Println(rect.Describe()) A Go method is a function with a receiver:
func (r Rectangle) MethodName() ReturnType. The receiver r is similar to this in JavaScript, but it is a copy of the value, not a reference. Methods can be defined on any named type — not just structs — and the definition can appear anywhere in the same package, not just adjacent to the type.Pointer receivers — mutate the struct
class Counter {
constructor() { this.count = 0; }
increment() { this.count++; }
value() { return this.count; }
}
const counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.value()); type Counter struct { Count int }
func (counter *Counter) Increment() { counter.Count++ }
func (counter Counter) Value() int { return counter.Count }
counter := &Counter{}
counter.Increment()
counter.Increment()
fmt.Println(counter.Value()) A value receiver (
func (c Counter)) operates on a copy — mutations are not visible to the caller. A pointer receiver (func (c *Counter)) operates on the original value and allows mutation. This is unlike JavaScript, where object methods always receive a reference. Use pointer receivers when the method needs to mutate the struct, or when copying the struct would be expensive. Go automatically takes the address when calling a pointer receiver on an addressable value.Embedding — composition over inheritance
// JS: extends for inheritance
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
fetch() { return `${this.name} fetches the ball!`; }
}
const dog = new Dog("Rex");
console.log(dog.speak()); // inherited
console.log(dog.fetch()); type Animal struct { Name string }
func (a Animal) Speak() string {
return fmt.Sprintf("%s makes a sound", a.Name)
}
type Dog struct {
Animal // embedded — promotes Animal's fields and methods
Breed string
}
func (d Dog) Fetch() string {
return fmt.Sprintf("%s fetches the ball!", d.Name)
}
dog := Dog{Animal: Animal{Name: "Rex"}, Breed: "Labrador"}
fmt.Println(dog.Speak()) // promoted from Animal
fmt.Println(dog.Fetch()) Go has no inheritance. Instead, embedding a type (
type Dog struct { Animal; ... }) promotes the embedded type's fields and methods to the outer struct — dog.Speak() works even though Speak() is defined on Animal. This is composition, not inheritance: there is no subtype relationship. Dog is not an Animal and cannot be used where an Animal is expected — only interfaces enable polymorphism.Interfaces
Interfaces — satisfied implicitly
// JS: duck typing at runtime — no interface declaration
function makeSound(animal) {
// No compile-time check that animal has .speak()
console.log(animal.speak());
}
const cat = { speak: () => "Meow" };
const dog = { speak: () => "Woof" };
makeSound(cat);
makeSound(dog); type Speaker interface {
Speak() string
}
type Cat struct{}
type Dog struct{}
func (c Cat) Speak() string { return "Meow" }
func (d Dog) Speak() string { return "Woof" }
func makeSound(animal Speaker) {
fmt.Println(animal.Speak())
}
makeSound(Cat{})
makeSound(Dog{}) Go interfaces are satisfied implicitly — a type implements an interface by having the required methods, with no
implements declaration. This is like JavaScript's duck typing, but verified at compile time rather than runtime. If a type has all the methods in the interface, it satisfies the interface — even if the type was written before the interface was defined. This allows interfaces to be defined close to where they are used, not close to where types are defined.fmt.Stringer — custom string representation
// JS: override toString()
class Point {
constructor(x, y) { this.x = x; this.y = y; }
toString() { return `(${this.x}, ${this.y})`; }
}
const point = new Point(3, 4);
console.log(String(point));
console.log(`Point is ${point}`); type Point struct{ X, Y int }
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.X, p.Y)
}
point := Point{3, 4}
fmt.Println(point) // (3, 4) — uses String() automatically
fmt.Println("Point is", point) // (3, 4) Any type that implements
String() string satisfies the fmt.Stringer interface. The fmt package automatically uses String() when formatting a value with %v, %s, or fmt.Println(). This is the Go equivalent of JavaScript's toString() — override it to control how a type appears in output.any — the empty interface
// JS: every value can be assigned to a variable (implicit any)
function printAnything(value) {
console.log(value, typeof value);
}
printAnything(42);
printAnything("hello");
printAnything([1, 2, 3]);
printAnything(null); func printAnything(value any) {
fmt.Printf("%v (%T)\n", value, value)
}
printAnything(42)
printAnything("hello")
printAnything([]int{1, 2, 3})
printAnything(nil) any (alias for interface{}, added in Go 1.18) is the empty interface — every type implements it, since it requires no methods. It is the Go equivalent of TypeScript's any or JavaScript's untyped values. To recover the concrete type from an any, use a type assertion (value.(int)) or a type switch. Using any loses compile-time type safety — prefer it only at API boundaries.Type assertions and type switches
// JS: typeof and instanceof for runtime type checking
function describe(value) {
if (typeof value === "number") return `number: ${value}`;
if (typeof value === "string") return `string: "${value}"`;
if (Array.isArray(value)) return `array of length ${value.length}`;
return "unknown";
}
console.log(describe(42));
console.log(describe("hi"));
console.log(describe([1,2,3])); func describe(value any) string {
switch typed := value.(type) {
case int:
return fmt.Sprintf("number: %d", typed)
case string:
return fmt.Sprintf("string: %q", typed)
case []int:
return fmt.Sprintf("slice of length %d", len(typed))
default:
return "unknown"
}
}
fmt.Println(describe(42))
fmt.Println(describe("hi"))
fmt.Println(describe([]int{1, 2, 3})) A type switch (
switch value.(type)) dispatches on the runtime type of an interface value — the Go equivalent of a chain of typeof/instanceof checks. Inside each case, the variable has the concrete type. For a single-type check without a switch, use a type assertion: number, ok := value.(int) — if ok is false, the value is not an int.Error Handling
Errors as values — no exceptions
// JS: throw/catch — errors propagate invisibly
function divide(a, b) {
if (b === 0) throw new Error("division by zero");
return a / b;
}
try {
console.log(divide(10, 2));
console.log(divide(10, 0));
} catch (error) {
console.log("Error:", error.message);
} func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil { fmt.Println("Error:", err); return }
fmt.Println(result)
result, err = divide(10, 0)
if err != nil { fmt.Println("Error:", err); return }
fmt.Println(result) Go has no exceptions. Functions that can fail return an
error as the last return value. Callers must check the error before using the result — if err != nil is the pervasive Go idiom. This makes all error paths explicit and visible in function signatures. Unlike JavaScript's throw, errors do not propagate automatically — if you don't check, the error is silently ignored.fmt.Errorf — wrap errors with context
// JS: Error chaining with cause (ES2022)
class DatabaseError extends Error {
constructor(message, cause) {
super(message, { cause });
this.name = "DatabaseError";
}
}
function fetchUser(identifier) {
throw new DatabaseError(`user ${identifier} not found`, null);
}
try {
fetchUser(42);
} catch (error) {
console.log(error.name, ":", error.message);
} import "errors"
var ErrNotFound = errors.New("not found")
func fetchUser(identifier int) (string, error) {
return "", fmt.Errorf("user %d: %w", identifier, ErrNotFound)
}
username, err := fetchUser(42)
if err != nil {
fmt.Println("Error:", err)
if errors.Is(err, ErrNotFound) {
fmt.Println("(it was a not-found error)")
}
}
_ = username fmt.Errorf() with the %w verb wraps an error — adding context while preserving the original error for inspection. errors.Is() checks whether any error in the wrap chain matches a sentinel error value. errors.As() extracts a specific error type from the chain. This is Go's equivalent of JavaScript's Error({ cause }) pattern for error chaining.panic and recover — for truly unrecoverable errors
// JS: throw is used for both recoverable errors and bugs
function mustBePositive(number) {
if (number <= 0) {
throw new Error(`expected positive, got ${number}`); // programmer error
}
return Math.sqrt(number);
}
try {
console.log(mustBePositive(9));
console.log(mustBePositive(-1));
} catch (error) {
console.log("Caught:", error.message);
} func mustBePositive(number float64) float64 {
if number <= 0 {
panic(fmt.Sprintf("expected positive, got %v", number))
}
return math.Sqrt(number)
}
func safeCall() {
defer func() {
if recovered := recover(); recovered != nil {
fmt.Println("Caught:", recovered)
}
}()
fmt.Println(mustBePositive(9))
fmt.Println(mustBePositive(-1))
}
safeCall() panic() is for programmer errors and invariant violations — situations that "should never happen" in correct code. It is not the normal error-handling mechanism. recover(), called inside a deferred function, can catch a panic and resume normal execution. Most Go code never uses recover() — it is mainly used in server frameworks to prevent one bad request from crashing the whole server. Normal, expected errors (file not found, invalid input) use the (value, error) return pattern instead.Goroutines & Channels
Goroutines — truly concurrent, not event-loop
// JS: async functions on a single thread (event loop)
async function slowTask(name, delayMs) {
await new Promise(resolve => setTimeout(resolve, delayMs));
console.log(`${name} done after ${delayMs}ms`);
}
// These run concurrently on the event loop
Promise.all([slowTask("A", 50), slowTask("B", 30)])
.then(() => console.log("all done")); import "sync"
func slowTask(name string, waitGroup *sync.WaitGroup) {
defer waitGroup.Done()
// simulate work without actual sleep in this example
fmt.Printf("%s working...\n", name)
fmt.Printf("%s done\n", name)
}
var waitGroup sync.WaitGroup
waitGroup.Add(2)
go slowTask("A", &waitGroup) // go keyword launches a goroutine
go slowTask("B", &waitGroup)
waitGroup.Wait() // block until both goroutines call Done()
fmt.Println("all done") The
go keyword before a function call launches it as a goroutine — a lightweight thread managed by the Go runtime. Goroutines are truly concurrent (multiple CPU cores) unlike JavaScript's single-threaded event loop. A goroutine starts at ~2 KB of stack (growing as needed) and a modern program can run millions simultaneously. sync.WaitGroup coordinates completion, similar to Promise.all().Channels — communicate between goroutines
// JS: Promises for async results; no shared-memory message passing
async function generateNumbers(count) {
return Array.from({ length: count }, (_, i) => i + 1);
}
async function run() {
const numbers = await generateNumbers(5);
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log("sum:", total);
}
run(); func generateNumbers(count int, channel chan<- int) {
for i := 1; i <= count; i++ {
channel <- i // send to channel
}
close(channel) // signal that no more values will be sent
}
channel := make(chan int)
go generateNumbers(5, channel)
total := 0
for number := range channel { // receive until channel is closed
total += number
}
fmt.Println("sum:", total) Channels provide typed conduits for communication between goroutines.
make(chan int) creates an unbuffered channel — a send blocks until a receiver is ready and vice versa, synchronizing the two goroutines. make(chan int, 10) creates a buffered channel with capacity 10 — sends don't block until the buffer is full. Closing a channel signals to receivers that no more values will arrive; a for ... range loop over a channel exits when the channel is closed.select — multiplex channels
// JS: Promise.race() to take the first resolved promise
async function withTimeout(promise, timeoutMs) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("timed out")), timeoutMs)
);
return Promise.race([promise, timeout]);
} import "time"
func withTimeout(work chan string, timeout time.Duration) string {
timer := time.After(timeout)
select {
case result := <-work:
return result
case <-timer:
return "timed out"
}
}
workChan := make(chan string, 1)
go func() { workChan <- "result ready" }()
fmt.Println(withTimeout(workChan, 100*time.Millisecond)) select blocks until one of its cases can proceed, then executes that case — similar to Promise.race(). If multiple cases are ready, one is chosen at random. A default case makes the select non-blocking. time.After(d) returns a channel that receives a value after duration d — the idiomatic way to implement timeouts.sync.Mutex — protect shared state
// JS: single-threaded — no mutex needed for in-process state
// Shared mutable state is safe by default (no concurrent access)
let sharedCounter = 0;
for (let i = 0; i < 1000; i++) {
sharedCounter++; // always safe — runs sequentially
}
console.log(sharedCounter); // always 1000 import "sync"
type SafeCounter struct {
mutex sync.Mutex
count int
}
func (counter *SafeCounter) Increment() {
counter.mutex.Lock()
defer counter.mutex.Unlock()
counter.count++
}
func (counter *SafeCounter) Value() int {
counter.mutex.Lock()
defer counter.mutex.Unlock()
return counter.count
}
counter := &SafeCounter{}
var waitGroup sync.WaitGroup
for i := 0; i < 1000; i++ {
waitGroup.Add(1)
go func() { defer waitGroup.Done(); counter.Increment() }()
}
waitGroup.Wait()
fmt.Println(counter.Value()) Because goroutines run truly in parallel on multiple CPU cores, shared mutable state requires synchronization. Without a mutex, concurrent increments produce a data race — the final count could be anything less than 1000.
sync.Mutex ensures only one goroutine modifies the value at a time. The defer counter.mutex.Unlock() pattern ensures the lock is always released even if the function panics. The sync/atomic package offers lighter-weight primitives for simple counters.Packages & Modules
import — standard library and packages
// JS: import named exports from stdlib or npm
const { readFileSync } = require("fs");
const path = require("path");
const fullPath = path.join("/home", "user", "file.txt");
console.log(fullPath);
// console.log(readFileSync(fullPath, "utf8")); // norun: file I/O import (
"fmt"
"path/filepath"
)
fullPath := filepath.Join("/home", "user", "file.txt")
fmt.Println(fullPath) Go imports are package paths (strings). Standard library packages are short paths like
"fmt" and "path/filepath". Third-party packages use module paths like "github.com/user/repo/pkg". Go requires every imported package to be used — an unused import is a compile error. The package name used in code is the last segment of the path: "path/filepath" is used as filepath.Join().Exported names — capitalization as visibility
// JS: explicit export keyword controls what's visible
// math.js:
// export function add(a, b) { return a + b; } // public
// function helper(x) { return x * 2; } // private
// export const PI = 3.14159; // public
// main.js:
// import { add, PI } from "./math.js";
// Simulated here (modules not runnable in this eval context):
const add = (a, b) => a + b;
const PI = 3.14159;
console.log(add(3, 4), PI); // In package greetings:
// func Hello(name string) string { ... } // exported — capital H
// func helper(name string) string { ... } // unexported — lowercase h
// const DefaultGreeting = "Hello" // exported
// Using it from another package:
// import "yourmodule/greetings"
// message := greetings.Hello("Alice")
// Demonstrating the rule directly:
type ExportedType struct{ Value int } // exported
type unexportedType struct{ value int } // unexported (package-private)
item := ExportedType{Value: 42}
fmt.Println(item.Value) Go uses capitalization to control visibility. A name starting with an uppercase letter is exported (visible outside the package); a lowercase name is unexported (package-private). This replaces JavaScript's explicit
export keyword. There is no private, protected, or public keyword — the single capitalization rule covers all cases. Fields, methods, functions, types, and constants all follow the same rule.Go modules — go.mod vs package.json
// package.json — Node.js module manifest:
// {
// "name": "my-app",
// "version": "1.0.0",
// "dependencies": {
// "express": "^4.18.2"
// }
// }
// npm install # installs to node_modules/
// node index.js # run
// Using a built-in module (no install):
const { createHash } = require("node:crypto");
const hash = createHash("sha256").update("hello").digest("hex");
console.log(hash.slice(0, 16)); // go.mod — Go module manifest:
// module github.com/user/my-app
//
// go 1.26
//
// require (
// github.com/gin-gonic/gin v1.10.0
// )
//
// go get github.com/gin-gonic/gin // add a dependency
// go build ./... // compile
// go run . // build and run
// Using the standard library (no go.mod entry needed):
import "crypto/sha256"
digest := sha256.Sum256([]byte("hello"))
fmt.Printf("%x\n", digest[:8]) go.mod is Go's equivalent of package.json: it declares the module path, Go version requirement, and dependencies. go get package@version adds a dependency (like npm install). Dependencies are downloaded to a local cache (~/go/pkg/mod), not a per-project node_modules directory. The go.sum file records cryptographic hashes of every dependency — the equivalent of package-lock.json.init() — package initialization
// JS: top-level code in a module runs on import
// math.js:
// console.log("math module loaded"); // runs on first import
// export const PI = computePi();
// No equivalent of Go's init() — module-level code fills that role.
console.log("module initialization");
const COMPUTED_VALUE = (() => {
return 42 * 2;
})();
console.log("ready, value:", COMPUTED_VALUE); var computedValue int
func init() {
// Runs automatically when the package is loaded
// before main() starts
computedValue = 42 * 2
fmt.Println("package initialized")
}
fmt.Println("main running, value:", computedValue) Every Go package can define one or more
init() functions, which run automatically when the package is loaded — before main() starts. They are used for one-time setup: registering drivers, validating configuration, initializing lookup tables. Unlike JavaScript's module-level code, init() cannot be called directly and does not accept parameters or return values. Multiple init() functions in the same file run in declaration order.