Go vs Erlang
I initially learnt concurrency-oriented programming from Erlang creator Joe Armstrong’s book Programming Erlang, so am learning Go by translating Joe’s Erlang examples, and these are my notes.
-module(hello).
-export([start/0]).
start() -> io:format("Hello world~n").
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello world")
}
The go mantra don’t communicate by sharing memory; share memory by communicating very much echoes Joe Armstrong’s philosophy: “Erlang has no mutexes, no synchronized methods, and none of the paraphernalia of shared memory programming. Processes interact by one method, and one method only, be exchanging messages. Processes share no data with other processes.”
How to structure messages
I want to keep my messaging independent of programing languages by using
JSON, which unfortunately neither
Erlang nor Go make simple. In his book, Armstrong included a section titled The JSON bridge
introducing build-in functions maps:to_json(Map) -> Bin
and maps:from_json(Bin) -> Map
which
unfortunatly don’t appear to have ever been included in the standard distribution, requiring
third party libraries.
Messages in Erlang tend to be written using curly-bracketed tuples, which are akin to JSON square-bracketed arrays. Erlang’s OTP framework includes conventions for writing these, essentially creating two types of messages: call which expects a response, and cast which doesn’t.
I posted notes on an exercise I did to rewrite the basics of OTP as part of an online course I did, with my own attempts to replicate the listening loop and call and cast.
If the loop receives a message structured {call, From, Ref, Request}
it returns to sender
{reply, Ref, Reply}
. If it receives {cast, Request}
, it doesn’t return anything.
JSON-RPC encourages this request format for what OTP calls a call:
{
"jsonrpc": "2.0",
"method": "funcname",
"params": ["Arg1", "Arg2", 3],
"id": 1
}
What OTP refers to as cast in JSON-RPC jargon is a notification and is done by not including an id key and value.
{
"jsonrpc": "2.0",
"method": "funcname",
"params": ["Arg1", "Arg2", 3]
}
If there’s no problem to a call, the response looks like:
{
"jsonrpc": "2.0",
"result": 42,
"id": 1
}
If there was a problem, the response looks like:
{
"jsonrpc": "2.0",
"error": {
"code": -32700,
"message": "Parse error",
"data": "missing dot"
},
"id": 1
}
The response includes either a result or an error, but not both. If the error is related to the id, a NULL valued id is returned.
Project Layout
One of the nice things about Erlang is its OTP framework which encourages a top-down approach to system design. Online book Adopting Erlang provides a nice overview.
In contrast to go where the executable is not just a compiler, but a complex tool covering commons tasks
such as go run .
, go fmt foo.go
, go mod init example/hello
etc, Erlang relies on third-party tools,
a popular choice being rebar3 which among other things creates project
layout directories with skeleton files.
There is a a guideline for go project layout
Something currently missing in the standard go toolset is a linter, for which I’m using revive.
systemd
A reason I’ve decided to switch from Erlang to Go is I just want an executable binary, and Go seems the quicker and simpler solution.
Running a Go binary as a systemd service
Erlang Modules vs Go Packages
The Erlang philosophy is programs are split into processes, and each process is associated with a module. A module is akin to a class in object-oriented programming, which could have several instances. So the same module could be running several times as different processes.
How to Write Go Code describes a go package as “a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.”
Something that initially confused me about go is there’s no equivalent of the -export([functor/arity]).
statement. This is thanks to the command go run .
assuming all *.go
files in the given directory are
part of the application and all capitalised variables in the various files are automatically exported.
This simplifies compilation, removing the need for make files or external tools such as Erlang’s rebar.