Carl's Notes
Life complexity navigation algorithms

Using a temporary variable to operate on math/big objects in Go

On one of my side projects, I opted to use the Go standard library’s math/big classes, in particular big.Rat, which is one of the best class names I’ve encountered in a long time. bit.Rat models rational numbers (of the type a/b) of arbitrary precision, which I’m using for a finance app I’m working on.

(There are two other classes as well, big.Nat (integers) and big.Float, which also have the limitation described below.)

big.Rat has a funny API:

var a, b, c big.Rat
// Some calculations...
a.Add(&b, &c) // a = b + c

Binary operations like .Add typically take two operands and assign the result to the receiver (a in the example above).

However, there is an explicit caveat in the documentation for most such operations: you’re not supposed to pass the receiver in as an argument. In other words: no aliasing. More precisely, the documentation states (emphasis mine):

Operations always take pointer arguments (*Rat) rather than Rat values, and each unique Rat value requires its own unique *Rat pointer. To “copy” a Rat value, an existing (or newly allocated) Rat must be set to a new value using the Rat.Set method; shallow copies of Rats are not supported and may lead to errors.

So the following, although it may seem to work fine, is technically forbidden:

func average(l []big.Rat) big.Rat {
    // DON'T DO THIS!!!
    var avg big.Rat
    
    for _, v := range l {
        avg.Add(&avg, &v)  // Aliasing: avg = avg + v
    }
    
    avg.Quot(&avg, len(l)) // Aliasing: avg = avg / len(l)
    return avg
}

I’m not sure how many users math/big has, but there is surprisingly little to be found about this specific use case online, so I thought I’d document it here.

It seems the easiest way to operate safely on big.Rat is to use a local temporary variable:

func average(l []big.Rat) big.Rat {
    var avg big.Rat
    var tmp big.Rat // Temp variable for binary operations
    
    for _, v := range l {
        avg.Set(tmp.Add(&avg, &v))  // Correct: avg = avg + v
    }

    avg.Set(tmp.Quot(&avg, len(l))) // Correct: avg = avg / len(l)
    return avg
}

You can keep the tmp variable on the stack and re-use it for every computation to minimize the overhead.

Read another post