PONY λ M2 Modula-2

PHP.CodeCompared.To/Go

An interactive executable cheatsheet comparing PHP and Go

PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM) Go 1.26.2
Output & Running
Hello, World
<?php echo "Hello, World!\n";
fmt.Println("Hello, World!")
Go is a compiled language with a main package and a main function as its entry point (the runner here adds that boilerplate for you). Output goes through the fmt package — fmt.Println writes its arguments and a trailing newline, so unlike PHP's echo you do not append \n. There is no <?php tag and no $ on variables.
Compiling vs Interpreting
<?php // PHP is interpreted — run the source directly: // php script.php $name = "World"; echo "Hello, {$name}!\n";
// Go compiles to a single static binary: // go build -o app then ./app // Or run without keeping the binary: // go run main.go name := "World" fmt.Printf("Hello, %s!\n", name)
Go is compiled ahead of time into a single self-contained native binary with no runtime to install — you ship one file, not a PHP interpreter plus source. The compiler is famously fast and strict: type errors, unused variables, and unused imports all stop the build. go run compiles and runs in one step for quick iteration.
Formatted Output
<?php $name = "Alice"; $age = 30; $ratio = 3.14159; printf("%s is %d\n", $name, $age); printf("%.2f\n", $ratio); var_dump([1, 2, 3]);
name := "Alice" age := 30 ratio := 3.14159 fmt.Printf("%s is %d\n", name, age) fmt.Printf("%.2f\n", ratio) fmt.Printf("%v\n", []int{1, 2, 3}) // %v = default format fmt.Printf("%T\n", ratio) // %T = the type: float64
Go's fmt.Printf uses the same C-style verbs PHP borrowed — %s, %d, %.2f — plus two you will use constantly: %v prints any value in a sensible default form (the everyday var_dump), and %T prints the value's static type. There is no string interpolation, so Printf or string concatenation is how you build output.
Variables & Types
Declaring Variables
<?php $name = "Alice"; // dynamically typed $age = 30; $age = "thirty"; // legal — type can change echo "{$name}: {$age}\n";
name := "Alice" // type inferred as string, fixed forever age := 30 // inferred as int // age = "thirty" // COMPILE ERROR — int can't hold a string fmt.Printf("%s: %d\n", name, age)
The := short declaration creates a variable and infers its type from the value — concise like PHP, but the type is then fixed permanently. There is no $ sigil. := only works inside functions and only for new names; at package level, or to declare without an initial value, you use var name string instead.
Static Types
<?php $count = 42; $ratio = 3.14; $text = "hello"; $flag = true; echo gettype($count) . "\n"; // integer echo gettype($ratio) . "\n"; // double
count := 42 // int ratio := 3.14 // float64 text := "hello" // string flag := true // bool fmt.Printf("%T\n", count) // int fmt.Printf("%T\n", ratio) // float64 fmt.Printf("%T\n", text) // string fmt.Printf("%T\n", flag) // bool
Go is statically typed: every variable has a type known at compile time, which you inspect with the %T verb rather than a runtime gettype. The defaults are int (machine word size), float64 for decimals, string, and bool. Sized variants (int32, uint8, float32) exist for when you need exact widths.
Zero Values, Not null
<?php // PHP: an undeclared/unset variable is null. $name; $count; var_dump($name ?? "unset"); // "unset" var_dump(isset($count)); // false
// Go: every type has a ZERO VALUE — never an unexpected null. var name string // "" var count int // 0 var active bool // false var items []int // nil slice (usable, len 0) fmt.Printf("%q %d %v %v\n", name, count, active, items)
Go has no null for value types. A declared-but-unassigned variable holds its type's zero value: "" for strings, 0 for numbers, false for bools, nil for pointers, slices, and maps. This eliminates "undefined variable" surprises — a variable always has a usable, well-defined value the moment it exists.
Explicit Conversion
<?php $total = 7; $count = 2; echo $total / $count . "\n"; // 3.5 — auto-promotes $asFloat = (float) $total; echo $asFloat / $count . "\n"; // 3.5
total := 7 count := 2 fmt.Println(total / count) // 3 — int division fmt.Println(float64(total) / float64(count)) // 3.5 // Mixing types without conversion is a compile error: // var x float64 = total // ERROR: int is not float64 fmt.Println(float64(total))
Go never converts between types implicitly — not even int to float64. You convert with explicit T(value) syntax (float64(total)), and mixing types in an expression without it is a compile error. This is stricter than PHP's automatic juggling, and it means int / int truncates to an int just like in C.
Strings
Strings Are Immutable Bytes
<?php $greeting = "Hello"; echo strlen($greeting) . "\n"; // 5 echo $greeting[0] . "\n"; // H $greeting[0] = "J"; // PHP allows in-place edit echo $greeting . "\n"; // Jello
greeting := "Hello" fmt.Println(len(greeting)) // 5 (bytes) fmt.Println(greeting[0]) // 72 — a byte, not "H" fmt.Printf("%c\n", greeting[0]) // H // greeting[0] = 'J' // COMPILE ERROR — strings are immutable greeting = "J" + greeting[1:] // rebuild instead fmt.Println(greeting) // Jello
A Go string is an immutable read-only sequence of bytes, so you cannot edit it in place as PHP lets you — you build a new string instead. Indexing yields a byte (a number), not a one-character string, so greeting[0] prints 72 unless you format it with %c. len counts bytes, which matters for multi-byte text (next).
Bytes vs Runes
<?php $text = "café"; echo strlen($text) . "\n"; // 5 bytes (é is 2 bytes) echo mb_strlen($text) . "\n"; // 4 characters echo mb_substr($text, 3, 1) . "\n"; // é
text := "café" fmt.Println(len(text)) // 5 bytes fmt.Println(utf8.RuneCountInString(text)) // 4 runes (characters) for index, char := range text { fmt.Printf("%d:%c ", index, char) // ranges by rune } fmt.Println()
Go strings are UTF-8 by default, and it distinguishes bytes from runes (Unicode code points). len counts bytes; utf8.RuneCountInString counts characters, the equivalent of mb_strlen. Ranging over a string with for ... range walks rune by rune, giving you the byte offset and the decoded character — no mb_ family of functions needed.
String Functions
<?php $text = "Hello, World"; echo strtoupper($text) . "\n"; // HELLO, WORLD echo str_replace("World", "Go", $text) . "\n"; var_dump(str_contains($text, "World")); // true echo implode("-", ["a", "b", "c"]) . "\n"; // a-b-c
text := "Hello, World" fmt.Println(strings.ToUpper(text)) fmt.Println(strings.ReplaceAll(text, "World", "Go")) fmt.Println(strings.Contains(text, "World")) // true fmt.Println(strings.Join([]string{"a", "b", "c"}, "-")) // a-b-c
Go gathers string helpers into the strings package, so PHP's global strtoupper/str_replace/str_contains become strings.ToUpper/strings.ReplaceAll/strings.Contains. They are regular functions that take the string as the first argument (Go has no string methods). strings.Builder is the efficient way to assemble a string from many pieces.
Slices & Maps
Lists: Slices
<?php $fruits = ["apple", "banana", "cherry"]; echo $fruits[0] . "\n"; echo count($fruits) . "\n"; $fruits[] = "date"; // append print_r($fruits);
fruits := []string{"apple", "banana", "cherry"} fmt.Println(fruits[0]) fmt.Println(len(fruits)) fruits = append(fruits, "date") // append returns a new slice fmt.Println(fruits)
Go's growable list is a slice — written []string{...}, with a fixed element type. You grow it with the built-in append, which returns a (possibly relocated) slice you must assign back: fruits = append(fruits, ...). Every element shares one type, so a slice cannot hold a mix of strings and ints the way a PHP array can.
Maps
<?php $ages = ["Alice" => 30, "Bob" => 25]; echo $ages["Alice"] . "\n"; $ages["Carol"] = 35; if (isset($ages["Dave"])) { echo "has Dave\n"; } else { echo "no Dave\n"; }
ages := map[string]int{"Alice": 30, "Bob": 25} fmt.Println(ages["Alice"]) ages["Carol"] = 35 // The "comma ok" idiom tests presence: if _, ok := ages["Dave"]; ok { fmt.Println("has Dave") } else { fmt.Println("no Dave") }
Unlike C, Go has a real built-in map — but it is a separate type from a slice, with the key and value types in its declaration (map[string]int). Reading a missing key returns the value's zero value, so to distinguish "absent" from "zero" you use the comma-ok idiom value, ok := ages[key], where ok is a bool. Map iteration order is randomized on purpose.
Iterating with range
<?php $numbers = [10, 20, 30]; foreach ($numbers as $index => $value) { echo "{$index}: {$value}\n"; }
numbers := []int{10, 20, 30} for index, value := range numbers { fmt.Printf("%d: %d\n", index, value) } // Ignore the index with the blank identifier _: for _, value := range numbers { fmt.Println(value * 10) }
Go's for ... range is the equivalent of foreach ($x as $k => $v), yielding the index/key and value for slices, maps, and strings. When you do not need the index you must explicitly discard it with the blank identifier _ — Go refuses to compile an unused variable, so you cannot just leave it named.
No Built-in map / filter
<?php $numbers = [1, 2, 3, 4, 5]; $doubled = array_map(fn($n) => $n * 2, $numbers); $evens = array_filter($numbers, fn($n) => $n % 2 === 0); print_r($doubled); print_r(array_values($evens));
numbers := []int{1, 2, 3, 4, 5} doubled := make([]int, 0, len(numbers)) for _, n := range numbers { doubled = append(doubled, n*2) } evens := make([]int, 0) for _, n := range numbers { if n%2 == 0 { evens = append(evens, n) } } fmt.Println(doubled, evens)
Until recently Go had no map/filter/reduce — the idiomatic way is an explicit for loop with append, which Go programmers consider clearer than chained callbacks. (Go 1.21+ adds a slices package, and generics make custom helpers possible, but the loop remains the everyday norm.) Pre-allocating with make([]int, 0, len) avoids repeated reallocation.
Control Flow
Conditionals
<?php $age = 20; if ($age >= 18) { echo "adult\n"; } else { echo "minor\n"; }
age := 20 if age >= 18 { fmt.Println("adult") } else { fmt.Println("minor") } // if can carry an init statement, scoped to the if: if doubled := age * 2; doubled > 30 { fmt.Println(doubled) }
Go drops the parentheses around the condition but requires the braces, even for a one-line body. A distinctive feature: an if may begin with a short init statement (if x := f(); x > 0) whose variable is scoped to the if/else — perfect for the comma-ok and error-check patterns. There is no elseif keyword; you write else if.
switch (No Fall-through)
<?php $status = 2; $label = match ($status) { 1 => "active", 2, 3 => "pending", default => "unknown", }; echo $label . "\n";
status := 2 var label string switch status { case 1: label = "active" case 2, 3: // comma-separated values label = "pending" default: label = "unknown" } fmt.Println(label)
Go's switch is much closer to PHP 8's match than to C's: each case does not fall through, so no break is needed, and a case can list several values (case 2, 3). Unlike match it is a statement, not an expression, so you assign a result inside it. A bare switch {} with boolean cases is Go's clean replacement for a long if/else if chain.
for Is the Only Loop
<?php for ($i = 0; $i < 3; $i++) { echo $i . "\n"; } $count = 0; while ($count < 2) { echo "tick\n"; $count++; }
for i := 0; i < 3; i++ { fmt.Println(i) } // No 'while' keyword — a for with only a condition IS the while: count := 0 for count < 2 { fmt.Println("tick") count++ }
Go has a single loop keyword, for, which covers every case. The three-clause form is the classic counter; a one-clause form (for condition) is the while loop; and a bare for {} loops forever until you break. There is no separate while or do/while — one construct does it all.
No Ternary Operator
<?php $age = 20; $label = $age >= 18 ? "adult" : "minor"; echo $label . "\n";
age := 20 // Go has NO ternary operator. Use a full if/else: var label string if age >= 18 { label = "adult" } else { label = "minor" } fmt.Println(label)
Go deliberately omits the ? : ternary operator — the designers considered it a readability hazard. You write an explicit if/else, declaring the variable first with var and assigning in each branch. It is more verbose than PHP's one-liner, but it is the only way, and it keeps every conditional in the same uniform shape.
Functions
Defining Functions
<?php function add(int $a, int $b): int { return $a + $b; } echo add(2, 3) . "\n";
func add(a, b int) int { return a + b } fmt.Println(add(2, 3))
A Go function uses func, and — unlike PHP — the type comes after each parameter name, with the return type after the parameter list. When consecutive parameters share a type you write it once (a, b int). Types are mandatory; there is no dynamic fallback. A function whose name starts with a capital letter is exported from its package (see Gotchas).
Multiple Return Values
<?php function divmod(int $a, int $b): array { return [intdiv($a, $b), $a % $b]; } [$quotient, $remainder] = divmod(17, 5); echo "{$quotient} r {$remainder}\n";
func divmod(a, b int) (int, int) { return a / b, a % b } quotient, remainder := divmod(17, 5) fmt.Printf("%d r %d\n", quotient, remainder)
Go functions can return several values natively — the signature lists the return types in parentheses, and the caller receives them as separate variables, no array packing/unpacking like PHP's [$q, $r] = .... This is not a niche feature: it is the foundation of Go's error handling, where functions return (result, error) as a pair.
Variadic Functions
<?php function sum(int ...$numbers): int { return array_sum($numbers); } echo sum(1, 2, 3, 4) . "\n"; // 10 $values = [5, 6, 7]; echo sum(...$values) . "\n"; // 18
func sum(numbers ...int) int { total := 0 for _, n := range numbers { total += n } return total } fmt.Println(sum(1, 2, 3, 4)) // 10 values := []int{5, 6, 7} fmt.Println(sum(values...)) // 18 — spread with trailing ...
Go's variadic parameter is numbers ...int (the ... follows the type), and inside the function it is an ordinary slice you range over. To pass an existing slice as the arguments you append ... after it at the call site — sum(values...) — mirroring PHP's sum(...$values) spread, just with the dots on the other side.
Closures
<?php $factor = 3; $scale = function ($n) use ($factor) { return $n * $factor; }; echo $scale(10) . "\n"; // 30
factor := 3 scale := func(n int) int { return n * factor // captures factor automatically } fmt.Println(scale(10)) // 30
Go closures capture surrounding variables automatically — there is no use ($factor) clause to maintain. A function literal func(n int) int { ... } is a first-class value you assign, pass, and return. It closes over the live variable, so later changes to factor are visible inside the closure.
defer
<?php function process(): void { echo "open resource\n"; try { echo "do work\n"; } finally { echo "close resource\n"; // cleanup } } process();
func process() { fmt.Println("open resource") defer fmt.Println("close resource") // runs when process returns fmt.Println("do work") } process()
Go's defer schedules a call to run when the surrounding function returns, no matter how — the cleanup half of PHP's try/finally, but written right next to the resource it pairs with. Deferred calls run in last-in-first-out order, making defer file.Close() the idiomatic way to guarantee cleanup immediately after acquiring a resource.
Structs & Methods
Structs vs Classes
<?php class Point { public function __construct(public int $x, public int $y) {} } $point = new Point(3, 4); echo "{$point->x}, {$point->y}\n";
type Point struct { X int Y int } point := Point{X: 3, Y: 4} fmt.Printf("%d, %d\n", point.X, point.Y)
Go has no classes; a struct is a typed bundle of fields. You create one as a plain value with a composite literal (no new keyword needed), naming fields for clarity. Note the fields are capitalized — in Go, an uppercase initial letter makes a name exported (public) and lowercase makes it package-private; there is no public/private keyword.
Methods
<?php class Rectangle { public function __construct(public int $width, public int $height) {} public function area(): int { return $this->width * $this->height; } } $rect = new Rectangle(3, 4); echo $rect->area() . "\n"; // 12
type Rectangle struct { Width int Height int } func (r Rectangle) Area() int { return r.Width * r.Height } rect := Rectangle{Width: 3, Height: 4} fmt.Println(rect.Area()) // 12
A method is a function with a receiver declared before its name — func (r Rectangle) Area() attaches Area to Rectangle. The receiver r is Go's explicit version of PHP's implicit $this. Methods live outside the struct body, so a type's data and its behavior are declared separately, often even in different files.
Pointer Receivers
<?php class Account { public int $balance = 0; public function deposit(int $amount): void { $this->balance += $amount; // mutates the object } } $account = new Account(); $account->deposit(100); echo $account->balance . "\n"; // 100
type Account struct { Balance int } // A *pointer* receiver can mutate the struct. func (a *Account) Deposit(amount int) { a.Balance += amount } account := Account{} account.Deposit(100) // Go takes the address automatically fmt.Println(account.Balance) // 100
Because Go passes everything by value, a method that modifies its struct must use a pointer receiver (*Account) — a value receiver would mutate a copy and lose the change. This is the explicit machinery behind PHP's automatic object-mutation. Conveniently, Go takes the address for you when you call account.Deposit() on an addressable value.
Composition over Inheritance
<?php class Animal { public function __construct(public string $name) {} public function describe(): string { return "I am {$this->name}"; } } class Dog extends Animal { public function speak(): string { return "Woof"; } } $dog = new Dog("Rex"); echo $dog->describe() . " — " . $dog->speak() . "\n";
type Animal struct { Name string } func (a Animal) Describe() string { return "I am " + a.Name } type Dog struct { Animal // embedded — Dog gets Describe() promoted } func (d Dog) Speak() string { return "Woof" } dog := Dog{Animal: Animal{Name: "Rex"}} fmt.Println(dog.Describe() + " — " + dog.Speak())
Go has no inheritance. Instead you embed one struct in another: writing Animal with no field name inside Dog promotes Animal's fields and methods so dog.Describe() works directly. This is composition, not an is-a hierarchy — there is no overriding and no parent::, just a has-a relationship that forwards calls.
Constructor Functions
<?php class User { public function __construct( public string $name, public bool $active = true, ) {} } $user = new User("Alice"); var_dump($user->active); // true
type User struct { Name string Active bool } // Convention: a NewX function acts as the constructor. func NewUser(name string) User { return User{Name: name, Active: true} // set defaults here } user := NewUser("Alice") fmt.Println(user.Active) // true
Go has no constructors and no default field values — a struct literal that omits a field gets that field's zero value. The idiom is a package-level NewX factory function that builds and returns a fully initialized value, which is where you apply defaults and validation. Calling code uses NewUser("Alice") rather than a new keyword.
Interfaces
Implicit Interfaces
<?php interface Speaker { public function speak(): string; } // PHP requires "implements Speaker" explicitly: class Dog implements Speaker { public function speak(): string { return "Woof"; } } function announce(Speaker $s): void { echo $s->speak() . "\n"; } announce(new Dog());
type Speaker interface { Speak() string } type Dog struct{} func (d Dog) Speak() string { return "Woof" } // No "implements" — Dog satisfies Speaker just by having Speak(). func announce(s Speaker) { fmt.Println(s.Speak()) } announce(Dog{})
Go interfaces are satisfied implicitly: any type that has the required methods is a Speaker, with no implements declaration. This structural typing decouples implementations from interfaces — you can define an interface a third-party type already satisfies. It is duck typing checked at compile time: "if it has Speak(), it speaks."
any & Type Switch
<?php function describe(mixed $value): string { return match (true) { is_int($value) => "int: {$value}", is_string($value) => "string: {$value}", is_bool($value) => "bool", default => "other", }; } echo describe(42) . "\n"; echo describe("hi") . "\n";
func describe(value any) string { switch v := value.(type) { case int: return fmt.Sprintf("int: %d", v) case string: return fmt.Sprintf("string: %s", v) case bool: return "bool" default: return "other" } } fmt.Println(describe(42)) fmt.Println(describe("hi"))
any (an alias for the empty interface interface{}) is Go's mixed — it holds a value of any type. To act on what is inside you use a type switch, switch v := value.(type), where each case binds v to the concrete type. This is the type-safe replacement for PHP's is_int/is_string ladder, recovering the static type in each branch.
Stringer
<?php class Money { public function __construct(private int $cents) {} public function __toString(): string { return "$" . number_format($this->cents / 100, 2); } } echo new Money(1599) . "\n"; // $15.99
type Money struct { Cents int } // Implement String() and fmt prints it automatically. func (m Money) String() string { return fmt.Sprintf("$%.2f", float64(m.Cents)/100) } fmt.Println(Money{Cents: 1599}) // $15.99
Go's fmt.Stringer interface — a single String() string method — is the counterpart to PHP's __toString. Implement it and the fmt package calls it automatically wherever the value is printed. It is satisfied implicitly, like every Go interface, so you simply add the method and printing "just works".
Error Handling
Errors Are Values
<?php function parsePort(string $text): int { if (!ctype_digit($text)) { throw new InvalidArgumentException("bad port: {$text}"); } return (int) $text; } try { echo parsePort("abc") . "\n"; } catch (InvalidArgumentException $error) { echo "Error: {$error->getMessage()}\n"; }
func parsePort(text string) (int, error) { n, err := strconv.Atoi(text) if err != nil { return 0, fmt.Errorf("bad port: %s", text) } return n, nil } if port, err := parsePort("abc"); err != nil { fmt.Println("Error:", err) } else { fmt.Println(port) }
Go has no exceptions for ordinary failures. A function that can fail returns an error as its last value, and the caller checks if err != nil right at the call site. The error is a plain value you can inspect, wrap, or pass on — there is no stack-unwinding throw. This explicit, repetitive checking is Go's defining trade-off: nothing fails silently.
Custom & Wrapped Errors
<?php class PaymentError extends \RuntimeException {} function charge(): void { throw new PaymentError("card declined"); } try { charge(); } catch (PaymentError $error) { echo "Caught: {$error->getMessage()}\n"; }
var ErrDeclined = errors.New("card declined") func charge() error { return fmt.Errorf("charge failed: %w", ErrDeclined) // %w wraps } err := charge() if errors.Is(err, ErrDeclined) { // unwraps to compare fmt.Println("Caught:", err) }
A sentinel error is just a package-level value made with errors.New. To add context while preserving the original you wrap it with fmt.Errorf("...: %w", err), then test the chain with errors.Is (compare to a sentinel) or errors.As (extract a typed error). This wrapping replaces PHP's exception hierarchy and catch (SpecificType) with composable values.
panic & recover
<?php // PHP throws and catches for exceptional conditions. function risky(): void { throw new RuntimeException("something broke"); } try { risky(); } catch (Throwable $error) { echo "Recovered: {$error->getMessage()}\n"; }
func risky() { panic("something broke") } func safe() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) } }() risky() } safe()
Go does have panic and recover, which resemble throw/catch — but they are reserved for truly unrecoverable situations (programmer bugs, impossible states), not routine error handling. recover only works inside a deferred function. Idiomatic Go returns error values for anything a caller might reasonably handle, and panics rarely.
Concurrency
Goroutines
<?php // PHP is synchronous — tasks run one after another. function task(string $name): void { echo "{$name} done\n"; } task("A"); task("B");
func task(name string, done chan<- bool) { fmt.Println(name + " done") done <- true } done := make(chan bool) go task("A", done) // 'go' starts a concurrent goroutine go task("B", done) <-done // wait for both to signal <-done
A goroutine is a function call prefixed with go — it runs concurrently on a lightweight, runtime-scheduled thread (you can launch hundreds of thousands). This is Go's signature feature and the opposite of PHP's strictly synchronous, per-request model. Because goroutines run independently, you need a way to coordinate them, which is what channels provide.
Channels
<?php // PHP has no built-in channels; you'd collect results sequentially. $results = []; foreach (["apple", "pear"] as $item) { $results[] = strlen($item); } echo array_sum($results) . "\n"; // 9
prices := make(chan int) for _, item := range []string{"apple", "pear"} { go func(name string) { prices <- len(name) // send the length into the channel }(item) } total := 0 total += <-prices // receive (blocks until a value arrives) total += <-prices fmt.Println(total) // 9
A channel is a typed pipe that goroutines use to communicate and synchronize: ch <- v sends, <-ch receives, and a receive blocks until a value is ready. Go's motto is "don't communicate by sharing memory; share memory by communicating" — channels let concurrent code hand off values safely instead of locking shared state. PHP has no equivalent in its core.
WaitGroup
<?php // PHP: a sequential loop finishes before the next line runs. $names = ["Alice", "Bob", "Carol"]; foreach ($names as $name) { echo "greeted {$name}\n"; } echo "all done\n";
names := []string{"Alice", "Bob", "Carol"} wg := sync.WaitGroup{} for _, name := range names { wg.Add(1) go func(who string) { defer wg.Done() fmt.Println("greeted " + who) }(name) } wg.Wait() // block until all goroutines call Done fmt.Println("all done")
When you fan out work to many goroutines and just need to wait for all of them, a sync.WaitGroup is the tool: Add counts the tasks, each goroutine calls Done (via defer) when finished, and Wait blocks until the count reaches zero. It replaces the implicit "the loop finished" guarantee PHP gives you for free with concurrent, explicit coordination.
Enums (iota)
Enums with iota
<?php enum Status: int { case Active = 0; case Pending = 1; case Closed = 2; } $status = Status::Pending; echo $status->value . "\n"; // 1
type Status int const ( Active Status = iota // 0 Pending // 1 Closed // 2 ) status := Pending fmt.Println(int(status)) // 1
Go has no enum keyword. The idiom is a named integer type plus a const block using iota — a counter that starts at 0 and increments on each line, so you list the names and the values fill in automatically. It is more type-safe than bare constants (a Status is distinct from an int) but, like C, it carries no string name by default.
Naming Enum Values
<?php enum Suit: string { case Hearts = "Hearts"; case Spades = "Spades"; } echo Suit::Hearts->name . "\n"; // Hearts
type Suit int const ( Hearts Suit = iota Spades ) func (s Suit) String() string { return []string{"Hearts", "Spades"}[s] } fmt.Println(Hearts) // Hearts — String() is called automatically
To give an iota enum a readable name, implement the String() method on the type — the same Stringer interface used for any value. Here a slice indexed by the enum maps each value to its label, and because fmt calls String() automatically, printing the enum shows "Hearts" instead of 0. PHP's backed enums bundle this name in for you; in Go you wire it up once.
Standard Library
JSON & Struct Tags
<?php $payload = ["name" => "Alice", "roles" => ["admin", "user"]]; echo json_encode($payload) . "\n"; $parsed = json_decode('{"name":"Bob"}', true); echo $parsed["name"] . "\n";
type User struct { Name string `json:"name"` Roles []string `json:"roles"` } user := User{Name: "Alice", Roles: []string{"admin", "user"}} data, _ := json.Marshal(user) fmt.Println(string(data)) // {"name":"Alice","roles":["admin","user"]} var parsed User json.Unmarshal([]byte(`{"name":"Bob"}`), &parsed) fmt.Println(parsed.Name) // Bob
Go marshals JSON to and from structs, not loose maps. json.Marshal turns a struct into bytes and json.Unmarshal fills one in (note the & — it writes through a pointer). The backtick struct tags (`json:"name"`) map struct fields to JSON keys, since exported Go fields are capitalized but JSON keys usually are not. This gives you typed, validated decoding instead of PHP's untyped associative array.
Sorting
<?php $numbers = [5, 2, 8, 1]; sort($numbers); // mutates in place print_r($numbers); $people = [["age" => 30], ["age" => 25]]; usort($people, fn($a, $b) => $a["age"] <=> $b["age"]); echo $people[0]["age"] . "\n"; // 25
numbers := []int{5, 2, 8, 1} sort.Ints(numbers) // mutates in place fmt.Println(numbers) // [1 2 5 8] type Person struct{ Age int } people := []Person{{Age: 30}, {Age: 25}} sort.Slice(people, func(i, j int) bool { return people[i].Age < people[j].Age // "less" comparator }) fmt.Println(people[0].Age) // 25
The sort package sorts in place like PHP's sort. For built-in types there are helpers (sort.Ints, sort.Strings); for anything custom, sort.Slice takes a less function that returns whether element i should come before j — a boolean, not a three-way spaceship as in usort. (Go 1.21+ adds the generic slices.Sort as a newer alternative.)
String Conversions
<?php $number = (int) "42"; $text = (string) 99; $float = (float) "3.5"; echo $number + 8 . "\n"; // 50 echo $text . "!\n"; // 99! echo $float * 2 . "\n"; // 7
number, _ := strconv.Atoi("42") text := strconv.Itoa(99) flt, _ := strconv.ParseFloat("3.5", 64) fmt.Println(number + 8) // 50 fmt.Println(text + "!") // 99! fmt.Println(flt * 2) // 7
Because Go never coerces between strings and numbers, conversions are explicit calls in the strconv package: Atoi (string→int), Itoa (int→string), and ParseFloat. The parsing functions return a value and an error — there is no silent (int) cast that turns "abc" into 0; a bad input gives you an error you are expected to check.
⚠ Gotchas for PHP Devs
Unused = Compile Error
<?php // PHP happily ignores unused variables and imports. $used = 5; $unused = 99; // no warning, no error echo $used . "\n";
// In Go, an unused variable or import is a COMPILE ERROR. // unused := 99 // "declared and not used" — build fails used := 5 fmt.Println(used) // Discard a value you must accept but won't use, with _: result, _ := strconv.Atoi("42") fmt.Println(result)
Go treats an unused local variable or an unused import as a hard compile error, not a warning — a deliberate choice to keep code clean. This surprises everyone coming from PHP. The escape hatch is the blank identifier _, which explicitly discards a value you are required to receive (like the error from a function) but do not intend to use.
Nil Map Panics on Write
<?php // PHP auto-creates an array when you write to an undeclared one. $counts = []; $counts["a"]++; // fine — starts at null→1 echo $counts["a"] . "\n"; // 1
// A nil map can be READ but panics if you WRITE to it. var counts map[string]int fmt.Println(counts["missing"]) // 0 — reading is safe // counts["a"]++ // PANIC: assignment to entry in nil map counts = make(map[string]int) // must initialize first counts["a"]++ fmt.Println(counts["a"]) // 1
A declared-but-uninitialized map is nil. Reading from it is safe (you get the zero value), but writing to a nil map panics at runtime — there is no auto-creation as in PHP, where writing to $counts["a"] springs the array into being. Always initialize a map with make(map[...]...) or a literal before writing to it.
Capitalization Is Visibility
<?php class Widget { public int $size = 10; // explicit visibility keywords private string $secret = "hidden"; public function getSecret(): string { return $this->secret; } } $widget = new Widget(); echo $widget->size . "\n";
type Widget struct { Size int // exported (public) — capital first letter secret string // unexported (package-private) — lowercase } func (w Widget) Secret() string { return w.secret } widget := Widget{Size: 10, secret: "hidden"} fmt.Println(widget.Size) fmt.Println(widget.Secret())
Go has no public/private keywords. Visibility is governed entirely by the first letter's case: a capitalized name (Size, Secret) is exported and visible to other packages, while a lowercase name (secret) is private to its own package. This applies to every identifier — types, functions, struct fields, methods — so naming is a deliberate API decision, not an afterthought.
Integer Division
<?php echo 7 / 2 . "\n"; // 3.5 — auto-float echo intdiv(7, 2) . "\n"; // 3 echo 7 % 3 . "\n"; // 1
fmt.Println(7 / 2) // 3 — int / int is int fmt.Println(float64(7) / 2) // 3.5 — convert first fmt.Println(7 % 3) // 1 fmt.Println(-7 / 2) // -3 — truncates toward zero
Like C and unlike PHP, dividing two ints in Go yields an int: 7 / 2 is 3, the remainder discarded. Convert an operand to float64 for a real quotient. There is no automatic promotion to floating point and no intdiv function — the plain / already is integer division when both sides are integers.