PONYλM2Modula-2

C.CodeCompared.To/Go

An interactive executable cheatsheet comparing C and Go

C17 (GCC) Go 1.26.5
Hello World & Build System
Hello, World
On this page, Go snippets are automatically wrapped in package main and func main(), and the fmt import is added by the runner.
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
fmt.Println("Hello, World!")
fmt.Println is Go's everyday print — it adds the newline itself, unlike printf.
Compile & run
Go has a single toolchain with no Makefile required.
// 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 run compiles and executes in one step. go build produces a statically linked binary with no runtime dependencies.
Imports vs #include
Go imports package paths, not header files — there is no header/source split and no include guards.
#include <stdio.h> #include <stdlib.h> #include <string.h> // Header guards prevent double-inclusion: // #ifndef MYLIB_H // #define MYLIB_H // ... declarations ... // #endif
import ( "fmt" "strings" "os" ) fmt.Println(strings.ToUpper("hello, world")) fmt.Println("GOOS:", os.Getenv("GOOS")) // Unused imports are a compile error in Go
Unused imports are a compile error, preventing the header-bloat that C codebases accumulate over time.
Variables & Types
Variable declaration
The := operator declares and initializes in one step, with the type inferred from the right side.
#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 explicit typed form is var count int = 0. All Go variables are zero-initialized by default — no garbage values.
Integer types & sizes
Go's integer types are explicit and portable — int8, int16, int32, int64 and unsigned variants, with no stdint.h required.
#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)
Plain int is 64-bit on 64-bit platforms.
Boolean type
Go has a native bool type, and integers are never implicitly treated as booleans — if 1 is a compile error.
#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") }
This eliminates the entire class of C bugs where an integer or assignment quietly stands in for a condition.
Constants & enumerations
iota is Go's enumeration mechanism — it auto-increments within a const block, replacing C's enum.
#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)
Go constants can be untyped and arbitrarily precise, avoiding the overflow surprises of C's #define.
No implicit numeric conversion
Go requires explicit conversions between numeric types — there are no implicit promotions at all.
#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)
This eliminates the silent truncation and precision-loss bugs that C's arithmetic conversion rules are infamous for.
Strings & Bytes
Strings: value type vs null-terminated
Go strings are immutable byte sequences with a stored length — len() is O(1), and there is no null terminator.
#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")
A Go string can contain any bytes, including null bytes. No buffer overflows from missing terminators.
String concatenation
Go strings concatenate with + — no destination buffer, no strcpy/strcat pair.
#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)
For building strings in a loop, strings.Builder is the efficient equivalent of a C character buffer, without the manual sizing or overflow risk.
String formatting
fmt.Sprintf returns a string — no pre-allocated buffer, no size limit.
#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)
The format verbs are similar to printf, plus %v, which prints any type automatically.
UTF-8 and Unicode
Go source and strings are UTF-8 by definition, and a rune is a Unicode code point (an alias for int32).
#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)))
Iterating with range over a string yields runes, not bytes — which is why byte length and rune count differ for the globe emoji above.
Arrays & Slices
Fixed-size arrays
Go arrays are values with the size baked into the type — [5]int and [10]int are incompatible types.
#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))
Assigning an array copies it entirely. Arrays are rarely used directly in Go; slices (next example) are preferred.
Slices (dynamic arrays)
A slice is Go's dynamic array: a reference to a backing array plus length and capacity metadata, grown automatically by append.
#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))
There is no manual realloc and no free — the slice is the C dynamic-array pattern without the memory management burden.
Maps (hash tables)
Go's built-in map is a hash table with O(1) average operations — the container C leaves to external libraries or hand-rolled code.
// 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"])
Accessing a missing key returns the zero value; use value, ok := m[key] to distinguish "missing" from "zero".
Bounds checking
Go performs runtime bounds checking on every slice and array access.
#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
Out-of-bounds access panics with a clear error instead of silently corrupting memory — eliminating a major category of C security vulnerabilities.
Memory Management
malloc/free vs garbage collection
Go's garbage collector eliminates use-after-free, double-free, and forgotten-free leak bugs — there is simply nothing to free.
#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
The GC runs concurrently, typically adding sub-millisecond pauses. For most applications the throughput cost is negligible.
make and new
Go has two allocation builtins: new(T), analogous to malloc(sizeof(T)) but typed and zero-initialized, and make, used only for slices, maps, and channels.
#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)
Both are freed automatically by the GC.
Stack vs heap — escape analysis
Returning a pointer to a local variable — undefined behavior in C — is perfectly safe in Go.
#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)
The compiler's escape analysis detects that the variable outlives its function and heap-allocates it automatically. Stack versus heap is an optimization decision the compiler makes, not a correctness decision the programmer must get right.
Pointers
Pointers: & and *
Go pointer syntax is identical to C: & takes an address, * dereferences.
#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)
What Go lacks is pointer arithmetic — no incrementing a pointer, no adding an offset to one. This eliminates buffer overflows from manual pointer walking.
No pointer arithmetic
Go deliberately omits pointer arithmetic — array traversal is done with range or index expressions.
#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) }
The unsafe package provides pointer arithmetic for systems programming, but it bypasses all safety guarantees.
Nil pointers
Go's nil is the zero value for pointers, interfaces, slices, maps, channels, and functions.
#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") }
Dereferencing a nil pointer panics with a clear message instead of undefined behavior.
Control Flow
for — the only loop keyword
Go has only one loop keyword: for. It covers C's for, while, and infinite loops.
#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) }
No parentheses around the condition, but braces are required.
while-style loop
A for with only a condition is Go's while.
#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++ }
An infinite loop is for { ... } — equivalent to C's while(1) { ... }.
range — iterating collections
range yields (index, value) pairs for slices, (key, value) for maps, and (index, rune) for strings — no length variable, no off-by-one risk.
#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) }
Use _ to discard either part: for _, value := range scores.
switch — no fallthrough by default
Go's switch does not fall through by default — no break needed, and no forgotten-break bugs.
#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") }
Use fallthrough explicitly when you want C's behavior. Cases can match multiple values: case 1, 2, 3:.
Functions
Multiple return values
Go functions return multiple values directly — no output-pointer parameters.
#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)
The same mechanism drives Go's idiomatic error handling: value, err := someFunc().
Variadic functions
Go variadic functions receive a typed, bounds-checked slice — not a raw va_list plus a count the caller must pass correctly.
#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))
Spread an existing slice into variadic arguments with sum(numbers...).
First-class functions
Go functions are first-class values, and anonymous functions are closures that capture variables from the enclosing scope.
#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))
C function pointers cannot close over local variables without a separate hand-passed context struct.
defer — cleanup without goto
defer schedules a function call to run when the enclosing function exits — normally or via panic — written right next to the acquisition it cleans up.
#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
It replaces the C pattern of duplicating cleanup code at every return and the goto cleanup idiom.
Structs & Methods
Struct definition
Go structs are close cousins of C structs — no classes, no inheritance.
#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)
Exported fields start with a capital letter (visible outside the package); unexported fields start lowercase.
Methods on structs
Go methods attach functions to types via a receiver — C's "pass the struct pointer as the first argument" convention, formalized in the language.
#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())
Value receivers (point Point) get a copy; pointer receivers (point *Point) can modify the original.
Struct embedding (composition)
Go embedding promotes the fields and methods of an embedded type to the outer struct — circle.X instead of circle.center.x.
#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)
This is Go's primary composition mechanism — not inheritance, and no polymorphism is implied.
Interfaces
Interfaces are satisfied implicitly
Go interfaces are satisfied implicitly — if a type has the required methods, it satisfies the interface with no declaration anywhere.
// 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"})
This structural typing eliminates the manual vtable wiring that C requires for polymorphism.
Any value: the empty interface
any (an alias for interface{}) accepts a value of any type — Go's counterpart to void*, but carrying runtime type information.
#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})
Use a type switch or type assertion to extract the concrete value safely; there is no blind cast.
Error Handling
Errors as return values
Go returns errors as ordinary values: result, err := fn() followed by if err != nil.
#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) }
The error travels with the result instead of through a global like errno, so it is explicit at every call site and much harder to overlook.
Error wrapping and inspection
fmt.Errorf("...: %w", err) wraps an error with context, and errors.Is unwraps the chain to find a target error.
// 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))
This gives structured error propagation that C's errno-based system entirely lacks.
Panic and recover
panic is Go's equivalent of abort — for truly unrecoverable situations — except that recover inside a defer can catch it.
#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)
Recovering converts the panic into an ordinary error, as this example does with the division by zero. The pattern is rare in Go; most error handling uses return values.
Goroutines & Channels
Goroutines vs threads
Goroutines are multiplexed onto OS threads by the Go runtime — starting one costs about 2 KB of stack, versus about 8 MB for a typical POSIX thread.
// 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()
A program can run millions of goroutines. The go keyword launches one with no thread-creation overhead, and sync.WaitGroup plays the role of pthread_join.
Channels for communication
Channels are typed communication pipes between goroutines — <- sends into or receives from one.
// 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)
Go's motto: "Don't communicate by sharing memory; share memory by communicating." Channels replace the mutex-plus-shared-state pattern that dominates C concurrent code.
Packages & Modules
Packages vs #include
Go packages are directory-based units of encapsulation, with visibility decided by capitalization: capital-letter names are exported, lowercase names are private to the package.
// 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")
This replaces C's convention of static for file-local visibility — and unlike a convention, the compiler enforces it.
Go modules vs Makefile
Go modules (go.mod) are the standard, built-in dependency system — no Makefile, CMake, or pkg-config required.
# 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")
Dependencies are versioned, checksummed, and cached; reproducible builds work out of the box.
Gotchas for C Programmers
Zero values: no garbage initialization
Every Go variable is initialized to its zero value — there is no uninitialized garbage, ever.
#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)
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
Go structs are value types — assignment copies the entire struct, exactly as in C.
#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
C programmers expect this; newcomers from OOP languages often don't. Pass a pointer *Point to avoid copying or to allow mutation.
GC pauses and latency
Watch out: the Go GC is a tradeoff — you give up C's deterministic memory timing in exchange for freedom from use-after-free and leaks.
// 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")
For most applications the sub-millisecond concurrent pauses are excellent. For hard real-time requirements, C or Rust remains the better choice.
No header files or forward declarations
Go has no header files and no forward declarations — the compiler reads all .go files in a package together, so definition order is irrelevant.
#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))
This eliminates the class of C bugs where a declaration and its definition disagree.