A brief introduction to Go Programming Language
Define Go
Go is a recent language which sits neatly in the middle of the landscape, providing lots of good features and deliberately omitting many bad ones. It compiles fast, runs fast-ish, includes a runtime and garbage collection, has a simple static type system and dynamic interfaces, and an excellent standard library.
Go takes a strong position on features that can lead to confusion and bugs. It omits OOP idioms such as inheritance and polymorphism, in favor of composition and simple interfaces. It downplays exception handling in favor of explicit errors in return values. There is exactly one correct way to lay out Go code, enforced by the
gofmt
tool. And so on.Go is also a great language for writing concurrent programs: programs with many independently running parts. An obvious example is a webserver: Every request runs separately, but requests often need to share resources such as sessions, caches, or notification queues. This means skilled Go programmers need to deal with concurrent access to those resources.
In this introduction to programming in Go, we’ll build the service in small steps, making a mess along the way and then cleaning it up again. Along the way, we’ll encounter lots of cool Go features, including:
- Struct types and methods
- Unit tests and benchmarks
- Goroutines and channels
- Interfaces and dynamic typing
A Simple Fund
Let’s write some code to track our startup’s funding. The fund starts with a given balance, and money can only be withdrawn (we’ll figure out revenue later).
Go is deliberately not an object-oriented language: There are no classes, objects, or inheritance. Instead, we’ll declare a struct type called Fund
, with a simple function to create new fund structs, and two public methods.
fund.go
package funding
type Fund struct {
// balance is unexported (private), because it's lowercase
balance int
}
// A regular function returning a pointer to a fund
func NewFund(initialBalance int) *Fund {
// We can return a pointer to a new struct without worrying about
// whether it's on the stack or heap: Go figures that out for us.
return &Fund{
balance: initialBalance,
}
}
// Methods start with a *receiver*, in this case a Fund pointer
func (f *Fund) Balance() int {
return f.balance
}
func (f *Fund) Withdraw(amount int) {
f.balance -= amount
}
Testing with benchmarks
Next we need a way to test Fund
. Rather than writing a separate program, we’ll use Go’s testing package, which provides a framework for both unit tests and benchmarks.
Benchmarks are like unit tests, but include a loop which runs the same code many times (in our case, fund.Withdraw(1)
).
This allows the framework to time how long each iteration takes,
averaging out transient differences from disk seeks, cache misses,
process scheduling, and other unpredictable factors.
For now, our benchmark will just deposit some money and then withdraw it one dollar at a time.
package funding
import "testing"
func BenchmarkFund(b *testing.B) {
// Add as many dollars as we have iterations this run
fund := NewFund(b.N)
// Burn through them one at a time until they are all gone
for i := 0; i < b.N; i++ {
fund.Withdraw(1)
}
if fund.Balance() != 0 {
b.Error("Balance wasnt zero:", fund.Balance())
}
}
We ran two billion (!!) iterations, and the final check on the balance was correct.$ go test -bench . funding testing: warning: no tests to run PASS BenchmarkWithdrawals 2000000000 1.69 ns/op ok funding 3.576s
Concurrent Access
Goroutines are the basic building block for concurrency in the Go language. They are green threads
– lightweight threads managed by the Go runtime, not by the operating
system. Goroutines are spawned with the go
keyword, and always start with a function (or method call):
// Returns immediately, without waiting for `DoSomething()` to complete go DoSomething()Often, we want to spawn off a short one-time function with just a few lines of code. In this case we can use a closure instead of a function name:
go func() { // ... do stuff ... }() // Must be a function *call*, so remember the ()
Once all our goroutines are spawned, we need a way to wait for them to finish. We can just use the WaitGroup
type in Go’s standard library, which exists for this very purpose. We’ll create one (called “wg
”) and call wg.Add(1)
before spawning each worker, to keep track of how many there are. Then the workers will report back using wg.Done()
. Meanwhile in the main goroutine, we can just say wg.Wait()
to block until every worker has finished.
Inside the worker goroutines in our next example, we’ll use defer
to call wg.Done()
.
defer
takes a function (or method) call and runs it
immediately before the current function returns, after everything else
is done. This is handy for cleanup:
func() { resource.Lock() defer resource.Unlock() // Do stuff with resource }()
This way we can easily match the Unlock
with its Lock
, for readability. More importantly, a deferred function will run even if there is a panic in the main function (something that we might handle via try-finally in other languages).
Lastly, deferred functions will execute in the reverse order to which they were called, meaning we can do nested cleanup nicely (similar to the C idiom of nested goto
s and label
s, but much neater):
func() { db.Connect() defer db.Disconnect() // If Begin panics, only db.Disconnect() will execute transaction.Begin() defer transaction.Close() // From here on, transaction.Close() will run first, // and then db.Disconnect() // ... }()
Remember that goroutines are green threads – they’re managed by the Go runtime, not by the OS. The runtime schedules goroutines across however many OS threads it has available. At the time of writing this Go language tutorial, Go doesn’t try to guess how many OS threads it should use, and if we want more than one, we have to say so. Finally, the current runtime does not preempt goroutines – a goroutine will continue to run until it does something that suggests it’s ready for a break (like interacting with a channel).
Implement Server in Go
Channels are the basic communication mechanism between goroutines. Values are sent to the channel (with channel <- value
), and can be received on the other side (with value = <- channel
). Channels are “goroutine safe”, meaning that any number of goroutines can send to and receive from them at the same time.
By default, Go channels are unbuffered. This means that
sending a value to a channel will block until another goroutine is ready
to receive it immediately. Go also supports fixed buffer sizes for
channels (using make(chan someType, bufferSize)
). However, for normal use, this is usually a bad idea.
Imagine a webserver for our fund, where each request makes a withdrawal. When things are very busy, the FundServer
won’t be able to keep up, and requests trying to send to its command
channel will start to block and wait. At that point we can enforce a
maximum request count in the server, and return a sensible error code
(like a 503 Service Unavailable
) to clients over that limit. This is the best behavior possible when the server is overloaded.
Adding buffering to our channels would easily end up with long queues of unprocessed commands.
Like any server, we will have a main loop in which it waits for commands, and responds to each in turn. There’s one more detail we need to address here: The type of the commands.
In the next section below, we’ll be sending several different commands, each with its own struct type. We want the server’s Commands channel to accept any of them. We can use interfaces for this.
server.go
package funding
type FundServer struct {
Commands chan interface{}
fund Fund
}
func NewFundServer(initialBalance int) *FundServer {
server := &FundServer{
// make() creates builtins like channels, maps, and slices
Commands: make(chan interface{}),
fund: NewFund(initialBalance),
}
// Spawn off the server's main loop immediately
go server.loop()
return server
}
func (s *FundServer) loop() {
// The built-in "range" clause can iterate over channels,
// amongst other things
for command := range s.Commands {
// Handle the command
}
}
Now let’s add a couple of struct types for the commands:type WithdrawCommand struct {
Amount int
}
type BalanceCommand struct {
Response chan int
}
The WithdrawCommand
just contains the amount to withdraw. There’s no response. The BalanceCommand
does have a response, so it includes a channel to send it on. This
ensures that responses will always go to the right place, even if our
fund later decides to respond out-of-order.
Now we can write the server’s main loop:
func (s *FundServer) loop() {
for command := range s.Commands {
// command is just an interface{}, but we can check its real type
switch command.(type) {
case WithdrawCommand:
// And then use a "type assertion" to convert it
withdrawal := command.(WithdrawCommand)
s.fund.Withdraw(withdrawal.Amount)
case BalanceCommand:
getBalance := command.(BalanceCommand)
balance := s.fund.Balance()
getBalance.Response <- balance
default:
panic(fmt.Sprintf("Unrecognized command: %v", command))
}
}
}
Implement service in Go
A server is something you talk to. What’s a service? A service is something you talk to with an API. Instead of having client code work with the command channel directly, we’ll make the channel unexported (private) and wrap the available commands up in functions.
type FundServer struct {
commands chan interface{} // Lowercase name, unexported
// ...
}
func (s *FundServer) Balance() int {
responseChan := make(chan int)
s.commands <- BalanceCommand{ Response: responseChan }
return <- responseChan
}
func (s *FundServer) Withdraw(amount int) {
s.commands <- WithdrawCommand{ Amount: amount }
}
Now our benchmark can just say server.Withdraw(1)
and balance := server.Balance()
, and there’s less chance of accidentally sending it invalid commands or forgetting to read responses.
There’s still a lot of extra boilerplate for the commands, but we’ll come back to that later.
Transactions
Eventually, the money always runs out. Let’s agree that we’ll stop withdrawing when our fund is down to its last ten dollars, and spend that money on a communal pizza to celebrate or commiserate around. Our benchmark will reflect this:
// Spawn off the workers
for i := 0; i < WORKERS; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < dollarsPerFounder; i++ {
// Stop when we are down to pizza money
if server.Balance() <= 10 {
break
}
server.Withdraw(1)
}
}()
}
// ...
balance := server.Balance()
if balance != 10 {
b.Error("Balance wasnt ten dollars:", balance)
}
This time we really can predict the result.$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 --- FAIL: BenchmarkWithdrawals-4 fund_test.go:43: Balance wasnt ten dollars: 6 ok funding 0.009s
We’re back where we started – several workers can read the balance at
once, and then all update it. To deal with this we could add some logic
in the fund itself, like a minimumBalance
property, or add another command called WithdrawIfOverXDollars
.
These are both terrible ideas. Our agreement is amongst ourselves, not a
property of the fund. We should keep it in application logic.
What we really need is transactions, in the same sense as
database transactions. Since our service executes only one command at a
time, this is super easy. We’ll add a Transact
command
which contains a callback (a closure). The server will execute that
callback inside its own goroutine, passing in the raw Fund
. The callback can then safely do whatever it likes with the Fund
.
// Typedef the callback for readability
type Transactor func(fund *Fund)
// Add a new command type with a callback and a semaphore channel
type TransactionCommand struct {
Transactor Transactor
Done chan bool
}
// ...
// Wrap it up neatly in an API method, like the other commands
func (s *FundServer) Transact(transactor Transactor) {
command := TransactionCommand{
Transactor: transactor,
Done: make(chan bool),
}
s.commands <- command
<- command.Done
}
// ...
func (s *FundServer) loop() {
for command := range s.commands {
switch command.(type) {
// ...
case TransactionCommand:
transaction := command.(TransactionCommand)
transaction.Transactor(s.fund)
transaction.Done <- true
// ...
}
}
}
Our transaction callbacks don’t directly return anything, but the Go
language makes it easy to get values out of a closure directly, so we’ll
do that in the benchmark to set the pizzaTime
flag when money runs low:pizzaTime := false
for i := 0; i < dollarsPerFounder; i++ {
server.Transact(func(fund *Fund) {
if fund.Balance() <= 10 {
// Set it in the outside scope
pizzaTime = true
return
}
fund.Withdraw(1)
})
if pizzaTime {
break
}
}
And check that it works:$ GOMAXPROCS=4 go test -bench . funding BenchmarkWithdrawals-4 5000000 775 ns/op ok funding 4.637s
Where to head next
This is just the tip of the ice-berg. Remember the title says - a brief introduction to the Go Programming Language, isnt it. I have deliberately left a lot of details here and there to keep it tight and short. For a much detailed narration and complete implementation of the examples mentioned here please head over to my detailed article - Go Programming Language