In the previous article, I discovered a repository that provides a deep explanation of generics: go-generics-the-hard-way. After reading it briefly, I finally managed to implement sorting for slices of multiple types. (I'm so bad at this!)
Theoretical Basis#
1. Generic constraints can be defined by specifying a collection of types.
// Numeric expresses a type constraint satisfied by any numeric type.
type Numeric interface {
uint | uint8 | uint16 | uint32 | uint64 |
int | int8 | int16 | int32 | int64 |
float32 | float64 |
complex64 | complex128
}
// Sum returns the sum of the provided arguments.
func Sum[T Numeric](args ...T) T {
var sum T
for i := 0; i < len(args); i++ {
sum += args[i]
}
return sum
}
2. The tilde (~) prefix in a constraint indicates support for other types with the same underlying type. With the above definition, you can write the following code:
// id is a new type definition for an int64
type id int64
func main() {
fmt.Println(Sum([]id{1, 2, 3}...))
}
This will result in a compilation error:
id does not implement Numeric (possibly missing ~ for int64 in constraint Numeric)
You need to add the ~ prefix to the int64 part of the type collection.
3. When a type contains generics, the generic symbol must be included in the function receiver. For example, if you have the following generic type:
// Ledger is an identifiable, financial record.
type Ledger[T ~string, K Numeric] struct {
// ID identifies the ledger.
ID T
// Amounts is a list of monies associated with this ledger.
Amounts []K
// SumFn is a function that can be used to sum the amounts
// in this ledger.
SumFn SumFn[K]
}
When defining methods for it, you need to explicitly include the generic symbol in the function receiver:
// PrintIDAndSum emits the ID of the ledger and a sum of its amounts on a
// single line to stdout.
func (l Ledger[T, K]) PrintIDAndSum() {
fmt.Printf("%s has a sum of %v\n", l.ID, l.SumFn(l.Amounts...))
}
Sorting Implementation#
With the understanding of the above knowledge, implementing sort.Interface
for multiple types becomes simple. The complete code is as follows:
package sort
type comparable interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64 |
~string
}
// Sortable is a generics slice which implements sort.Interface
type Sortable[T comparable] []T
func (s Sortable[T]) Len() int {
return len(s)
}
func (s Sortable[T]) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Sortable[T]) Less(i, j int) bool {
return s[i] < s[j]
}
To sort slices of various types, simply use the Sortable
wrapper:
package main
import (
"fmt"
"sort"
gsort "github.com/amtoaer/generic-sort"
)
func main() {
intSlice := []int{1, 3, 2, 4}
stringSlice := []string{"h", "e", "l", "l", "o"}
byteSlice := []byte{'h', 'e', 'l', 'l', 'o'}
sort.Sort(gsort.Sortable[int](intSlice))
sort.Sort(gsort.Sortable[string](stringSlice))
sort.Sort(gsort.Sortable[byte](byteSlice))
fmt.Println(intSlice) // [1 2 3 4]
fmt.Println(stringSlice) // [e h l l o]
fmt.Println(byteSlice) // [101 104 108 108 111]
}