Implementing a generic filter for slice or map of structs in Go

I have multiple structs with a "DateLastModified" field, like so:

type Thing struct {
  DateLastModified time.Time
  ...
}

type Thing2 struct {
  DateLastModified time.Time
  ...
}

I have slices or maps of each of these structs:

things := []Thing{...}
thing2s := []Thing2{...}

//or

things := make(map[string]Thing)
thing2s := make(map[string]Thing2)

What I'd like to do is filter each of those slices or maps on their DateLastModified field.

This is simple to implement in a basic way, but I'm interested in learning more about Go.

What I'm wondering is: is there a way to implement that filter in such a way that I could do something like:

filteredThings := filterSliceOnTime(things, someTimeToFilterOn)
filtered2Things := filterSliceOnTime(thing2s, someTimeToFilterOn)

//or

filteredThings := filterMapOnTime(things, someTimeToFilterOn)
filtered2Things := filterMapOnTime(thing2s, someTimeToFilterOn)

The thing I'm trying to figure out is how to reduce redundant code since all these structs do have that DateLastModified field.

Thanks!


Go has no notion of co-/contra-variance, therefore filterXOnTime will not be able to operate on slices []V or maps map[K]V of structs of different underlying Vs.

With an interface

What you can do is declaring an interface I that exposes the common behavior, and have both structs implement that interface:

type Modifiable interface {
    GetDateLastModified() time.Time
}

type Thing struct {
    DateLastModified time.Time
    ...
}

func (t Thing) GetDateLastModified() time.Time {
    return t.DateLastModified
}

type Thing2 struct {
    DateLastModified time.Time
    ...
}

func (t Thing2) GetDateLastModified() time.Time {
    return t.DateLastModified
}

At this point you can have slices and maps of I, and your filter function can work (accept and return) with those:

func filterSliceOnTime(modifiables []Modifiable, someTimeToFilterOn time.Time) []Modifiable { 
    var filtered []Modifiable
    for _, m := range modifiables {
        if m.GetDateLastModified.After(someTimeToFilterOn) {
             filtered = append(filtered, m)
        }
    }
    return filtered
}

The effectiveness of this approach is slightly limited by the fact that you have to remap []ThingX to []Modifiable and vice versa in order to use the "generic" function.

With a helper func

To mitigate the maintenance hassle of the solution above, you can use instead a helper filter function that operates on one single item, so you don't have to map complex types back and forth:

func checkOne(v Modifiable, filterOn time.Time) bool {
    return v.GetDateLastModified().After(filterOn)
}

func main() {
    a := make(map[string]Thing, 0)
    a["foo"] = Thing{time.Now()}

    filtered := make(map[string]Thing, 0)
    for k, v := range a {
        if checkOne(v, time.Now().Add(-2*time.Hour)) {
             filtered[k] = v
        }
    }
    
    fmt.Println(filtered) // map[foo:{blah}]
}

Go 1.18 and type parameters

With Go 1.18 (early 2022) and the introduction of generics, you will be able instead to use the slices and maps directly. You will still have to declare the interface to provide the proper constraint for the type parameter, and the structs will still have to implement it.

With the current draft design, that might look like:

func filterSliceOnTime[T Modifiable](s []T, filterOn time.Time) []T {
    var filtered []T
    for _, v := range s {
        if v.GetDateLastModified().After(filterOn) {
            filtered = append(filtered, v)
        }
    }
    return filtered
}

Go2 playground: https://go2goplay.golang.org/p/FELhv0NSr5A