I have two different types of activity structs which both have a StartTime. For both of these activity types I have defined a type for a slice of activities. For both of these activity types I want to perform the same processing of their start times. I currently have to have 2 copies of the function, differing only in parameter type, as I cant work out how to get a single function to accept either type.
I've tried adding an interface above the activity types based on the StartTime, but it didnt help due to the custom slice types.
How can I factor out this function so that it can process either activity type?
Here is an example of the problem, with the 2 virtually identical functions: https://play.golang.org/p/pFg8yJwW2Hl
CodePudding user response:
You can create a Starter
interface that your two (or more) types implement:
type Starter interface {
Start() time.Time
}
func (a activityA) Start() time.Time { return a.StartTime }
func (b activityB) Start() time.Time { return b.StartTime }
Then use the reflect
package to introspect your function parameter:
- ensuring the argument is a slice type
- and each element of the slice implements
Starter
the downside of this is that any errors are only caught at runtime, not compile time.
func genericStartTimeActivity(v interface{}) (counter int, err error) {
if reflect.TypeOf(v).Kind() != reflect.Slice {
err = fmt.Errorf("argument must be slice type, got type '%T'", v)
return
}
s := reflect.ValueOf(v)
// since we know v is a slice:
// - s.Len() gets slice length
// - s.Index(i) gets a particular value
for i := 0; i < s.Len(); i {
activity, ok := s.Index(i).Interface().(Starter)
if !ok {
err = fmt.Errorf("argument's slice index %d does not implement '%s' interface", i, reflect.TypeOf((*Starter)(nil)).Elem().Name()) // pay no attention to the man behind the curtain
return
}
counter = activity.Start().Day()
}
return
}
https://play.golang.org/p/v8dXSPPLk1l
CodePudding user response:
An alternative to interfaces and reflect is to use a functional style.
func lookupSomethingToDoWithStartTimesFromFunction(n int, getStartTime func(i int) time.Time) int {
counter := 0
for i := 0; i < n; i {
counter = getStartTime(i).Day() // do something with the start time
}
return counter
}
...
lookupSomethingToDoWithStartTimesFromFunction(len(AActivities), func(i int) time.Time { return AActivities[i].StartTime }),
lookupSomethingToDoWithStartTimesFromFunction(len(BActivities), func(i int) time.Time { return BActivities[i].StartTime }),
https://play.golang.org/p/X95AAkwUVX3
CodePudding user response:
Make your function declare what it needs by using an interface. If your function needs to know only the start time of many different things, make all of those things able to say what their start time is using a method. Using your original example (playground):
package main
import (
"fmt"
"time"
)
type activityA struct {
Name string
StartTime time.Time
UniqueStuffForActivityA int
}
func (a activityA) StartedAt() time.Time {
return a.StartTime
}
type activityB struct {
ActivityName string
StartTime time.Time
UniqueStuffForActivityB string
}
func (a activityB) StartedAt() time.Time {
return a.StartTime
}
type Starter interface {
StartedAt() time.Time
}
func main() {
AActivities := []activityA{
activityA{"Act1", time.Now(), 5},
activityA{"Act2", time.Now(), 99},
activityA{"Act3", time.Now(), 23},
}
BActivities := []activityB{
activityB{"Act1", time.Now(), "foo"},
activityB{"Act2", time.Now(), "bar"},
activityB{"Act3", time.Now(), "badger"},
}
AActivitiesStarter := make([]Starter, len(AActivities))
for i := range AActivities {
AActivitiesStarter[i] = AActivities[i]
}
BActivitiesStarter := make([]Starter, len(BActivities))
for i := range AActivities {
BActivitiesStarter[i] = BActivities[i]
}
fmt.Printf("A: [%d]\nB: [%d]\n",
lookupSomethingToDoWithStartTimes(AActivitiesStarter),
lookupSomethingToDoWithStartTimes(BActivitiesStarter),
)
}
func lookupSomethingToDoWithStartTimes(activities []Starter) int {
counter := 0
for _, activity := range activities {
counter = activity.StartedAt().Day() // do something with the start time
}
return counter
}
The loop to convert from a slice of []activityX to a []Starter could be removed by just using a []Starter in the first place, depending on what else you might do with the slices. This loop may be faster or slower than using reflection on a []interface{}, you'd need to Benchmark your case to find out, but it's more readable.
You can also avoid converting entirely if you allow your collection objets to yield Starters with a channel as in this example but you must ensure you read the channels to completion or you will leak goroutines.