Encountering Go as a PHP developer

Last modified 2022/09/22 00:40

One morning I sat down at my laptop and looked at a Go service, written 2 months previously, that represented my first attempt with the language. When I wrote it I had been unsure about everything, questioning each line and frequently googling things like “how to do a foreach in Go”. In just two months I was now able to look at this code and immediately see several ways in which it could be improved. I now had opinions and felt confident.

I had been a PHP developer for around 14 years. At least 99.9% of the code I’ve written has been PHP, my coding experience and skill set has evolved through PHP and it’s community. At this stage in my career I consider myself to be a relatively good architect, I have a mature understanding of object oriented programming and it’s patterns.

So why did I start writing Go code?

I was the technical lead of a project at Inviqa and the project had a microservice architecture. I tended to imagine that microservices should be fast and self contained.

PHP was an option, libraries such as Amphp or ReactPhp allow you to write long running processes, but PHP is not designed with this use case and elsewhere in Inviqa we had already started using Go.

I knew I was taking a risk, our team didn’t have any Go experience, even if people in the wider company did. But I also wanted to use the best tool for the job. In the end we decided to write the services in Go and haven’t regretted it.

This blog post (originally written in January 2021) is an attempt to give my first impressions of the language and to map familiar concepts from PHP to Go.

Culture Shock

For 10 years the importance of descriptive naming has been impressed on me. Using $customer instead of $c for example, or register(Customer $customer) instead of save($c). So I was a bit shocked to see that short variable names are idiomatic in Go:

func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
	n := len(es)
	i := sort.Search(n, func(i int) bool {
		return len(es[i].pattern) < len(e.pattern)
	})
	if i == n {
		return append(es, e)
	}
	es = append(es, muxEntry{})
	copy(es[i+1:], es[i:])
	es[i] = e
	return es
}

Selected example from the Go HTTP package

My first impression was 🤢 and then I grew to embrace it, but then I would get confused and start using meaningful names again and my code was an inconistent mix of short and long names.

I guess the implication is that you write code that is succinct and that if you are confused it’s because your code is confusing and should be refactored. But this argument applies equally to PHP so the trade offs are the same.

This is perhaps one aspect of the general culture of minimalism in Go. The Go language itself is very minimal and doesn’t have many functions you would expect to find in other languages. This is both a strength and a weakness as you either writing everything verbosely, or create your own library for each microservice or you end up using any number of publically available packages, any one of which could be abandonned.

“I don’t think one letter variables are that popular as they make it seem to be. The idea is to have the more verbose variable name the longer it lives, so one or two liners can have one-letter variables indeed. Exceptions could be indexes in for loops, or reference to the struct we’re working with, which in PHP would be $this” - @SirRFI via. Symfony Slack

Read more:

Executing Code

Let’s start with a comparison of “Hello World”, in PHP:

<?php
// test.php

echo "Hello World";

We execute it as:

$ php test.php
Hello World

In Go:

// hello.go
package main

import (
	"fmt"
)

func main() {
    fmt.Print("Hello World")
}

It is executed with go run:

$ go run hello.go
Hello World

Here we use the standard fmt package which we’ll see more of shortly.

Var Dump and Die

When learning a new language the first thing I want to do is be able to var_dump, console.log or whatever it takes to know:

  • The my code is being executed.
  • The value of a variable, method call, etc.

In PHP I use var_dump (or posh variants). In Go the native equivalent is fmt.Printf("%#v", value):

package main

import (
    "fmt"
)

type Command struct {
    Name string;
    Args []string;
}

func main() {
    cmd := Command{
        Name: "greet",
        Args: []string{"Daniel"},
    }

    fmt.Printf("%#v", cmd) // print the value
}

This will print a representation of the value to STDOUT:

$ go run test.go
main.Command{Name:"greet", Args:[]string{"Daniel"}}

If you want to “var dump and die”:

// ...
panic(fmt.Sprintf("%#v", cmd))

Note that we use Sprintf (which is analagous to PHP sprintf). It returns a formatted string, while Printf will send it to STDOUT.

Recently I started using Spew which is to fmt.Sprintf("%v") what dump(...) is to var_dump(...);.

Composer vs. Go Modules

Go does not have a third-party “package” manager - this functionality is built in. What we call packages in PHP are known as modules in Go (packages are something else - see the following packages section).

To require a new module use go get

$ go get github.com/stretchr/testify

This will automatically add an entry to go.mod (which is like composer.json) and update go.sum (which is like composer.lock).

Notable differences:

  • There is no package registry: you reference the source code repository directly.
  • You can use two or more major versions of a package concurrently.

Read more about using go modules.

Namespaces vs. Packages

In PHP we organize code with namespaces. Go has packages.

Like namespaces, packages in Go help you to organize your code. Each directory within your Go repository can contain one package only.

Since the time of Composer we have adopted the PSR-0/4 autoloading conventions. As Go is a compiled language it does not need to “guess” where source files may be, so autoloading is not required and a package name need not correspond to it’s directory name.

The following source file in PHP:

<php
// src/Handler/InvoiceHandler.php

namespace MyProject\\Handler;

class InvoiceHandler {
}

Might look like this in Go:

// handler/invoice_handler.go
package handler

type InvoiceHandler struct {
}

The main package is special and we’ll talk about this next.

Main

In PHP you can execute any script:

<?php 

echo "Hello World!";

but the following will not work with Go:

package hello

import (
	"fmt"
)

fmt.Print("Hello World")
go run test.go                                                                                            ✘ 1 
go run: cannot run non-main package

Only the main package can be executed, let’s fix it:

package main

import (
	"fmt"
)

fmt.Print("Hello World")

But it still doesn’t work:

go run test.go                                                                                             2 
# command-line-arguments
./test.go:7:1: syntax error: non-declaration statement outside function body

Unlike PHP we cannot call functions wherever we like, they must be called from another function. You may have noticed in the previous examples that we always put our code in the main() function. The go run command will run the main function inside of the main package.

package main

import (
	"fmt"
)

func main() {
    fmt.Print("Hello World")
}

Read more:

Use vs. Import

In PHP we import classes, functions and constants using use:

<?php

use MyProject\Handler\PostHandler;
use function Amp\call;
use const MyProject\FOOBAR;

We can either import the fully-qualified name and use it (e.g. echo FOOBAR) or we use it relative to an imported namespace: use MyProject; and echo MyProject\\FOOBAR.

In Go we import packages:

import (
	"github.com/imdario/mergo"
)

func main() {
    mergo.Merge(/** ... */)
}

Above we call the function Merge from the mergo package. Note that we do not import specific definitions, we can only import packages.

Like PHP you can also alias imports:

import (
	m "github.com/imdario/mergo"
)

func main() {
    m.Merge(/** ... */)
}

You can also import packages into the current namepsace using the . alias and avoid referencing the package when using it’s definitions, but this probably isn’t a great idea due to the potential for conflicts.

Read more:

Classes vs. Structs

Go is not an object-oriented language but a huge amount of knowledge from that domain can be transferred to Go. The concept of class can be roughly mapped to struct. Unlike classes structs are simply data structures, but they are data structures to which you can associate methods as we will see later.

This PHP class:

<?php

class Pet {
    public string $name;
    public string $species;
    public int $age;
}

could be represented in Go as:

type Pet struct {
    Name string;
    Species string;
    Age int;
}

Note that capitalisation of the fields - in Go capitalisation is used to determine the (package level) visiblity of the fields, see visiblity

In PHP methods are defined within the class definition. In Go they are attached outside of the struct definition (more on this in the methods section).

Structs can be “instantiated”:

pet := Pet{
    Name: "Thor",
    Species: "Hamster",
    Age: 5,
}

In PHP we have constructors (public function __construct(string $one, string $two) {}) which enable us to use the provided constructor arguments in any way we choose (normally we assign them to properties).

Structs do not have this mechanism. There are no constructors in Go. Instead it is typical to create “constructor functions”:

func NewPet(string name, string species, age int) Pet {
    return Pet{
        Name: name,
        Species: species,
        Age: age,
    }
}

// and use it as follows
fmt.Printf("%#v", NewPet("Thor", "Hamster", 5))

Read more:

Methods

As previously mentioned a struct loosely corresponds to a class. A struct can have methods associated with it, but unlike PHP, you can associate methods on any type as long as it is defined in the same package.

In PHP a class may look like this:

<?php

class Pet {
    private bool $vaccinated = false;

    public function isVaccinated(): void {
        return $this->vaccinated;
    }

    public function vaccinate(): void {
        $this->vaccinated = true;
    }
}

In Go this might look something like:

type Pet struct {
    vaccinated bool;
}

func (p Pet) IsVaccinated() bool {
    return p.vaccinated
}

func (p *Pet) Vaccinate() {
    p.vaccinated = true
}

Notice that the first method is defined with a reciever (p Pet). This reciever indicates to which type the method should be bound. The reciever name maps to the concept of $this in PHP.

The second method uses a pointer reciever, we know it’s a pointer because the type is prefixed with *. This effectively means that we can mutate the fields of the struct to which the method is attached - more on this later

If your method only needs to read the field, then it makes sense to use a value receiver. The first method uses a value reciever: (p Pet).

The public (see visiblity) Vaccinate method can be called as follows:

pet := Pet{}
pet.Vaccinate()

we did not give vaccinated a default value. In PHP we’d expect the value to be NULL (or more recently for an uninitialized property to cause an error) but in Go types are not nullable, instead they assume the “empty” value, in the case of bool that value is false. See null vs. nil for more information.

Read more:

Interfaces

Go has interfaces, but unlike PHP they are not explicitly implemented (there is no implements keyword). Rather they define the “methods” that a struct needs in order to be accepted as an argument.

type user interface {
	name() string
}

Above we define a user interface (with a lowercase u meaning it’s private and available only to the current package). We can depend on an interface:

func hello(u user) {
    fmt.Printf("Hello %s", u.name())
}

This enables any struct to be passed as long as it exactly implements the name method.

Compared to PHP this is arguably more flexible. Each package can define what it needs and it doesn’t care how you supply them. On the other hand it makes refactoring more difficult, as implementations don’t know they are implementations until they are used.

interface{} is also a type which can be used to indicate “any value” (similar to mixed in PHP). We will see more about this later.

since Go 1.18 you can also use any to indicate any value (it’s an alias to interface{})

Read more:

Visiblity

In PHP, class-member visiblity is determined by the private, protected and public keywords:

<?php

class Example {
    public int $one;
    protected int $two;
    private int $three;
}

In Go visiblity is NOT indicated by a keyword but by the case (🤯) of the first character of the field name:

type Example struct {
    One string;   // public
    three string; // private
}

Above One is public while three is private to the current package (the concept of protected does not exist as one struct cannot inherit from another).

Private fields are not private to the struct which defined them, but rather private to the entire package in which the struct is defined.

This rule applies to any definition:

// functions
func ThisFuncIsPublic() {
}
func thisFuncIsPrivate() {
}

// constants
const ThisConstantIsPublic = "yes"
const thisConstantIsPrivate = "yes"

var ThisIsPublicVariable = "yes"
var thisIsPrivateVariable = "yes"

So, unlike PHP, packages can conceal definitions from other packages and strictly expose public APIs.

NULL vs. Nil

The Go concept of Nil roughly corresponds to the null in PHP but differs in some important aspects.

Nil represents the “empty” value of various, but not all, types.

Unlike PHP there is no concept of “nullability” and there are no union types. If you declare a field to be string then it has to be a string, if no value is specified the “empty” value will be used (in this case an empty string).

While a string value cannot be Nil, a pointer to a string value can be Nil. This is because the default value of a pointer value is Nil.

There are various different types of Nil as they differ based on the type they are the empty value for: Nil for a pointer cannot be compared with Nil for a map for example.

In PHP we can have a nullable string type:

<?php

class Foo {
    public ?string $bar = null;
}
var_dump(new Foo()); // bar is NULL

But in Go a string value can only be a string:

type Foo struct {
    Bar string
}
fmt.Printf("%#v", Foo{}) // Bar is empty string

Unless it is a pointer, in which case it can also be Nil as all pointers can be Nil - but of course then you have to be careful to avoid NULL pointer errors!

If you are using a relational database you will likely soon encounter the joy of mapping nullable database fields to structs in Go

Read more:

Errors and Exceptions

In PHP we would typically handle an error by throwing an exception, the exeption will bubble up through the call stack and can be handled with a try/catch.

In Go the concept of exceptions maps to the concept of panic/recover but it is common to explicitly return errors:

func GetUser(name string) (User, error) {
    if name == "Alice" {
         return User{Name: "Alice"}, nil
    }

    return User{}, errors.New("Name must be 'Alice'")
}

Note that we return both a value and an error type. If the happy path is followed (the name is “Alice”) then we return a populated User struct and nil as the error, in the error case we return an empty User and a non-nil error.

The call is handled as follows:

something, err := GetUser("Bob")

if err != nil {
    // handle the error
}

// do something with "something"

Explicitly returning an error clearly indicates to the consumer of the library that an error can occur and is expected. It forces the consumer to consider the handling of the error.

The alternate way of handling an error is to panic. Panic maps roughly to the concept of “Exception”. It bubbles-up through the call stack and can be handled via. the recover mechanism. Panic should generally be used only when an unexpected error occcurs. If you have an error that must be handled further up the call stack, it’s idiomatic and wise to return an error, if you have an error that nothing can handle (e.g. MySQL connection has gone away) then panicing is fine.

There seems to be a general misconception that panicing will crash your entire application, but in reality panics can be “caught” and handled in much the same way exceptions can be. The Go HTTP server will handle panics from handlers and return a 500 (not crashing the entire server).

Read more:

Pointers vs. Pass-By-Reference

In PHP objects are always passed by reference. If you pass an object to a function and modify that object not only in the local scope, but in all other scopes where that object is referenced - because it’s an alias to the same object!

In contrast non-object values in PHP are passed by value by default, and you can pass by reference by type hinting the parameter with & (e.g. function foobar(&$array) { $array['bar'] = 'baz'; }).

Go has the concept of pointers which are different to references in that they actually reference the memory location where a value is stored.

In the following code example we set the name on a User struct, but it does not work you might expect:

package main

import ("fmt")

type User struct {
    Name string;
}

func SetUserName(user User, name string) {
    user.Name = name
}


func main() {
    user := User{}

    SetUserName(user, "Daniel")

    fmt.Printf("%#v", user.Name) // returns empty string
}

The user struct is “copied” to the function, which means the function modifies a different “instance” of the struct. If we use a pointer in the SetUserName function it works on the same “instance”:

package main

import ("fmt")

type User struct {
    Name string;
}

func SetUserName(user *User, name string) {
    user.Name = name
}


func main() {
    user := User{}

    SetUserName(&user, "Daniel")

    fmt.Printf("%v", user.Name) // prints "Daniel"
}

Unlike PHP Go does not change it’s behavior based on the type of value that is passed. If you pass a struct as a parameter to a function it is passed by value by default (i.e. it is copied).

Testing

Go has a built-in test runner based on a convention: any file ending with _test.go will be treated as test. It is conventional to place test files “next” to the files they test:

handler/
    invoice.go
    invoice_test.go

We mentioned that it is a test runner. Unlike PHPUnit it has no built-in support for assertions, instead you can use conditionals:

package hello

import (
    "testing"
)

func TestHello(t *testing.T) {
    expected := 3
    result   := AddOne(2)

    if result != expected {
        t.Fatalf(`Expected "%d", got "%d"`, expected, result)
    }
}

I personally prefer to use an assertion library such as testify in which case the test can be written as:

func TestHello(t *testing.T) {
    expected := 3
    result   := AddOne(2)

    require.IsEqual(t, 3, 2)
}

You can run tests with:

$ go test ./handler/invoice_test.go`

Or run all tests using:

$ go test ./...

The output is minimal. You can use a third-party tool such as gotestsum to achieve more colorful results.

Collections and Arrays

In PHP we have the array type which can be either a list or a dictionary. In Go we have distinct types for lists (as arrays and slices) and maps:

For example:

$foo = [1, 2, 3];
$bar = ['one' => 1, 'two' => 2];

could be represented in Go as:

foo := []int{1,2,3} // dynamically sized slice
bar := map[string]int{"one": 1, "two": 2} // map

note that arrays are fixed size lists, e.g. [3]int is an “list” of 3 ints

One of the features of Go’s type system is type aliasing and being able to associate methods to structs any type which is defined in the same package, in the following example we define UserCollection as an alias of a “slice” of users:

type User struct {
    Name string;
}
type UserCollection []User

Now we now add methods to this “collection”:

func (c UserCollection) ByName() UserCollection {
    // filter by user name and return a new collection
    // return c
}

Read more:

Foreach and loop scope

In PHP we iterate over arrays and iterables with the foreach loop:

foreach ($items as $key => $value) {
}

In Go this translates to:

for key, value := range items {
}

One nice feature of Go’s variable scoping is that the variables key and value above are only available within the scope of the for loop, they do not pollute the subsequent code.

Summary

Above I’ve outlined some of the concepts which I was able to map from PHP. I am an aspiring Gopher and am capable of being productive and applying most of my existing progamming knowledge to Go.

If you haven’t already it is strongly encouraged to do the Go Tour to fill in the blanks. In fact you should have done that before reading this blog post.

Better learning resources: