PONY λ M2 Modula-2

Python.CodeCompared.To/Go

An interactive executable cheatsheet comparing Python and Go

Python 3.13 Go 1.26.2
Output & Running
Hello, World
print("Hello, World!")
fmt.Println("Hello, World!")
Both print() and fmt.Println() append a trailing newline. Every standalone Go program requires a package main declaration and a func main() entry point — the runner adds this boilerplate automatically. There is no Go script mode.
print() / fmt.Println vs fmt.Print
name = "Alice" age = 30 print(name) # appends newline print(name, age) # space-separated, then newline print(name, end="") # no trailing newline print() # blank line
name := "Alice" age := 30 fmt.Println(name) // appends newline fmt.Println(name, age) // space-separated, then newline fmt.Print(name) // no trailing newline fmt.Println() // blank line
fmt.Println is the direct counterpart to Python's print() — it adds spaces between arguments and appends a newline. fmt.Print omits the trailing newline, like print(..., end=""). Unlike Python, Go's fmt.Print only adds spaces between adjacent non-string arguments.
Formatted output (f-strings vs fmt.Printf)
name = "Alice" score = 42 print(f"Player {name} scored {score} points") print(f"Pi is approximately {3.14159:.2f}")
name := "Alice" score := 42 fmt.Printf("Player %s scored %d points\n", name, score) fmt.Printf("Pi is approximately %.2f\n", 3.14159)
Python's f-strings embed expressions directly: f"{value:.2f}". Go uses C-style format verbs: %s for strings, %d for integers, %f for floats. The \n must be written explicitly — fmt.Printf does not add a trailing newline automatically.
Building a formatted string (fmt.Sprintf)
name = "Alice" score = 42 message = f"Player {name} scored {score} points" print(message)
name := "Alice" score := 42 message := fmt.Sprintf("Player %s scored %d points", name, score) fmt.Println(message)
fmt.Sprintf is the Go equivalent of Python's f-string when you need to store a formatted string rather than print it immediately. It returns a string instead of writing to stdout. The format verbs are the same as fmt.Printf.
Comments
# Single-line comment # Python has no built-in multi-line comment syntax. # Use consecutive # lines for block comments. value = 42 # inline comment print(value)
// Single-line comment /* Multi-line block comment */ value := 42 // inline comment fmt.Println(value)
Go uses C-style // for line comments and /* */ for block comments. The go doc tool extracts documentation from // comments immediately preceding a declaration; by convention, every exported identifier begins its doc comment with the identifier's name.
Variables & Types
Short variable declaration (:=)
name = "Alice" count = 0 active = True print(name, count, active)
name := "Alice" count := 0 active := true fmt.Println(name, count, active)
Go's := declares a new variable and infers its type from the right-hand side — the rough equivalent of Python's assignment. The key difference: the type is fixed at declaration and cannot change. Python's True/False become true/false (lowercase) in Go.
Explicit type declaration
name: str = "Alice" # type hint — ignored at runtime count: int = 0 active: bool = True print(name, count, active)
var name string = "Alice" var count int = 0 var active bool = true fmt.Println(name, count, active)
Python type hints are documentation only — the interpreter ignores them at runtime. Go's var declarations are enforced by the compiler: a type mismatch is a compile error. The := shorthand is preferred inside functions; var is used at package level or when the zero value is the intended starting value.
Zero values (no undefined / None)
# Python variables must be assigned before use. # Uninitialized state is typically represented with None. name = None count = None items = [] # must be explicitly created print(name, count, items)
var name string // "" — empty string var count int // 0 — zero integer var active bool // false var items []string // nil — nil slice, safe to append to fmt.Println(name, count, active, items)
Every Go variable has a zero value — the natural empty state for its type. There is no None for value types (integers, structs, booleans). A zero-value struct is fully usable with no constructor call. A nil slice behaves identically to an empty slice for all built-in operations.
Constants
# Python convention: ALL_CAPS for module-level constants. # Any code can still reassign them — nothing enforces it. MAX_RETRIES = 3 DEFAULT_HOST = "localhost" print(MAX_RETRIES, DEFAULT_HOST)
const MaxRetries = 3 const DefaultHost = "localhost" fmt.Println(MaxRetries, DefaultHost)
Go's const is enforced by the compiler — reassigning a constant is a compile error. Go uses UpperCamelCase for exported constants and lowerCamelCase for package-private ones; the Python ALL_CAPS convention is not used in idiomatic Go.
Multiple assignment and swap
x, y = 1, 2 x, y = y, x # swap print(x, y)
x, y := 1, 2 x, y = y, x // swap — note: = not :=, since x and y already exist fmt.Println(x, y)
Go supports multiple assignment with the same a, b = b, a swap idiom as Python. Use := when declaring new variables and = when reassigning existing ones. If at least one variable on the left is new, := is allowed even when others already exist.
Type conversion
integer_value = 42 float_value = float(integer_value) # 42.0 text_value = str(integer_value) # "42" print(float_value, text_value)
integerValue := 42 floatValue := float64(integerValue) // 42.0 textValue := strconv.Itoa(integerValue) // "42" fmt.Println(floatValue, textValue)
Go requires explicit conversion between all numeric types — there is no implicit widening or narrowing. The target type name is used as a function: float64(x), int(y). For string-to-number and number-to-string conversions, use the strconv package (strconv.Atoi, strconv.ParseFloat, strconv.Itoa).
Strings
String basics
greeting = "Hello, World!" print(len(greeting)) # 13 characters print(greeting[0]) # 'H' print(greeting[7:12]) # 'World'
greeting := "Hello, World!" fmt.Println(len(greeting)) // 13 bytes (same for ASCII) fmt.Println(string(greeting[0])) // "H" (byte at index 0) fmt.Println(greeting[7:12]) // "World"
Go strings are sequences of bytes, not characters. For pure ASCII text, len() and indexing behave identically to Python. For Unicode text, len() returns the byte count and indexing returns a byte — use []rune(greeting) or range iteration for character-level access.
String formatting
name = "Alice" score = 95.5 result = f"Hello, {name}! Score: {score:.1f}%" print(result)
name := "Alice" score := 95.5 result := fmt.Sprintf("Hello, %s! Score: %.1f%%", name, score) fmt.Println(result)
Go's fmt.Sprintf covers the same ground as Python's f-strings but uses positional format verbs rather than embedded expressions. The literal percent sign must be escaped as %% in Go format strings. Common verbs: %s (string), %d (integer), %f/%.2f (float), %v (any value), %T (type name).
Multi-line strings
poem = """Roses are red, Violets are blue, Go is fast, Python is too.""" print(poem)
poem := `Roses are red, Violets are blue, Go is fast, Python is too.` fmt.Println(poem)
Go uses backtick-delimited raw string literals for multi-line strings. They behave exactly like Python's triple-quoted strings: no escape sequences are processed, and all whitespace including newlines is preserved literally. Backtick strings cannot contain a backtick character; use "..." strings with \n in that case.
String methods (strings package)
message = " Hello, World! " print(message.strip()) print(message.upper()) print(message.lower()) print("hello,world".split(",")) print(",".join(["a", "b", "c"]))
message := " Hello, World! " fmt.Println(strings.TrimSpace(message)) fmt.Println(strings.ToUpper(message)) fmt.Println(strings.ToLower(message)) fmt.Println(strings.Split("hello,world", ",")) fmt.Println(strings.Join([]string{"a", "b", "c"}, ","))
Python string methods are called on the string object itself (text.upper()). Go's string manipulation lives in the strings package as standalone functions (strings.ToUpper(text)). The behavior is equivalent, but you must import "strings" explicitly. The runner handles this import automatically.
String search and contains
text = "Hello, World!" print("World" in text) print(text.startswith("Hello")) print(text.endswith("!")) print(text.find("World")) # -1 if not found
text := "Hello, World!" fmt.Println(strings.Contains(text, "World")) fmt.Println(strings.HasPrefix(text, "Hello")) fmt.Println(strings.HasSuffix(text, "!")) fmt.Println(strings.Index(text, "World")) // -1 if not found
Python's in operator has no direct Go counterpart — use strings.Contains. Python's startswith/endswith become strings.HasPrefix/strings.HasSuffix. strings.Index returns the byte offset of the first occurrence, or -1 if not found.
Unicode and runes
emoji = "🐍" print(len(emoji)) # 1 character print(len(emoji.encode("utf-8"))) # 4 bytes for character in emoji: print(repr(character))
emoji := "🐍" fmt.Println(len(emoji)) // 4 bytes fmt.Println(utf8.RuneCountInString(emoji)) // 1 rune (character) for _, character := range emoji { fmt.Printf("%c\n", character) }
Go's len() on a string returns the byte count; utf8.RuneCountInString returns the Unicode character count. Iterating a string with range correctly decodes UTF-8 and yields Unicode code points (type rune, an alias for int32) — not raw bytes. Python strings are always character sequences, so this distinction does not arise.
Numbers
Integer arithmetic
x = 17 y = 5 print(x + y) # 22 print(x - y) # 12 print(x * y) # 85 print(x // y) # 3 (integer division) print(x % y) # 2 (remainder)
x := 17 y := 5 fmt.Println(x + y) // 22 fmt.Println(x - y) // 12 fmt.Println(x * y) // 85 fmt.Println(x / y) // 3 (integer division: both operands are int) fmt.Println(x % y) // 2 (remainder)
Python requires // for integer division and / for float division. Go uses / for both — when both operands are integers, the result is an integer (truncated toward zero). To get a float result from integer operands, convert first: float64(x) / float64(y).
Float arithmetic
x = 3.14 y = 2.0 print(x + y) print(x / y) print(round(x, 1))
x := 3.14 y := 2.0 fmt.Println(x + y) fmt.Println(x / y) fmt.Printf("%.1f\n", x)
Python has one floating-point type (float, IEEE 754 double precision). Go has float32 and float64; untyped float literals default to float64. Use math.Round() for rounding to a specific precision, or fmt.Sprintf("%.1f") to produce a rounded string representation.
Numeric type conversion
integer_value = 42 float_value = float(integer_value) # 42.0 back_to_int = int(float_value) # 42 print(float_value, back_to_int)
integerValue := 42 floatValue := float64(integerValue) // 42.0 backToInt := int(floatValue) // 42 fmt.Println(floatValue, backToInt)
Go requires an explicit conversion expression for every numeric type change — there is no implicit narrowing or widening even between int and int64. The target type is written as a function call: float64(value), int(value). Python applies many of these conversions automatically in arithmetic contexts.
Integer size and overflow
# Python integers grow to arbitrary precision automatically. big = 2 ** 100 print(big)
// Go int is 64 bits on 64-bit systems; it overflows silently. // Use math/big for arbitrary precision. large := 1 << 40 // 2^40 fits comfortably in int64 fmt.Println(large)
Python integers grow arbitrarily large with no overflow. Go's integer types are fixed-width (int is 32 or 64 bits depending on the platform); overflow wraps around silently. For numbers larger than 64 bits, use the math/big package, which provides arbitrary-precision integer arithmetic similar to Python's built-in behavior.
Lists / Slices
Creating slices (list literals)
numbers = [1, 2, 3, 4, 5] names = ["Alice", "Bob", "Carol"] print(numbers) print(names)
numbers := []int{1, 2, 3, 4, 5} names := []string{"Alice", "Bob", "Carol"} fmt.Println(numbers) fmt.Println(names)
Go slice literals look like Python list literals but require an explicit element type prefix: []int{...}, []string{...}. The type is not inferred from the values — it must be stated. A slice is a view over an underlying array; unlike Python lists, slices are not self-contained dynamic arrays.
Appending to a slice
items = [1, 2, 3] items.append(4) items.extend([5, 6]) print(items)
items := []int{1, 2, 3} items = append(items, 4) items = append(items, 5, 6) fmt.Println(items)
Python's list.append() mutates the list in place. Go's append() is a built-in function that returns a new (possibly reallocated) slice — you must assign the result back: items = append(items, value). Forgetting to reassign is a common mistake; the original slice header is not modified.
Slicing
numbers = [10, 20, 30, 40, 50] print(numbers[1:3]) # [20, 30] print(numbers[:2]) # [10, 20] print(numbers[3:]) # [40, 50] print(numbers[-1]) # 50 (last element)
numbers := []int{10, 20, 30, 40, 50} fmt.Println(numbers[1:3]) // [20 30] fmt.Println(numbers[:2]) // [10 20] fmt.Println(numbers[3:]) // [40 50] fmt.Println(numbers[len(numbers)-1]) // 50
Go slice syntax is identical to Python's [start:stop], but Go has no negative indexing. To access the last element, use slice[len(slice)-1]. A Go slice expression creates a view into the underlying array — modifying a sub-slice modifies the original data, unlike Python's list slicing which copies.
Iterating over a slice
fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit) for index, fruit in enumerate(fruits): print(index, fruit)
fruits := []string{"apple", "banana", "cherry"} for _, fruit := range fruits { fmt.Println(fruit) } for index, fruit := range fruits { fmt.Println(index, fruit) }
Go's range always yields both index and element. Use _ to discard the index when only the value is needed. Python requires a separate enumerate() call to get the index; in Go, range always provides it — discarding is opt-in.
Allocating a slice with make()
zeros = [0] * 5 print(zeros) print(len(zeros))
zeros := make([]int, 5) // [0 0 0 0 0] fmt.Println(zeros) fmt.Println(len(zeros))
make([]T, length) allocates a slice pre-filled with zero values, equivalent to Python's [0] * n. An optional third argument specifies capacity: make([]T, 0, 100) creates an empty slice that can hold 100 elements before reallocation. Use make when the final length is known up front to avoid repeated copies during growth.
Length and capacity
items = [1, 2, 3] print(len(items)) # 3 # Python exposes only len(); capacity is internal
items := []int{1, 2, 3} fmt.Println(len(items)) // 3 — elements in use fmt.Println(cap(items)) // 3 — capacity before reallocation
Go slices expose both length (elements currently in use) and capacity (total allocated space before a reallocation is needed). Python lists expose only len(); capacity management is entirely internal. In Go, knowing the capacity lets you pre-allocate with make([]T, 0, expectedSize) to avoid repeated allocations during growth.
Dicts / Maps
Creating maps (dict literals)
person = {"name": "Alice", "age": 30} scores = {"math": 95, "science": 87} print(person) print(scores)
person := map[string]any{"name": "Alice", "age": 30} scores := map[string]int{"math": 95, "science": 87} fmt.Println(person) fmt.Println(scores)
Go map literals require explicit key and value types: map[KeyType]ValueType{...}. All keys must be the same type and all values must be the same type. For mixed-value maps (like Python dicts), use map[string]any where any is an alias for interface{} — this trades compile-time type safety for flexibility.
Accessing and checking keys
person = {"name": "Alice"} print(person["name"]) # KeyError if missing print(person.get("email", "")) # "" if missing
person := map[string]string{"name": "Alice"} fmt.Println(person["name"]) // "Alice" value, exists := person["email"] // comma-ok idiom if exists { fmt.Println(value) } else { fmt.Println("key not found") }
Accessing a missing key in a Go map returns the zero value for the value type (empty string, 0, false) rather than panicking. Use the two-value assignment value, ok := map[key] to check whether the key was actually present. This "comma-ok" idiom is the idiomatic Go equivalent of Python's dict.get(key, default).
Adding, updating, and deleting
person = {"name": "Alice"} person["age"] = 30 # add person["name"] = "Bob" # update del person["age"] # delete print(person)
person := map[string]any{"name": "Alice"} person["age"] = 30 // add person["name"] = "Bob" // update delete(person, "age") // delete fmt.Println(person)
delete(map, key) is a built-in function that removes a key. Python's del dict[key] does the same thing. Both operations are safe on a missing key — no error is raised. There is no pop() method in Go; use delete after reading the value if you need it.
Iterating over a map
scores = {"math": 95, "science": 87, "history": 78} for subject, score in scores.items(): print(f"{subject}: {score}")
scores := map[string]int{"math": 95, "science": 87, "history": 78} for subject, score := range scores { fmt.Printf("%s: %d\n", subject, score) }
Go map iteration order is deliberately randomized on each run — the language specification guarantees instability. Python dicts maintain insertion order since Python 3.7. If you need sorted output in Go, collect the keys into a slice, sort it with sort.Strings(), then iterate the sorted keys.
Control Flow
if / else if / else
score = 75 if score >= 90: print("A") elif score >= 80: print("B") elif score >= 70: print("C") else: print("D")
score := 75 if score >= 90 { fmt.Println("A") } else if score >= 80 { fmt.Println("B") } else if score >= 70 { fmt.Println("C") } else { fmt.Println("D") }
Go's else if corresponds to Python's elif. Parentheses around the condition are optional and conventionally omitted. The curly braces are mandatory even for single-line bodies — there is no one-liner if. Go also has no ternary operator (x if cond else y); use a full if/else block.
switch statement
day = "Monday" match day: case "Saturday" | "Sunday": print("Weekend") case "Monday": print("Back to work") case _: print("Weekday")
day := "Monday" switch day { case "Saturday", "Sunday": fmt.Println("Weekend") case "Monday": fmt.Println("Back to work") default: fmt.Println("Weekday") }
Go's switch does not fall through by default (unlike C/Java), which makes it safer than C. Multiple match values in one case are comma-separated. The default clause corresponds to Python's case _. Go's switch predates Python's match statement and is the idiomatic way to replace long if/else if chains.
switch as if/elif chain
temperature = 35 if temperature > 40: print("Extreme heat") elif temperature > 30: print("Hot") elif temperature > 20: print("Warm") else: print("Cool")
temperature := 35 switch { case temperature > 40: fmt.Println("Extreme heat") case temperature > 30: fmt.Println("Hot") case temperature > 20: fmt.Println("Warm") default: fmt.Println("Cool") }
A switch with no expression evaluates each case as an independent boolean condition and runs the first true one. This is often cleaner than a long if/else if chain when the cases share no common base expression.
if with initialization statement
text = "42" value = int(text) if value > 0: print(f"Positive: {value}")
text := "42" if value, err := strconv.Atoi(text); err == nil { fmt.Printf("Positive: %d\n", value) }
Go's if supports an optional initialization expression before the condition, separated by a semicolon. Variables declared there are scoped to the entire if/else block and unavailable afterward. This is the idiomatic Go pattern for error-returning functions whose result is only needed inside the block.
Loops & Iteration
for/range over a slice
fruits = ["apple", "banana", "cherry"] for fruit in fruits: print(fruit)
fruits := []string{"apple", "banana", "cherry"} for _, fruit := range fruits { fmt.Println(fruit) }
Go's range clause on a slice always yields both index and element. When only the element is needed, discard the index with _. Go has only one loop keyword — for — which covers Python's for, while, and the C-style three-part counting loop.
Index and value (enumerate)
items = ["a", "b", "c"] for index, item in enumerate(items): print(index, item)
items := []string{"a", "b", "c"} for index, item := range items { fmt.Println(index, item) }
Python requires a separate enumerate() call to get the index alongside the value. In Go, range always provides the index; discarding it with _ is opt-in. This means Go's range naturally replaces both for item in collection and for index, item in enumerate(collection).
while loop
count = 0 while count < 5: print(count) count += 1
count := 0 for count < 5 { fmt.Println(count) count++ }
Go has no while keyword. A for loop with only a condition behaves exactly like Python's while. The ++ increment is a statement in Go (not an expression), so x = count++ is invalid — use count++ alone on its own line.
Counting loop (range(n))
for counter in range(5): print(counter)
for counter := 0; counter < 5; counter++ { fmt.Println(counter) }
Python's range(n) produces 0..n-1. Go uses the classic three-part C-style loop: initialization, condition, post-statement. There is no range(n) shorthand for integers in Go. For a counting loop over a slice, prefer for index := range slice which is slightly shorter.
Loop with step (range with step)
for value in range(0, 20, 5): print(value)
for value := 0; value < 20; value += 5 { fmt.Println(value) }
Python's range(start, stop, step) has no direct Go equivalent. Use the three-part for loop with a custom increment in the post-statement. Negative steps (counting down) also use this form: for i := 10; i >= 0; i-- {}.
break and continue
for number in range(10): if number == 3: continue if number == 7: break print(number)
for number := 0; number < 10; number++ { if number == 3 { continue } if number == 7 { break } fmt.Println(number) }
break and continue work identically in Go and Python. Go also supports labeled break and continue to target an outer loop — useful for breaking out of nested loops without a sentinel variable.
Infinite loop (while True)
count = 0 while True: print(count) count += 1 if count >= 3: break
count := 0 for { fmt.Println(count) count++ if count >= 3 { break } }
A bare for {} is Go's infinite loop — the equivalent of Python's while True:. Use break to exit. This pattern is common in Go servers and background workers where the loop is intentionally endless.
Functions
Function definition
def add(first_number: int, second_number: int) -> int: return first_number + second_number print(add(3, 4))
func add(firstNumber int, secondNumber int) int { return firstNumber + secondNumber } fmt.Println(add(3, 4))
Go lists parameter types after the parameter name, and return types appear after the closing parenthesis. Unlike Python, all parameter types must be explicitly declared — type inference does not apply to function signatures. Adjacent parameters of the same type can share a type annotation: func add(a, b int) int.
No default argument values
def greet(name: str, greeting: str = "Hello") -> str: return f"{greeting}, {name}!" print(greet("Alice")) print(greet("Bob", "Hi"))
func greet(name string) string { return greetWith(name, "Hello") } func greetWith(name, customGreeting string) string { return fmt.Sprintf("%s, %s!", customGreeting, name) } fmt.Println(greet("Alice")) fmt.Println(greetWith("Bob", "Hi"))
Go does not support default argument values. The idiomatic workarounds are: providing multiple functions for simple overloads (as shown here), using an options struct for complex APIs, or accepting variadic functional options. This is a deliberate design choice — Go favors explicit call sites over implicit defaults.
Multiple return values
def min_max(numbers: list) -> tuple: return min(numbers), max(numbers) minimum, maximum = min_max([3, 1, 4, 1, 5, 9]) print(minimum, maximum)
func minMax(numbers []int) (int, int) { minimum := numbers[0] maximum := 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 has native multiple return values as a language feature — no tuple packing or unpacking needed. Python achieves the same effect by returning a tuple. Multiple returns are the standard Go idiom for returning both a result and an error: return value, nil on success, return zero, err on failure.
Variadic functions (*args)
def total(*numbers: int) -> int: return sum(numbers) print(total(1, 2, 3)) print(total(1, 2, 3, 4, 5))
func total(numbers ...int) int { result := 0 for _, number := range numbers { result += number } return result } fmt.Println(total(1, 2, 3)) fmt.Println(total(1, 2, 3, 4, 5))
Go's variadic parameter (...T) is equivalent to Python's *args. Inside the function, the parameter is a []T slice. To pass an existing slice where a variadic argument is expected, spread it with the ... suffix: total(mySlice...).
defer (cleanup scheduling)
def greet(name: str) -> str: print(f"Entering greet({name})") result = f"Hello, {name}!" print(f"Leaving greet({name})") return result print(greet("Alice"))
func greet(name string) string { fmt.Printf("Entering greet(%s)\n", name) defer fmt.Printf("Leaving greet(%s)\n", name) return fmt.Sprintf("Hello, %s!", name) } fmt.Println(greet("Alice"))
defer schedules a function call to execute when the surrounding function returns, regardless of how it returns (normal return or panic). Multiple deferred calls execute in last-in, first-out order. The canonical use is resource cleanup: defer file.Close() immediately after opening — similar to Python's with statement but scoped to the function rather than a block.
Closures & First-class Functions
Closures
def make_counter(start: int = 0): count = start def increment(): nonlocal count count += 1 return count return increment counter = make_counter() print(counter()) print(counter())
func makeCounter(start int) func() int { count := start return func() int { count++ return count } } counter := makeCounter(0) fmt.Println(counter()) fmt.Println(counter())
Go closures capture variables by reference, just like Python's closures. Python requires the nonlocal keyword to modify a captured variable from an enclosing scope; Go has no such requirement — any captured variable can be read or written directly. The captured count is shared between all closures returned by makeCounter.
Higher-order functions (map/filter)
numbers = [1, 2, 3, 4, 5] doubled = list(map(lambda number: number * 2, numbers)) evens = list(filter(lambda number: number % 2 == 0, numbers)) print(doubled) print(evens)
numbers := []int{1, 2, 3, 4, 5} doubled := make([]int, len(numbers)) for index, number := range numbers { doubled[index] = number * 2 } var evens []int for _, number := range numbers { if number%2 == 0 { evens = append(evens, number) } } fmt.Println(doubled) fmt.Println(evens)
Go has no built-in map(), filter(), or list comprehensions. The idiomatic Go approach uses explicit for loops — more verbose but transparent. The slices package (Go 1.21+) provides some generic helpers. Writing custom higher-order functions using generics is straightforward when needed.
Functions as values
def apply(operation, value: int) -> int: return operation(value) def double(number: int) -> int: return number * 2 print(apply(double, 5)) print(apply(lambda number: number ** 2, 5))
func apply(operation func(int) int, value int) int { return operation(value) } func double(number int) int { return number * 2 } fmt.Println(apply(double, 5)) fmt.Println(apply(func(number int) int { return number * number }, 5))
Functions are first-class values in Go, as in Python. A function type is written as func(ParameterTypes) ReturnType. Go's anonymous functions (written func(...) { ... }) can contain any statements, not just a single expression — they are more powerful than Python's lambda, which is limited to one expression.
Structs & Methods
Struct definition (vs class / dataclass)
from dataclasses import dataclass @dataclass class Person: name: str age: int alice = Person(name="Alice", age=30) print(alice.name) print(alice.age)
type Person struct { Name string Age int } alice := Person{Name: "Alice", Age: 30} fmt.Println(alice.Name) fmt.Println(alice.Age)
Go structs serve the role of Python dataclasses for simple data holders. Fields beginning with an uppercase letter are exported (accessible outside the package). There is no __init__ — struct literal syntax initializes fields by name. Methods are declared separately outside the struct body.
Methods on structs
class Rectangle: def __init__(self, width: float, height: float): self.width = width self.height = height def area(self) -> float: return self.width * self.height def __str__(self) -> str: return f"Rectangle({self.width}x{self.height})" rect = Rectangle(5.0, 3.0) print(rect.area()) print(rect)
type Rectangle struct { Width float64 Height float64 } func (rect Rectangle) Area() float64 { return rect.Width * rect.Height } func (rect Rectangle) String() string { return fmt.Sprintf("Rectangle(%.1fx%.1f)", rect.Width, rect.Height) } rect := Rectangle{Width: 5.0, Height: 3.0} fmt.Println(rect.Area()) fmt.Println(rect)
Go methods are declared outside the struct body using a receiver parameter: func (rect Rectangle) Area(). The receiver (rect) plays the same role as Python's self. Implementing String() string is the Go equivalent of Python's __str__fmt.Println calls it automatically.
Pointer receivers (mutating methods)
class Counter: def __init__(self): self.value = 0 def increment(self): self.value += 1 counter = Counter() counter.increment() counter.increment() print(counter.value)
type Counter struct { Value int } func (counter *Counter) Increment() { counter.Value++ } counter := &Counter{} counter.Increment() counter.Increment() fmt.Println(counter.Value)
Python's self is always a reference — mutating self.value changes the original object. In Go, a value receiver (rect Rectangle) receives a copy. To mutate the original struct, use a pointer receiver (counter *Counter). &Counter{} creates a pointer to a new zero-value Counter.
Constructor pattern (New… functions)
class Person: def __init__(self, name: str, age: int): self.name = name self.age = age def greet(self) -> str: return f"Hi, I'm {self.name}" alice = Person("Alice", 30) print(alice.greet())
type Person struct { Name string Age int } func NewPerson(name string, age int) *Person { return &Person{Name: name, Age: age} } func (person *Person) Greet() string { return fmt.Sprintf("Hi, I'm %s", person.Name) } alice := NewPerson("Alice", 30) fmt.Println(alice.Greet())
Go has no constructor syntax. The idiomatic pattern is a New() function that returns a pointer to a new, initialized value. Callers may also use struct literal syntax directly when no special initialization logic is needed. The New prefix is a Go convention, not a language feature.
Struct embedding (composition vs inheritance)
class Animal: def __init__(self, name: str): self.name = name def describe(self) -> str: return f"{self.name} is an animal" class Dog(Animal): def describe(self) -> str: return f"{self.name} says: woof!" dog = Dog("Rex") print(dog.describe()) print(dog.name)
type Animal struct { Name string } func (animal Animal) Describe() string { return fmt.Sprintf("%s is an animal", animal.Name) } type Dog struct { Animal // embedded — fields and methods are promoted } func (dog Dog) Describe() string { return fmt.Sprintf("%s says: woof!", dog.Name) } dog := Dog{Animal: Animal{Name: "Rex"}} fmt.Println(dog.Describe()) fmt.Println(dog.Name) // promoted from Animal
Go uses embedding rather than classical inheritance. Embedding a type by name (without a field label) promotes the embedded type's fields and methods to the outer struct. Go deliberately lacks a class hierarchy — composition is the primary code reuse mechanism. There is no super() call; you access the embedded type directly: dog.Animal.Describe().
Interfaces
Interface definition and implicit satisfaction
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: ... class Circle: def __init__(self, radius: float): self.radius = radius def area(self) -> float: return 3.14159 * self.radius ** 2 shape: Shape = Circle(5.0) print(f"Area: {shape.area():.2f}")
type Shape interface { Area() float64 } type Circle struct { Radius float64 } func (circle Circle) Area() float64 { return 3.14159 * circle.Radius * circle.Radius } var shape Shape = Circle{Radius: 5.0} fmt.Printf("Area: %.2f\n", shape.Area())
Go interfaces are satisfied implicitly — there is no implements keyword and no ABC registration. Any type providing the required methods satisfies the interface. Python's abstract base classes require explicit class Foo(ABC) registration and decorator syntax; Go's approach is enforced at compile time with no boilerplate.
Polymorphism through interfaces
from math import pi class Circle: def __init__(self, radius: float): self.radius = radius def area(self) -> float: return pi * self.radius ** 2 class Rectangle: def __init__(self, width: float, height: float): self.width, self.height = width, height def area(self) -> float: return self.width * self.height shapes = [Circle(3), Rectangle(4, 5)] for shape in shapes: print(f"{type(shape).__name__}: {shape.area():.2f}")
type Shape interface { Area() float64 TypeName() string } type Circle struct{ Radius float64 } type Rectangle struct{ Width, Height float64 } func (circle Circle) Area() float64 { return 3.14159 * circle.Radius * circle.Radius } func (circle Circle) TypeName() string { return "Circle" } func (rect Rectangle) Area() float64 { return rect.Width * rect.Height } func (rect Rectangle) TypeName() string { return "Rectangle" } shapes := []Shape{Circle{Radius: 3}, Rectangle{Width: 4, Height: 5}} for _, shape := range shapes { fmt.Printf("%s: %.2f\n", shape.TypeName(), shape.Area()) }
A []Shape slice can hold any value whose type satisfies the Shape interface. This is equivalent to Python's duck typing — if it has the methods, it qualifies — but checked at compile time. A missing method on any type in the slice is a compile error, not a runtime AttributeError.
The any type (interface{})
# Python is dynamically typed — any variable holds any type. items = [1, "hello", True, 3.14] for item in items: print(type(item).__name__, item)
items := []any{1, "hello", true, 3.14} for _, item := range items { fmt.Printf("%T %v\n", item, item) }
any (an alias for interface{}) is Go's escape hatch from the static type system. Values stored as any lose their specific compile-time type. Python's dynamic typing makes every variable implicitly any; in Go, any should be used sparingly — only when the type is genuinely unknown at compile time. The %T verb prints the runtime type.
Error Handling
Error return values (vs exceptions)
def divide(numerator: float, denominator: float) -> float: if denominator == 0: raise ValueError("cannot divide by zero") return numerator / denominator try: print(divide(10, 2)) print(divide(10, 0)) except ValueError as error: print(f"Error: {error}")
func divide(numerator, denominator float64) (float64, error) { if denominator == 0 { return 0, errors.New("cannot divide by zero") } return numerator / denominator, nil } result, err := divide(10, 2) if err != nil { fmt.Println("Error:", err) } else { fmt.Println(result) } result, err = divide(10, 0) if err != nil { fmt.Println("Error:", err) }
Go uses error return values instead of exceptions. Functions that can fail return (result, error); the caller checks err != nil. This makes error paths explicit and visible at every call site — there is no hidden exception propagation. Python's raise/except allows errors to propagate silently; Go's approach requires explicit handling at each level.
Wrapping errors with context
class ConfigError(Exception): pass def load_config(filename: str) -> dict: try: raise FileNotFoundError(f"no file: {filename}") except FileNotFoundError as error: raise ConfigError(f"config load failed: {error}") from error try: load_config("config.json") except ConfigError as error: print(error)
func openFile(filename string) error { return fmt.Errorf("no file: %s", filename) } func loadConfig(filename string) error { if err := openFile(filename); err != nil { return fmt.Errorf("config load failed: %w", err) } return nil } if err := loadConfig("config.json"); err != nil { fmt.Println(err) }
fmt.Errorf("context: %w", err) wraps an error with additional context. The %w verb preserves the original error so callers can use errors.Is() to match specific errors and errors.As() to extract specific error types from the chain. Python uses raise ... from for the same chaining purpose.
Custom error types
class ValidationError(Exception): def __init__(self, field: str, message: str): self.field = field self.message = message super().__init__(f"{field}: {message}") try: raise ValidationError("email", "invalid format") except ValidationError as error: print(f"Validation failed on '{error.field}': {error.message}")
type ValidationError struct { Field string Message string } func (validationErr ValidationError) Error() string { return fmt.Sprintf("%s: %s", validationErr.Field, validationErr.Message) } func validate(email string) error { if email == "" { return ValidationError{Field: "email", Message: "invalid format"} } return nil } err := validate("") var validationErr ValidationError if errors.As(err, &validationErr) { fmt.Printf("Validation failed on '%s': %s\n", validationErr.Field, validationErr.Message) }
A Go type implements the error interface by providing an Error() string method — there is no base class to extend. Use errors.As(err, &target) to type-assert an error to a specific custom type. This corresponds to Python's except CustomError as error, but without the exception hierarchy.
Goroutines & Concurrency
Goroutines (vs threading.Thread)
import threading def worker(worker_id: int) -> None: print(f"Worker {worker_id} running") threads = [threading.Thread(target=worker, args=(worker_id,)) for worker_id in range(3)] for thread in threads: thread.start() for thread in threads: thread.join()
var waitGroup sync.WaitGroup for workerID := 0; workerID < 3; workerID++ { waitGroup.Add(1) go func(identifier int) { defer waitGroup.Done() fmt.Printf("Worker %d running\n", identifier) }(workerID) } waitGroup.Wait()
Goroutines start at ~2 KB of stack and are multiplexed across OS threads by the Go runtime. Python threads map 1:1 to OS threads and are limited by the GIL for CPU-bound work. sync.WaitGroup waits for all goroutines to finish, like thread.join(). Pass the loop variable as an argument to the goroutine to avoid capturing it by reference.
Channels (vs queue.Queue)
import queue import threading def send_message(message_queue: queue.Queue) -> None: message_queue.put("Hello from worker") message_queue = queue.Queue() worker = threading.Thread(target=send_message, args=(message_queue,)) worker.start() worker.join() message = message_queue.get() print(message)
messages := make(chan string) go func() { messages <- "Hello from worker" }() message := <-messages fmt.Println(message)
Go channels are typed, first-class values for communication between goroutines — similar in purpose to Python's queue.Queue but built into the language. The <- operator sends to a channel (left of chan) and receives from one (right of chan). An unbuffered channel synchronizes sender and receiver — the sender blocks until the receiver is ready.
Mutual exclusion (sync.Mutex vs threading.Lock)
import threading lock = threading.Lock() total = [0] # list allows mutation inside nested function def increment(): with lock: total[0] += 1 workers = [threading.Thread(target=increment) for _ in range(5)] for worker in workers: worker.start() for worker in workers: worker.join() print(total[0])
type SafeCounter struct { mu sync.Mutex value int } func (counter *SafeCounter) Increment() { counter.mu.Lock() defer counter.mu.Unlock() counter.value++ } safeCounter := &SafeCounter{} var waitGroup sync.WaitGroup for index := 0; index < 5; index++ { waitGroup.Add(1) go func() { defer waitGroup.Done() safeCounter.Increment() }() } waitGroup.Wait() fmt.Println(safeCounter.value)
Python's threading.Lock is typically used as a context manager (with lock:). Go's sync.Mutex requires explicit Lock()/Unlock() calls; pairing Unlock() with defer ensures the lock is released even if the method panics. Note that Python's GIL already prevents concurrent bytecode execution — a Lock in Python is needed for atomicity of compound operations, not raw memory safety.
Packages & Modules
Imports
import math import os from datetime import datetime print(math.sqrt(16)) print(datetime.now().year)
fmt.Println(math.Sqrt(16)) fmt.Println(time.Now().Year())
Go imports entire packages by their import path; the package name (last component of the path) becomes the qualifier. All imports are declared at the top of the file and are enforced by the compiler — an unused import is a compile error (unlike Python where it is only a style warning). The runner adds import declarations automatically based on the packages you use.
Package visibility (exported vs unexported)
# Python convention: _ prefix means "private by convention". # Nothing prevents external code from accessing _private_thing. _internal_limit = 42 public_limit = 100 def _internal_helper() -> str: return "internal" def public_function() -> str: return _internal_helper() print(public_function())
// Uppercase first letter = exported (accessible from any package). // Lowercase first letter = package-private (inaccessible from outside). const internalLimit = 42 const PublicLimit = 100 func internalHelper() string { return "internal" } func PublicFunction() string { return internalHelper() } fmt.Println(PublicFunction())
Go's visibility rule is simple and compiler-enforced: any identifier starting with an uppercase letter is exported; lowercase means package-private. Python's _ prefix convention is respected by tools like linters and IDEs but not enforced by the interpreter — any code can still access module._private_thing. In Go, external packages cannot even refer to an unexported name at compile time.
Package initialization (init() vs module-level code)
# Python: module-level code runs when the module is imported. print("module setup starting") DEFAULT_SIZE = 100 def compute_max() -> int: return DEFAULT_SIZE * 2 MAX_VALUE = compute_max() print(f"MAX_VALUE = {MAX_VALUE}")
var defaultSize = 100 var maxValue = computeMax() func computeMax() int { return defaultSize * 2 } func init() { fmt.Println("package setup complete") } fmt.Println("maxValue =", maxValue)
Go package-level variables are initialized in declaration order; init() runs after all variables are initialized, before main(). A package may have multiple init() functions — they run in source order. Unlike Python module-level code, init() cannot be called directly. The _ import (import _ "pkg") triggers a package's init() for side effects only.