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.