Go for PHP Developers

Moving to Go

You must rewrite PHP applications in Go from scratch using Go's static typing, structs, and concurrency primitives, as there is no direct migration path.

The shift from dynamic scripts to compiled structure

You have spent years gluing things together with PHP. You know the rhythm of $_POST, the flexibility of dynamic arrays, and the comfort of a language that rarely stops you from making a typo until runtime. You write a script, upload it, and the server interprets it on the fly. Now you are looking at Go. The syntax feels familiar. Curly braces, optional semicolons, if statements look like home. The mental model is different. Go does not let you wing it. It asks for structure before you run. This is not about translating line by line. It is about learning how Go thinks about data, errors, and execution. You are moving from a language that adapts to your code to a language that requires your code to adapt to its rules.

Explicit types and the compiler as a safety net

PHP embraces dynamism. Variables hold whatever you give them. An array can be a list, a map, or an object depending on the moment. You can add properties to an object at runtime. Go embraces explicitness. A variable has a type, and that type does not change. An array has a fixed size. A slice is a view into an array. A map is a hash table. You define the shape of your data with struct, and the compiler enforces it.

Go does not have classes or inheritance. You build behavior by composing structs and attaching methods to types. This feels restrictive at first. It becomes a superpower once you trust the compiler. The compiler is your safety net. It catches typos, type mismatches, and unused code before you ever deploy. Go compiles to a single binary. There is no interpreter. You run go build and get an executable. This means faster startup and no runtime dependencies on the server.

gofmt is the standard. Do not argue about indentation. Let the tool decide. Most editors run it on save. Your code will look like everyone else's code. This reduces cognitive load when reading other people's work.

Go does not hide complexity. It moves complexity to compile time so runtime is simple.

Defining data and iterating

In PHP, you might create an object with new stdClass or just use an associative array. Go requires you to define the shape first. The User struct declares that every user has an integer ID and a string Name. You cannot accidentally access a field that does not exist. If you try to access u.Email, the compiler rejects the program with u.Email undefined (type User has no field or method Email). This is different from PHP, where accessing an undefined property might return null or trigger a warning. Go stops you immediately.

The slice users holds a sequence of User values. You initialize it with a composite literal. The range keyword iterates over the slice. It returns the index and the value. The underscore _ discards the index. This is the idiomatic way to say you do not need the index. In PHP, you might use foreach ($users as $user). Go's range is similar but returns both index and value by default.

package main

import "fmt"

// User represents a person in the system.
type User struct {
	ID   int
	Name string
}

// main is the entry point for the program.
func main() {
	// users is a slice of User values.
	// Go infers the type from the literal.
	users := []User{
		{ID: 1, Name: "Alice"},
		{ID: 2, Name: "Bob"},
	}

	// range iterates over the slice.
	// The underscore discards the index value.
	for _, u := range users {
		fmt.Printf("User %d: %s\n", u.ID, u.Name)
	}
}

Handling errors as values

PHP uses exceptions or return codes. Go uses explicit error returns. Functions that can fail return an error as the last return value. The caller checks the error immediately. This is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. You cannot miss an error. In PHP, you might swallow an exception or check === false and forget. Go forces you to look at the error. If you ignore the error, the compiler complains with err declared and not used. You must handle it or discard it explicitly with _.

The receiver name is usually one or two letters matching the type. You will see (b *Buffer) Write(...) in the standard library, not (this *Buffer) or (self *Buffer). This keeps code concise and consistent.

package main

import (
	"errors"
	"fmt"
)

// User represents a person in the system.
type User struct {
	ID   int
	Name string
}

// FindUser looks up a user by ID.
// It returns the user and an error if not found.
func FindUser(id int) (User, error) {
	if id <= 0 {
		return User{}, errors.New("invalid user ID")
	}
	return User{ID: id, Name: "Alice"}, nil
}

func main() {
	// Call the function and capture both return values.
	user, err := FindUser(1)
	if err != nil {
		// Handle the error immediately.
		fmt.Println("Error:", err)
		return
	}

	// Use the user value.
	fmt.Printf("Found: %s\n", user.Name)
}

Errors are values. Treat them like data, not exceptions.

Common pitfalls for PHP developers

PHP developers often expect variables to be null until assigned. In Go, variables get a zero value. An int is 0. A string is "". A pointer or interface is nil. This prevents undefined variable warnings but can hide logic bugs if you assume a variable is empty when it is actually zero.

If you have a pointer to a struct and you try to access a field, Go panics. The runtime crashes with panic: runtime error: invalid memory address or nil pointer dereference. PHP might suppress this or return null. Go crashes the program. You must check for nil before dereferencing.

Forget to use a package and you get imported and not used. This keeps code clean. If you need to ignore a return value, use _. The underscore discards a value intentionally. result, _ := ... says you considered the second return value and chose to drop it. Use it sparingly with errors.

The compiler is your friend. Listen to it.

Choosing the right data structure

Use a struct when you need to group related data fields with a fixed shape. Use a slice when you need a dynamic list of items that can grow or shrink. Use a map when you need key-value lookups by string or integer keys. Use an interface when you want to define behavior without specifying the underlying type. Use a pointer when you need to modify a value in place or share large data without copying. Use plain values when the data is small and you want the compiler to manage memory automatically.

Pick the tool that matches the shape of your data.

Where to go next