@h12.io

Go Anti-pattern: Parent Closer

8 January 2021

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.

Furthermore, the Closer interface is contagious. If we organise our code by wrapping objects layer by layer like above, and any one of the descendants is a Closer, then all the types in the hierarchy are infected and have to implement the Closer interface too.

Parent container

Unlike the parent closer, all of the complexity could have been avoided if the parent is a simple container, borrowing the references of the children rather than owning them, as long as the children outlive its parent.


func NewParent(child1 Child1, child2 Child2, child3 Child3) *Parent {
    return &Parent{child1: child1, child2: child2, child3: child3}
}

func run() error {
    child1, err := NewChild1()
    if err != nil {
        return nil, err
    }
    defer child1.Close()
    child2, err := NewChild1()
    if err != nil {
        return nil, err
    }
    defer child2.Close()
    child3, err := NewChild1()
    if err != nil {
        return nil, err
    }
    defer child3.Close()

    parent := NewParent(child1, child2, child3)

    // the parent can be used safely here before func run returns
}

It is usually straightforward to guarantee the children outlive its parent in real cases:

  • either the parent is created and held by a service during its whole lifetime, and func run could be a function that keeps running until the service terminates
  • or the parent is created when handling a request, and func run is the request handler

The key difference between a “parent closer” and a “parent container” is that the latter makes it possible to use the defer statements to close the children in either error or normal case, so the duplicated clean-up code can be avoided.

Conclusion

io.Closer interfaces are contagious. Usually, we do not want to wrap them to make another io.Closer, instead, we should only wrap them by reference borrowing, without managing their lifetime within the wrapper.

Probability as a Generalisation of Boolean Algebra

14 December 2020

A summary of Boolean algebra

Given the following notations:

  • a proposition is denoted as a lowercase letter, e.g. $p$, $q$
  • the truth value of a proposition $p$ is denoted as $ B(p) \in \set{0, 1} $, where $B(p)=1$ if $p$ is true or $B(p)=0$ if $p$ is false

Negation (not, $¬$), conjunction (and, $∧$) and disjunction (or, $∨$) are defined by the truth tables below:

$B(p)$ $B(¬p)$
0 1
1 0

$B(p)$ $B(q)$ $B(p∧q)$ $B(p∨q)$
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 1

Truth value allocation as an intuitive picture of Boolean algebra

Intuitively, we can derive the same truth tables by following the process of the truth value allocation with the two steps below:

  • List all mutually exclusive results from propositions under consideration
  • Allocate the truth value to them so that their truth-values sum up to $1$

Note that the second step actually requires only one of result has a truth value $1$, given $ B(p) \in \set{0, 1} $.

Negation

For a single proposition $p$, there are two mutually exclusive results $p$ (is true) or $¬p$ (is true), as follows:

$¬p$ $p$
$B(¬p)$ $B(p)$

Where we can allocate the truth value:

$$B(¬p) + B(p) = 1$$

Thus we get an equivalent equation to the negation truth table.

Conjunction

Now let’s consider two propositions $p$ and $q$ at the same time. There are four mutually exclusive results: $(p∧q)$, $(¬p∧q)$, $(p∧¬q)$ or $(¬p∧¬q)$. Thus we have the truth value allocation table:

$¬p$ $p$
$¬q$ $B(¬p∧¬q)$ $B(p∧¬q)$
$q$ $B(¬p∧q)$ $B(p∧q)$

Where we can allocate the truth value:

$$ B(¬p∧¬q) + B(p∧¬q) + B(¬p∧q) + B(p∧q) = 1 $$

From the allocation we can easily get $B(p)$ as the sum of the second column:

$$ B(p) = B(p∧q) + B(p∧¬q) $$

And $B(q)$ as the sum of the second row:

$$ B(q) = B(p∧q) + B(¬p∧q) $$

Similar equations can be derived for $B(¬p)$ as the sum of the first column and $B(¬q)$ as the sum of the first row.

If we use the notation $B(q|p)$ as the ratio of truth value “q is true” given “p is true”, the conjunction $B(p∧q)$ can be expressed as:

$$ B(p∧q) = B(p) \times B(q|p) $$

This equation can be read from the allocation table as “the sum of the first column where $p$ is true” times “the ratio of $q$ is true given $p$ is true”.

Also, note that $B(q|p) = B(q)$, which can be easily derived from the allocation table. Thus we have the equivalent equation for the conjunction:

$$ B(p∧q) = B(p) \times B(q) $$

Disjunction

The disjunction $B(p∨q)$ can be calculated by summing up the second column (where $p$ is true) and the second row (where $q$ is true) but subtracting $B(p∧q)$ (which is summed twice):

$$ B(p∨q) = B(p) + B(q) - B(p∧q) $$

From Boolean algebra to probability

The probability can be generalised from the similar notations and intuitive allocation picture, by replacing the truth value with the probability value.

Given the following notations:

  • a proposition is denoted as a lowercase letter, e.g. $p$, $q$
  • the probability of a proposition $p$ is denoted as $ P(p) \in [0, 1] $, where $P(p)$ represents our belief in the truthfulness of the proposition $p$

If we enumerate all possible mutually exclusive outcomes and allocate the probability value $1$ to them. We will get similar equations as the Boolean algebra.

$$ P(p) + P(¬p) = 1 $$

The product (chain) rule:

$$ P(p∧q) = P(p) \times P(q|p) $$

Where $P(q|p)$ is called the conditional probability of $q$ given $p$.

And the sum rule:

$$ P(p∨q) = P(p) + P(q) - P(p∧q) $$

Then all the rest of the probability theory could be derived from the definitions above.

Also note that the notations above are equivalent to the Kolmogorov first and second axioms and the probability allocation is the third axiom, only expressed more intuitively but less formally.

Go Pattern: Context-aware Lock

30 November 2020

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.

Unlike mutexes, we could easily cancel the lock attempt with a context:

select {
    case <-ctx.Done():
        // cancelled
    case lockChan <- struct{}{}:
        // locked
}

Let’s wrap it up:

type CtxMutex struct {
    ch chan struct{}
}

func (mu *CtxMutex) Lock(ctx context.Context) bool {
    select {
        case <-ctx.Done():
            return false
        case mu.ch <- struct{}{}:
            return true
    }
}

func (mu *CtxMutex) Unlock() {
    <- mu.ch
}


func (mu *CtxMutex) Locked() bool {
    return len(mu.ch) > 0 // locked or not
}

Further, context.WithTimeout could be used to apply a timeout to the lock.

Go Pattern: Buffered Writer

22 November 2020

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.

Then how would you design a client interface to do that batching?

How about this?

type BatchProcessor interface {
    Process(items []Item) error
}

It looks like a straightforward solution, but in reality, it introduces unnecessary complexity in both business logic and error handling.

The processing is often composed of multiple steps working on the items, e.g. transformations and encoding.

items -> transformations -> encoding -> bytes

With a batch processor interface like above, each step has to loop around the items, and each step has to deal with the errors from multiple items. Not only there is more complexity but also less flexibility. What if the client would like to send the rest of the items, even if some of the items return errors? What if the client instead would like to discard the whole batch if any one of them is erroneous?

There must be a better way.

End-to-end principle

“Smart terminals, dumb network”. The end-to-end (e2e) principle, articulated in the field of computer network, basically says any smart features should reside in the communicating end nodes, rather than in intermediary nodes.

In our use case, the smart feature is batching. By e2e, we make sure each step should only process a single item, and only the initial sender and the final receiver knows about the batching.

There are various examples In Go’s standard packages that already do this, e.g. bufio.Writer. The basic idea is an interface similar to below:

type BufferedWriter interface {
    Write(item Item) error
    Flush() error
}

The caller issues multiple writes to make a batch and a flush to mark the end of the batch. The writer chains the transformation and encoding steps of an item in a single write method and returns the error for the item. When the flush method is called, the writer flushes the whole batch and completes the batch.

Stateless vs Stateful

On the surface, BatchProcessor is stateless while BufferedWriter is stateful, but the former only pushes to its caller the responsibility of aggregating a batch, which is a stateful operation. On the other hand, the final step of the processing - the underlying driver regardless it is of file or network IO - is stateful too. So BufferedWriter does not add additional burden to its caller for managing a stateful interface.

Rather, BufferedWriter not only simplifies the chain of processing within it, but also simplifies the batching logic on its caller side.

Concurrency

A BufferedWriter can become concurrently safe by locking both Write and Flush methods. However, the ideal way of calling a BufferedWriter is from a single goroutine so that the caller is able to control exactly what are in the batch, and we can get rid of the overhead of the lock.

If multiple goroutines must share a single underlying writer and at the same time want to control its own batches, then we could return an object instead of flushing, as below:

type Builder interface {
    Write(item Item) error
    Bytes() []byte // return bytes
    Object() Batch // or a batch object
}

In fact, it becomes the Builder Pattern. Each goroutine has its own builder, building its own batches, and then sending those batches to a shared driver.

In addition, we could even have various write methods, each for its own item type.

Transaction

If the caller needs to discard a batch, we could extend it with a rollback method, similar to sql.Tx:

type TxWriter interface {
    Write(item Item) error
    Commit() error
    Rollback() error
}

Then it becomes the Unit of Work Pattern.

Conclusion

Whenever we want to process and send multiple items, consider this Buffered Writer Pattern and its variants and see if it can better suit our needs.

Value vs Pointer Receivers

19 June 2020

Should I use value receivers or pointer receivers?

Value receivers have some benefits include immutability, concurrent safety and clean logic (not always, often true). But to what extend can I use value receivers without an issue or performance penalty?

In the Go FAQ, there are 3 rules:

  1. most important, does the method need to modify the receiver? If it does, the receiver must be a pointer
  2. if the receiver is large, a big struct for instance, it will be much cheaper to use a pointer receiver
  3. if some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used

Let’s look at rule 1. In many cases, an object needs to be modified by a method after its initialization, but it doesn’t mean the modification has to be done in place, an alternative and often a better way is to get a modified copy of the object, which is usually called a Fluent Interface. Basically it means a method with a value receiver and a return value of the same type. It’s just pure (no side effect) functional programming style under another name.

Then rule 2 tells us to choose based on the struct size, but how big is a big struct? We know that time.Time is 3-words large with value receivers, but how about 4-words, 5-words or bigger structs? To answer this question, I did some benchmarks.

The benchmarks are based on the idea that a type with value receivers would need to implement fluent interface, so it should compare the performance between two fluent methods with value and pointer receivers:

func (s S) ByValue() S {
    // ...
	return s
}

func (s *S) ByPointer() *S {
    // ...
	return s
}

It turned out inlining is critical to the overhead of value copying.

When inlining is disabled (gcflags=-l), for a struct of 1 word, value receiver has the same performance as the pointer receiver, but for structs larger than 2 words, value receivers begin to show more and more overhead.

Without Inline

However, when inlining is enabled (the default option), value receivers have almost the same performance as pointer receivers for struct size from 1 up to 9 words! Overhead of value copying starts to rise for structs of 10 words or more.

With Inline

These results were tested on a Macbook Pro with go1.14.3.

Conclusion

From the benchmark results, value receivers should be preferred if the methods are inlined and the struct size is equal to or smaller than 9 words (1 word == 64 bit), but this might not be the end of the story, the more complex a method is, the less likely it is inlined, but also the less proportion the copying overhead to the whole method logic. The overhead of copying from a value receiver might not be significant to the overall running time, so at the end of the day, the choice depends on the frequency of the method calls, the business logic and the performance requirements. When in doubt, benchmark it!

Go vs Python

27 September 2019

Slices

Go slice and Python slice have very similar syntax, but Python slice is a shallow copy of part of the original list, while Go slice is just a new range within the same underlying array of the original slice.

Let’s try:

a = [1, 2, 3]
b = a[:2]
b[0] = 9
print(a)
print(b)

# output:
# [1, 2, 3]
# [9, 2]

See a[0] remains the same.

package main

import (
    "fmt"
)

func main() {
	a := []int{1, 2, 3}
	b := a[:2]
	b[0] = 9
	fmt.Println(a)
	fmt.Println(b)

	# output:
	# [9 2 3]
	# [9 2]
}

See a[0] changes because slice a and b shares the same underlying array.

Learning Frontend

15 February 2019