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 runsgo 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 ...
// #endifimport (
"fmt"
"strings"
"os"
)
fmt.Println(strings.ToUpper("hello, world"))
fmt.Println("GOOS:", os.Getenv("GOOS"))
// Unused imports are a compile error in GoUnused 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 rangeOut-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 automaticallyThe 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 occursIt 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) // 99C 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.