PONY λ M2 Modula-2

C.CodeCompared.To/Go

An interactive executable cheatsheet comparing C and Go

C17 (GCC) Go 1.26.2
Hello World & Build System
Hello, World
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
fmt.Println("Hello, World!")
Go automatically wraps snippets in package main and func main(). The fmt import is added automatically by this runner.
Compile & run
// Compile: gcc -o hello hello.c && ./hello // Or: cc hello.c -o hello && ./hello // With libs: gcc hello.c -lm -o hello
// go build -o hello && ./hello // Or: go run hello.go // Both compile; go run is faster for one-off runs
Go has a single toolchain with no Makefile required. go run compiles and executes in one step. go build produces a statically linked binary with no runtime dependencies.
Imports vs #include
#include <stdio.h> #include <stdlib.h> #include <string.h> // Header guards prevent double-inclusion: // #ifndef MYLIB_H // #define MYLIB_H // ... declarations ... // #endif
import ( "fmt" "os" "strings" ) // Unused imports are a compile error in Go
Go imports package paths, not header files. There are no header/source splits. Unused imports are a compile error, preventing the header-bloat that C codebases accumulate over time.
Variables & Types
Variable declaration
#include <stdio.h> int main(void) { int count = 0; double ratio = 3.14; char letter = 'A'; printf("count=%d ratio=%.2f letter=%c\n", count, ratio, letter); return 0; }
count := 0 ratio := 3.14 letter := 'A' fmt.Printf("count=%d ratio=%.2f letter=%c\n", count, ratio, letter)
The := operator declares and initializes in one step with type inferred from the right side. Explicit typed form: var count int = 0. All Go variables are zero-initialized by default — no garbage values.
Integer types & sizes
#include <stdio.h> #include <stdint.h> int main(void) { int8_t small = 127; int32_t medium = 2147483647; int64_t large = 9223372036854775807LL; printf("%d %d %lld\n", small, medium, large); return 0; }
var small int8 = 127 var medium int32 = 2147483647 var large int64 = 9223372036854775807 fmt.Println(small, medium, large)
Go's integer types are explicit and portable — int8, int16, int32, int64 and unsigned variants. No stdint.h required. Plain int is 64-bit on 64-bit platforms.
Boolean type
#include <stdio.h> #include <stdbool.h> int main(void) { bool found = false; bool ready = true; if (!found && ready) { printf("go\n"); } return 0; }
found := false ready := true if !found && ready { fmt.Println("go") }
Go has a native bool type. Unlike C, integers are never implicitly treated as booleans — if 1 is a compile error. This eliminates an entire class of C bugs.
Constants & enumerations
#include <stdio.h> #define MAX_SIZE 100 enum Color { RED, GREEN, BLUE }; int main(void) { const double pi = 3.14159265358979; enum Color favorite = GREEN; printf("pi=%.5f MAX=%d color=%d\n", pi, MAX_SIZE, favorite); return 0; }
const MaxSize = 100 const Pi = 3.14159265358979 const ( Red = iota Green Blue ) fmt.Printf("Pi=%.5f MaxSize=%d color=%d\n", Pi, MaxSize, Green)
iota is Go's enumeration mechanism — it auto-increments within a const block, replacing C's enum. Go constants can be untyped and arbitrarily precise, avoiding the overflow surprises of C's #define.
No implicit numeric conversion
#include <stdio.h> int main(void) { int count = 10; double wrong = count / 3; // integer division: 3.0 double correct = (double)count / 3; // explicit cast needed printf("wrong=%.4f correct=%.4f\n", wrong, correct); return 0; }
count := 10 // ratio := count / 3.0 // compile error: mismatched types correct := float64(count) / 3.0 fmt.Printf("correct=%.4f\n", correct)
Go requires explicit type conversions between numeric types. There are no implicit promotions, eliminating the silent truncation and precision-loss bugs that C's arithmetic rules are infamous for.
Strings & Bytes
Strings: value type vs null-terminated
#include <stdio.h> #include <string.h> int main(void) { const char *greeting = "Hello"; // pointer to read-only memory; strlen is O(n) printf("length: %zu bytes\n", strlen(greeting)); return 0; }
greeting := "Hello" // immutable value type; len() is O(1) fmt.Println("length:", len(greeting), "bytes")
Go strings are immutable byte sequences with a stored length — len() is O(1). They are not null-terminated and can contain any bytes including null. No buffer overflows from missing null terminators.
String concatenation
#include <stdio.h> #include <string.h> int main(void) { char result[64]; strcpy(result, "Hello"); strcat(result, ", "); strcat(result, "World!"); printf("%s\n", result); return 0; }
result := "Hello" + ", " + "World!" fmt.Println(result)
Go strings concatenate with +. For building strings in a loop, strings.Builder is the efficient equivalent of a C character buffer. No manual buffer sizing or overflow risk.
String formatting
#include <stdio.h> int main(void) { char buffer[128]; snprintf(buffer, sizeof(buffer), "Name: %s, Age: %d", "Alice", 30); printf("%s\n", buffer); return 0; }
message := fmt.Sprintf("Name: %s, Age: %d", "Alice", 30) fmt.Println(message)
fmt.Sprintf returns a string — no pre-allocated buffer needed, no size limit. The format verbs are similar to printf but %v prints any type automatically.
UTF-8 and Unicode
#include <stdio.h> #include <string.h> int main(void) { // C has no built-in Unicode — strlen counts bytes const char *text = "Hello!"; printf("bytes: %zu\n", strlen(text)); return 0; }
text := "Hello U0001F30D" // 🌍 is 4 UTF-8 bytes fmt.Println("bytes:", len(text)) fmt.Println("runes:", len([]rune(text)))
Go source and strings are UTF-8 by definition. A rune is a Unicode code point (alias for int32). Iterating with range over a string yields runes, not bytes.
Arrays & Slices
Fixed-size arrays
#include <stdio.h> int main(void) { int scores[5] = {10, 20, 30, 40, 50}; printf("first: %d, length: %d\n", scores[0], 5); return 0; }
scores := [5]int{10, 20, 30, 40, 50} fmt.Println("first:", scores[0], "length:", len(scores))
Go arrays are values — assigning an array copies it entirely. The size is part of the type: [5]int and [10]int are incompatible types. Arrays are rarely used directly; slices are preferred.
Slices (dynamic arrays)
#include <stdio.h> #include <stdlib.h> int main(void) { int capacity = 4; int *numbers = malloc(capacity * sizeof(int)); numbers[0] = 1; numbers[1] = 2; numbers[2] = 3; numbers[3] = 4; printf("len=%d first=%d last=%d\n", capacity, numbers[0], numbers[3]); free(numbers); return 0; }
numbers := []int{1, 2, 3} numbers = append(numbers, 4) fmt.Println(numbers, "len:", len(numbers))
A slice is a reference to a backing array with length and capacity metadata. append grows it automatically — no manual realloc. This is the Go equivalent of a C dynamic array, without the memory management burden.
Maps (hash tables)
// C has no built-in hash map. // Common options: uthash, GLib GHashTable, // or a hand-rolled open-addressing table. // Each requires manual setup, teardown, and // collision-handling decisions.
ages := map[string]int{ "Alice": 30, "Bob": 25, } ages["Carol"] = 35 fmt.Println(ages["Alice"])
Go's built-in map is a hash table with O(1) average operations — no external library. Accessing a missing key returns the zero value; use value, ok := m[key] to distinguish "missing" from "zero".
Bounds checking
#include <stdio.h> int main(void) { int data[5] = {1, 2, 3, 4, 5}; printf("last valid: %d\n", data[4]); // data[10] is undefined behavior in C — // may crash, corrupt memory, or silently // return a garbage value. return 0; }
data := []int{1, 2, 3, 4, 5} fmt.Println("last valid:", data[4]) // data[10] would panic: runtime index out of range
Go performs runtime bounds checking on every slice/array access. Out-of-bounds access panics with a clear error instead of silently corrupting memory. This eliminates a major category of C security vulnerabilities.
Memory Management
malloc/free vs garbage collection
#include <stdio.h> #include <stdlib.h> int main(void) { int *buffer = malloc(8 * sizeof(int)); if (!buffer) { return 1; } for (int i = 0; i < 8; i++) buffer[i] = i * i; printf("buffer[3] = %d\n", buffer[3]); free(buffer); // must not forget; must not double-free return 0; }
buffer := make([]int, 8) for index := range buffer { buffer[index] = index * index } fmt.Println("buffer[3] =", buffer[3]) // no free() — GC reclaims it automatically
Go's garbage collector eliminates use-after-free, double-free, and memory leak bugs. The GC runs concurrently, typically adding sub-millisecond pauses. For most applications the throughput cost is negligible.
make and new
#include <stdio.h> #include <stdlib.h> int main(void) { // Allocate a single int on the heap: int *pointer = malloc(sizeof(int)); *pointer = 42; printf("value: %d\n", *pointer); free(pointer); return 0; }
// new allocates a zero-valued T and returns *T pointer := new(int) *pointer = 42 fmt.Println("value:", *pointer) // make creates slices, maps, channels (initialized) numbers := make([]int, 5) fmt.Println("slice:", numbers)
new(T) is analogous to malloc(sizeof(T)) but returns a typed pointer and zero-initializes. make is only for slices, maps, and channels. Both are freed automatically by the GC.
Stack vs heap — escape analysis
#include <stdio.h> #include <stdlib.h> // Returning a pointer to a local is UB in C: // int* bad(void) { int x = 42; return &x; } int *good(void) { int *heap_ptr = malloc(sizeof(int)); *heap_ptr = 42; return heap_ptr; // caller must free } int main(void) { int *result = good(); printf("%d\n", *result); free(result); return 0; }
func makeValue() *int { value := 42 return &value // safe: Go detects this escapes to heap } result := makeValue() fmt.Println(*result)
Go's compiler performs escape analysis — variables that outlive their function are automatically heap-allocated. Returning a pointer to a local variable is safe in Go; the compiler moves it to the heap. In C, that is undefined behavior.
Pointers
Pointers: & and *
#include <stdio.h> int main(void) { int value = 42; int *pointer = &value; printf("value: %d\n", *pointer); *pointer = 100; printf("modified: %d\n", value); return 0; }
value := 42 pointer := &value fmt.Println("value:", *pointer) *pointer = 100 fmt.Println("modified:", value)
Go pointer syntax is identical to C: & takes an address, * dereferences. What Go lacks is pointer arithmetic — you cannot do ptr++ or ptr + 2. This eliminates buffer overflows from manual pointer walking.
No pointer arithmetic
#include <stdio.h> int main(void) { int numbers[] = {10, 20, 30}; int *pointer = numbers; printf("%d\n", *pointer); // 10 pointer++; printf("%d\n", *pointer); // 20 printf("%d\n", *(pointer + 1)); // 30 return 0; }
numbers := []int{10, 20, 30} // Iterate by index — pointer arithmetic not allowed for index, value := range numbers { fmt.Println(index, value) }
Go deliberately omits pointer arithmetic. Array traversal is done with range or index expressions. The unsafe package provides pointer arithmetic for systems programming, but it bypasses all safety guarantees.
Nil pointers
#include <stdio.h> int main(void) { int *pointer = NULL; if (pointer != NULL) { printf("%d\n", *pointer); } else { printf("null pointer\n"); } return 0; }
var pointer *int // zero value for a pointer is nil if pointer != nil { fmt.Println(*pointer) } else { fmt.Println("nil pointer") }
Go's nil is the zero value for pointers, interfaces, slices, maps, channels, and functions. Dereferencing a nil pointer panics with a clear message instead of undefined behavior.
Control Flow
for — the only loop keyword
#include <stdio.h> int main(void) { for (int i = 0; i < 5; i++) { printf("%d\n", i); } return 0; }
for i := 0; i < 5; i++ { fmt.Println(i) }
Go has only one loop keyword: for. It covers C's for, while, and infinite loops. No parentheses around the condition, but braces are required.
while-style loop
#include <stdio.h> int main(void) { int count = 0; while (count < 3) { printf("%d\n", count); count++; } return 0; }
count := 0 for count < 3 { fmt.Println(count) count++ }
A for with only a condition is Go's while. An infinite loop is for { ... } — equivalent to C's while(1) { ... }.
range — iterating collections
#include <stdio.h> int main(void) { int scores[] = {10, 20, 30, 40, 50}; int length = 5; for (int i = 0; i < length; i++) { printf("index=%d value=%d\n", i, scores[i]); } return 0; }
scores := []int{10, 20, 30, 40, 50} for index, value := range scores { fmt.Printf("index=%d value=%d\n", index, value) }
range yields (index, value) pairs for slices, (key, value) for maps, and (index, rune) for strings. Use _ to discard either: for _, value := range scores.
switch — no fallthrough by default
#include <stdio.h> int main(void) { int day = 3; switch (day) { case 1: printf("Mon\n"); break; // break required case 2: printf("Tue\n"); break; case 3: printf("Wed\n"); break; default: printf("other\n"); break; } return 0; }
day := 3 switch day { case 1: fmt.Println("Mon") case 2: fmt.Println("Tue") case 3: fmt.Println("Wed") default: fmt.Println("other") }
Go's switch does not fall through by default — no break needed. Use fallthrough explicitly when you want C's behavior. Cases can match multiple values: case 1, 2, 3:.
Functions
Multiple return values
#include <stdio.h> // C can only return one value — use output pointer: int divide(int a, int b, int *remainder) { *remainder = a % b; return a / b; } int main(void) { int remainder; int quotient = divide(17, 5, &remainder); printf("%d remainder %d\n", quotient, remainder); return 0; }
func divide(a, b int) (int, int) { return a / b, a % b } quotient, remainder := divide(17, 5) fmt.Println(quotient, "remainder", remainder)
Go functions can return multiple values cleanly — no output pointer parameters needed. The idiomatic pattern for errors: value, err := someFunc().
Variadic functions
#include <stdio.h> #include <stdarg.h> int sum(int count, ...) { va_list args; va_start(args, count); int total = 0; for (int i = 0; i < count; i++) total += va_arg(args, int); va_end(args); return total; } int main(void) { printf("%d\n", sum(3, 10, 20, 30)); return 0; }
func sum(numbers ...int) int { total := 0 for _, number := range numbers { total += number } return total } fmt.Println(sum(10, 20, 30))
Go variadic functions receive a typed slice, not a raw va_list. This is type-safe and bounds-checked. Spread a slice into variadic args with sum(numbers...).
First-class functions
#include <stdio.h> int add(int a, int b) { return a + b; } int apply(int (*operation)(int, int), int a, int b) { return operation(a, b); } int main(void) { printf("%d\n", apply(add, 3, 4)); return 0; }
apply := func(operation func(int, int) int, a, b int) int { return operation(a, b) } add := func(a, b int) int { return a + b } fmt.Println(apply(add, 3, 4))
Go functions are first-class values. Anonymous functions (closures) can capture variables from the enclosing scope — unlike C function pointers, which cannot close over local variables without a separate context struct.
defer — cleanup without goto
#include <stdio.h> void process(void) { printf("opening\n"); // Must remember to close at every exit point. // C idiom: goto cleanup at end of function. printf("working\n"); printf("closing\n"); // easy to forget on early return } int main(void) { process(); return 0; }
fmt.Println("opening") defer fmt.Println("closing") // runs when function returns fmt.Println("working") // closing prints last, even if a panic occurs
defer schedules a function call to run when the enclosing function exits — whether normally or via panic. It replaces the C pattern of duplicating cleanup code at every return and the goto cleanup idiom.
Structs & Methods
Struct definition
#include <stdio.h> typedef struct { char name[64]; int age; } Employee; int main(void) { Employee alice = {"Alice", 30}; printf("%s is %d\n", alice.name, alice.age); return 0; }
type Employee struct { Name string Age int } alice := Employee{Name: "Alice", Age: 30} fmt.Println(alice.Name, "is", alice.Age)
Go structs are similar to C structs — no classes, no inheritance. Exported fields start with a capital letter (visible outside the package); unexported fields start lowercase.
Methods on structs
#include <stdio.h> #include <math.h> typedef struct { double x, y; } Point; // C convention: pass struct pointer as first argument double distance(const Point *point) { return sqrt(point->x * point->x + point->y * point->y); } int main(void) { Point origin = {3.0, 4.0}; printf("%.1f\n", distance(&origin)); return 0; }
type Point struct{ X, Y float64 } func (point Point) Distance() float64 { return math.Sqrt(point.X*point.X + point.Y*point.Y) } origin := Point{3, 4} fmt.Println(origin.Distance())
Go methods attach functions to types via a receiver. Value receivers (point Point) get a copy; pointer receivers (point *Point) modify the original. This is identical in spirit to C's "pass struct pointer as first arg" convention, formalized in the language.
Struct embedding (composition)
#include <stdio.h> typedef struct { int x, y; } Point; typedef struct { Point center; // must use center.x, center.y double radius; } Circle; int main(void) { Circle circle = {{1, 2}, 5.0}; printf("(%d, %d) r=%.0f\n", circle.center.x, circle.center.y, circle.radius); return 0; }
type Point struct{ X, Y int } type Circle struct { Point // embedded: Circle.X and Circle.Y work directly Radius float64 } circle := Circle{Point: Point{1, 2}, Radius: 5} fmt.Printf("(%d, %d) r=%.0f\n", circle.X, circle.Y, circle.Radius)
Go embedding promotes the fields and methods of the embedded type to the outer struct. This is Go's primary composition mechanism — not inheritance. There is no polymorphism implied.
Interfaces
Interfaces are satisfied implicitly
// C achieves polymorphism via function pointers in structs. // Every "class" must manually wire up its vtable: // // typedef struct { // void (*speak)(void *self); // } AnimalVtable; // // typedef struct { // AnimalVtable *vtable; // char name[64]; // } Dog; // // void dog_speak(void *self) { ... } // AnimalVtable dog_vtable = { dog_speak };
type Speaker interface { Speak() string } type Dog struct{ Name string } func (dog Dog) Speak() string { return "Woof!" } func makeNoise(speaker Speaker) { fmt.Println(speaker.Speak()) } makeNoise(Dog{Name: "Rex"})
Go interfaces are satisfied implicitly — if a type has the required methods, it satisfies the interface with no declaration needed. This is structural typing, eliminating the manual vtable wiring that C requires for polymorphism.
Any value: the empty interface
#include <stdio.h> // C uses void* for "any type" — no type information attached void print_int(void *data) { printf("%d\n", *(int*)data); } int main(void) { int number = 42; print_int(&number); return 0; }
func printAnything(value any) { fmt.Println(value) } printAnything(42) printAnything("hello") printAnything([]int{1, 2, 3})
any (alias for interface{}) accepts any value. Unlike C's void*, it carries runtime type information — use a type switch or type assertion to extract the concrete value safely.
Error Handling
Errors as return values
#include <stdio.h> #include <errno.h> #include <string.h> int safe_divide(int a, int b, int *result) { if (b == 0) { errno = EDOM; return -1; } *result = a / b; return 0; } int main(void) { int result; if (safe_divide(10, 0, &result) != 0) { fprintf(stderr, "Error: %s\n", strerror(errno)); } else { printf("result: %d\n", result); } return 0; }
import "errors" func safeDivide(a, b int) (int, error) { if b == 0 { return 0, errors.New("division by zero") } return a / b, nil } result, err := safeDivide(10, 0) if err != nil { fmt.Println("Error:", err) } else { fmt.Println("result:", result) }
Go returns errors as values — result, err := fn() followed by if err != nil. This is explicit and impossible to accidentally ignore, unlike C's global errno which is often overlooked.
Error wrapping and inspection
// C has no standard error wrapping. // strerror(errno) gives a string, not a structured type. // Propagating error context means string concatenation // or a custom error-code enum — both are error-prone.
import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func lookup(key string) error { return fmt.Errorf("lookup %q: %w", key, ErrNotFound) } err := lookup("missing-key") fmt.Println(err) fmt.Println("is ErrNotFound:", errors.Is(err, ErrNotFound))
fmt.Errorf("...: %w", err) wraps an error with context. errors.Is unwraps chains to find a target error. This gives structured error propagation that C's errno-based system entirely lacks.
Panic and recover
#include <stdio.h> #include <stdlib.h> // C: unrecoverable errors terminate via abort() or exit() // SIGFPE from divide-by-zero is catchable but recovery // from undefined behavior is implementation-defined. int main(void) { printf("before\n"); // abort(); // terminates immediately, no recovery printf("after\n"); return 0; }
func safeDiv(a, b int) (result int, err error) { defer func() { if recovered := recover(); recovered != nil { err = fmt.Errorf("recovered: %v", recovered) } }() return a / b, nil } result, err := safeDiv(10, 0) fmt.Println(result, err)
panic is Go's equivalent of abort — for truly unrecoverable situations. recover inside a defer can catch a panic and convert it to an error. This is rare in Go; most error handling uses return values.
Goroutines & Channels
Goroutines vs threads
// POSIX threads — requires linking with -lpthread: // // #include <pthread.h> // void *worker(void *arg) { printf("working\n"); return NULL; } // pthread_t thread; // pthread_create(&thread, NULL, worker, NULL); // pthread_join(thread, NULL); // // Each thread: ~8MB stack, ~10µs to start. // Goroutines: ~2KB stack, ~200ns to start.
import "sync" var waitGroup sync.WaitGroup waitGroup.Add(1) go func() { defer waitGroup.Done() fmt.Println("working") }() waitGroup.Wait()
Goroutines are multiplexed on OS threads by the Go runtime — starting one costs ~2KB of stack vs ~8MB for a typical POSIX thread. A program can run millions of goroutines. The go keyword launches one with no thread-creation overhead.
Channels for communication
// C producer/consumer requires a mutex + condition variable: // // pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // int value_ready = 0; // int shared_value; // ... extensive setup omitted ...
channel := make(chan int, 1) go func() { channel <- 42 // send }() value := <-channel // receive fmt.Println(value)
Channels are typed communication pipes between goroutines. Go's motto: "Don't communicate by sharing memory; share memory by communicating." Channels replace the mutex + shared state pattern that dominates C concurrent code.
Packages & Modules
Packages vs #include
// C: include header files; no enforced encapsulation. // Any file can include any header from anywhere. // // #include "mylib.h" // #include "../utils/helper.h" // // "Private" functions are by convention (static) or naming, // not enforced by the language.
// Go: import by module path; compiler enforces boundaries. // Exported names start with a capital letter (public). // Unexported names start lowercase (private to the package). // import "myproject/mylib" // import "myproject/utils/helper" fmt.Println("Println is exported from package fmt")
Go packages are directory-based units of encapsulation. Capital-letter names are exported (public); lowercase is unexported (private). This replaces C's convention of static for file-local visibility.
Go modules vs Makefile
# C: dependencies managed manually or via pkg-config/CMake. # No standard dependency manager in the language itself. # # Typical approaches: # - System libraries via pkg-config # - Vendored source trees # - CMakeLists.txt or Makefile with find_package()
// go.mod declares the module and dependencies: // module myproject // go 1.26 // require github.com/some/library v1.2.3 // // Commands: // go get github.com/some/library — fetch and add // go build ./... — build everything // go test ./... — test everything fmt.Println("module system is built in")
Go modules (go.mod) are the standard dependency system — no Makefile or CMake required. Dependencies are versioned, checksummed, and cached. Reproducible builds work out of the box.
Gotchas for C Programmers
Zero values: no garbage initialization
#include <stdio.h> int main(void) { // C: uninitialized local variables contain garbage. // Always initialize before use! int count = 0; // explicit zero double ratio = 0.0; // explicit zero char *pointer = NULL; // explicit null printf("count=%d ratio=%.1f pointer=%p\n", count, ratio, (void*)pointer); return 0; }
var count int // 0 var ratio float64 // 0.0 var message string // "" var ready bool // false var pointer *int // nil fmt.Println(count, ratio, message, ready, pointer)
Every Go variable is initialized to its zero value — no garbage. Integers are 0, strings are "", booleans are false, pointers are nil. This eliminates an entire class of C bugs from reading uninitialized memory.
Structs are copied by value
#include <stdio.h> typedef struct { int x, y; } Point; int main(void) { Point original = {1, 2}; Point copied = original; // full copy of struct copied.x = 99; printf("original.x = %d\n", original.x); // still 1 printf("copied.x = %d\n", copied.x); // 99 return 0; }
type Point struct{ X, Y int } original := Point{1, 2} copied := original // full copy copied.X = 99 fmt.Println("original:", original.X) // still 1 fmt.Println("copy:", copied.X) // 99
Go structs are value types — assignment copies the entire struct. C programmers expect this, but Go newcomers from OOP languages often don't. Pass a pointer *Point to avoid copying or to allow mutation.
GC pauses and latency
// C: no GC — deterministic latency. // malloc/free run exactly when you call them. // Essential for hard real-time, embedded systems, // and game engines where frame-time budgets are fixed.
// Go's GC runs concurrently but can cause brief pauses. // For most web/cloud services: pauses are <1ms. // Tuning options: // GOGC=200 — GC less often (more memory, fewer pauses) // runtime.GC() — trigger GC at a predictable moment // sync.Pool — reuse objects to reduce GC pressure fmt.Println("GC is tunable but not eliminatable")
The Go GC is a tradeoff: you give up deterministic memory control but gain freedom from use-after-free and leaks. For most applications this is excellent. For hard real-time requirements, C or Rust remains the better choice.
No header files or forward declarations
#include <stdio.h> // Must declare before use, or include a header: int add(int, int); // forward declaration int main(void) { printf("%d\n", add(2, 3)); return 0; } int add(int a, int b) { return a + b; }
// Go: any order — compiler resolves the whole package func add(a, b int) int { return a + b } fmt.Println(add(2, 3))
Go has no header files. The compiler reads all .go files in a package together — function order is irrelevant, no forward declarations needed. This eliminates the class of C bugs where declaration and definition disagree.