Since version 1.18
, Go has finally introduced the support for generics, together with any
as a type alias of interface{}
It gives more flexibility for user to create a general collection for similar data structures. Previously we have no choice but
to write up the same struct
for each data type we want to support.
The syntax can look a bit strange if you are already used to the old-school Go code. Or maybe it’s just because I haven’t used it often enough.
Generic struct
For example, I want to create a Cache
data structure that allows me to look up for compute resources available to execute workloads.
However, I have two types of such resources, so I want my Cache
and its methods to accept both data types.
type HostStatus int
// define enum values for the type HostStatus
const (
statusFree HostStatus = iota
statusOccupied
statusDecommissioned
)
// I've got Host that represents a single physical host
type Host struct {
Name string
Status HostStatus // it can be free, occupied or already shut down
}
func (h Host) GetName() string {
return h.Name
}
// I've also got resource pool that represents a cluster of multiple hosts
type Pool struct {
Name string
IsActive bool // it may or may not be active to accept workloads
}
func (p Pool) GetName() string {
return p.Name
}
I can now create one generic Cache
to contain either of the above types.
// define the resource interface here as a set of types,
// plus GetName method has to be implemented
type Resource interface {
Host | Pool
GetName() string
}
// define the generic Cache with type parameter of Resource
type Cache[T Resource] struct {
data map[string]*T
mu sync.Mutex
}
func (q *Cache[T]) Lookup(name string) *T {
return q.data[name]
}
func (q *Cache[T]) Update(resources []T) {
q.mu.Lock()
defer q.mu.Unlock()
for _, r := range resources {
q.data[r.GetName()] = &r
}
}
Now let’s use it in action:
func main() {
c := Cache[Host]{
data: make(map[string]*Host),
}
hosts := []Host{
{
Name: "host-1",
Status: statusFree,
},
{
Name: "host-2",
Status: statusOccupied,
},
}
c.Update(hosts)
fmt.Println(c.Lookup("host-1")) // &{host-1 0}
fmt.Println(c.Lookup("host-2")) // &{host-2 1}
fmt.Println(c.Lookup("host-3")) // nil
}
The same Cache
can be used for Pool
as well. Let’s use it again:
func main() {
pc := Cache[Pool]{
data: make(map[string]*Pool),
}
pools := []Pool{
{
Name: "pool-1",
IsActive: true,
},
{
Name: "pool-2",
IsActive: false,
},
}
pc.Update(pools)
fmt.Println(pc.Lookup("pool-1")) // &{pool-1 true}
fmt.Println(pc.Lookup("pool-2")) // &{pool-2 false}
fmt.Println(pc.Lookup("pool-3")) // nil
}
Generic function
Since the introduction of generics, new packages in Go added in later versions also start to make use of the concept.
Take package slices
for example:
// Index returns the index of the first occurrence of v in s,
// or -1 if not present.
func Index[S ~[]E, E comparable](s S, v E) int {
for i := range s {
if v == s[i] {
return i
}
}
return -1
}
The generic expression S ~[]E, E comparable
means that S
can be any type whose underlying type is a slice of E
, including []E
itself.
And element E
has to implement comparable
interface, which is a builtin interface implemented by basic types such as numbers
, strings
(so that equality operation can be performed).
Many functions in the slices
package have a more general counterpart. Take IndexFunc
for example, the element E
can be anything, but it is
now your responsibility to define the custom matching criteria in the form of a function parameter.
// IndexFunc returns the first index i satisfying f(s[i]),
// or -1 if none do.
func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
for i := range s {
if f(s[i]) {
return i
}
}
return -1
}
Overall, I don’t think Generics is used as often in my Go projects as the case in Java. However, I do find it come in handy when I occasionally have multiple entities that need to be managed in a similar way.