Go introduced generics in version 1.18, opening up exciting possibilities for creating more flexible and reusable code. In this article, we’ll explore how to build a generic Accumulator that can work with both integers and floating-point numbers, giving you a reusable tool for tracking sums, counts, maximums, minimums, and averages.
This article will guide you through building a generic accumulator using Go’s type parameters, constraint interfaces, and some core features of the Go language.
An accumulator is a simple data structure used to collect and analyze numerical data, like sums, counts, averages, and more. Without generics, we would need to write separate code for each numeric type (e.g., int, float64, etc.). Generics allow us to write this logic once and apply it to multiple types, making the code cleaner and easier to maintain.
Let’s walk through the design and implementation of our generic accumulator.
The first step in creating a generic accumulator is defining the types that will be allowed. In our case, we want the accumulator to work with both integers and floating-point numbers.
Go provides the constraints package, which contains predefined interfaces for numeric types. Here’s how we define a constraint called Numeric:
import “golang.org/x/exp/constraints”
type Numeric interface {
constraints.Integer | constraints.Float
}This constraint tells the compiler that Numeric can be any type that satisfies either the Integer or Float constraint. This is a key feature of Go’s generics: restricting type parameters to types that meet specific conditions.
type Accumulator[T Numeric] struct {
Sum T
Count float64
Max T
Min T
}The type parameter T is the generic type that must satisfy the Numeric constraint. This allows us to use any numeric type (e.g., int, float64) when creating an accumulator.
Next, we need a function to create and initialize a new accumulator. This function will ensure that the initial values for Sum, Count, Max, and Min are properly set:
func NewAccumulator[T Numeric]() *Accumulator[T] {
var zero T
return &Accumulator[T]{
Sum: zero,
Count: 0,
Max: zero,
Min: zero,
}
}In Go, generic functions use type parameters, which we specify with T. In this case, NewAccumulator returns a pointer to a new Accumulator[T] where all the fields are initialized to zero. We use var zero T to initialize a zero value for the generic type.
The Add method allows us to add a value to the accumulator and update the sum, count, max, and min:
func (a *Accumulator[T]) Add(value T) {
a.AddC(value, 1)
}This method calls a helper method AddC (short for “Add with Count”) that also allows us to specify how many times to add the value:
func (a *Accumulator[T]) AddC(value T, count float64) {
if a.Count == 0 {
a.Max = value
a.Min = value
} else {
if value > a.Max {
a.Max = value
}
if value < a.Min {
a.Min = value
}
}
a.Sum += value
a.Count += count
}In AddC, we check whether the count is zero, which would indicate that this is the first value being added. If it is, we initialize the max and min to the value being added. Otherwise, we compare the new value to the existing max and min to determine whether they need to be updated.
To make our accumulator more useful, we add a method to calculate the average of the values:
func (a *Accumulator[T]) Avg() T {
if a.Count == 0 {
var zero T
return zero
}
return a.Sum / T(a.Count)
}This method checks if any values have been added. If not, it returns zero. Otherwise, it calculates the average by dividing the sum by the count. Since Count is a float64, we need to convert it to the generic type T.
Sometimes, it’s useful to reset an accumulator to start fresh. Here’s a simple Reset method:
func (a *Accumulator[T]) Reset() {
var zero T
a.Sum = zero
a.Count = 0
a.Max = zero
a.Min = zero
}This method resets all the fields in the accumulator to their zero values.
Here’s the full code for our generic accumulator:
package main
import "golang.org/x/exp/constraints"
type Numeric interface {
constraints.Integer | constraints.Float
}
type Accumulator[T Numeric] struct {
Sum T
Count float64
Max T
Min T
}
func NewAccumulator[T Numeric]() *Accumulator[T] {
var zero T
return &Accumulator[T]{
Sum: zero,
Count: 0,
Max: zero,
Min: zero,
}
}
func (a *Accumulator[T]) Add(value T) {
a.AddC(value, 1)
}
func (a *Accumulator[T]) Reset() {
var zero T
a.Sum = zero
a.Count = 0
a.Max = zero
a.Min = zero
}
func (a *Accumulator[T]) AddC(value T, count float64) {
if a.Count == 0 {
a.Max = value
a.Min = value
} else {
if value > a.Max {
a.Max = value
}
if value < a.Min {
a.Min = value
}
}
a.Sum += value
a.Count += count
}
func (a *Accumulator[T]) Avg() T {
if a.Count == 0 {
var zero T
return zero
}
return a.Sum / T(a.Count)
}Let’s see how we can use the Accumulator for different numeric types:
package main
import (
"fmt"
)
func main() {
// Integer accumulator example
intAcc := NewAccumulator[int]()
intAcc.Add(10)
intAcc.Add(20)
intAcc.Add(5)
fmt.Printf("Integer Accumulator - Sum: %d, Count: %.0f, Max: %d, Min: %d, Avg: %d\n", intAcc.Sum, intAcc.Count, intAcc.Max, intAcc.Min, intAcc.Avg())
// Float accumulator example
floatAcc := NewAccumulator[float64]()
floatAcc.Add(10.5)
floatAcc.Add(20.3)
floatAcc.Add(5.1)
fmt.Printf("Float Accumulator - Sum: %.2f, Count: %.0f, Max: %.2f, Min: %.2f, Avg: %.2f\n", floatAcc.Sum, floatAcc.Count, floatAcc.Max, floatAcc.Min, floatAcc.Avg())
}Output:
Integer Accumulator - Sum: 35, Count: 3, Max: 20, Min: 5, Avg: 11
Float Accumulator - Sum: 35.90, Count: 3, Max: 20.30, Min: 5.10, Avg: 11.97
In the example above, we create two accumulators: one for integers and one for floating-point numbers. Both accumulators work seamlessly due to the use of Go’s generic type parameters.
Go’s support for generics makes it possible to write cleaner, more flexible, and reusable code. In this article, we’ve explored how to build a generic accumulator that works with both integers and floating-point numbers. With just a few lines of code, we can now accumulate sums, counts, averages, and more without writing separate implementations for different types.
This is just the beginning of what you can do with generics in Go. Whether you’re building libraries or complex systems, generics help reduce redundancy and improve code maintainability.