Go Pattern: Hybrid Handler

Overview In today’s high-performance and concurrent computing environments, effectively processing a stream of messages using a mix of purely computational functions and remote procedure calls has become a significant challenge. The Go programming language is designed to handle concurrency well, but when it comes to managing a hybrid load, even Go can struggle to achieve optimal CPU utilization. In this article, we will discuss the Hybrid Handler pattern, an efficient and unified approach to address this challenge. ...

April 22, 2023

Go Pattern: Runner

Again and again, a concurrent pattern emerges from the need to control goroutine lifecycles and handle their errors, and I call it the “Runner Pattern”. The runner interface and its contract The pattern is as simple as a single-method interface: // Runner defines the Run method to be executed within a goroutine type Runner interface { Run(ctx context.Context) error } The contract of the interface covers two aspects. On the goroutine lifecycle, the Run method will block until one of the following occurs: ...

February 22, 2022

Go Anti-pattern: Parent Closer

Imagine you need to wrap multiple objects which implements io.Closer, e.g. three clients to fetch and combine data from different endpoints. type Parent struct { child1 Child1 child2 Child2 child3 Child3 } Parent closer Let’s see how we can create and destroy a parent object. func NewParent() (*Parent, error) { child1, err := NewChild1() if err != nil { return nil, err } child2, err := NewChild1() if err != nil { // oops, child1 needs to be closed here child1.Close() return nil, err } child3, err := NewChild1() if err != nil { // oops again, both child1, and child2 needs to be closed here child1.Close() child2.Close() return nil, err } return &Parent{ child1: child1, child2: child2, child3: child3, }, nil } func (p *Parent) Close() error { var errs []error if err := p.child1.Close(); err != nil { errs = append(errs, err) } if err := p.child2.Close(); err != nil { errs = append(errs, err) } if err := p.child3.Close(); err != nil { errs = append(errs, err) } return multierr.Combine(errs...) } Note the boilerplate code of closing the children. Because the parent creates its children, it must be responsible for calling their Close method whenever needed. If there are any errors during the initialisation, the children already created have to be properly closed, and before the parent exits its scope, it has to close its children too. ...

January 8, 2021

Go Pattern: Context-aware Lock

We often use Mutex or RWMutex as locks in Go, but sometimes we need a lock that can be cancelled by a context during the lock attempt. The pattern is simple - we use a channel with length 1: lockChan := make(chan struct{}, 1) lockChan <- struct{}{} // lock <- lockChan // unlock When multiple goroutines try to obtain the lock, only one of them is able to fill into the only slot, and the rest are blocked until the slot is empty again after a readout. ...

November 30, 2020

Go Pattern: Buffered Writer

A buffered writer is so ubiquitous that we do not usually consider it as a pattern, but sometimes we reinvent it or even do it in an inferior way. Let us look at a real use case first. Batch processor What would you do to improve the throughput of a service? The answer is short: batching. By processing and sending in a batch of multiple items instead of a single item at a time, you are amortizing the network overhead from the request-response round trip among all the items in the batch. ...

November 22, 2020