1
votes

Rather than having multiple API fetching functions, I want to make one function that can fetch the appropriate database, format it into a slice of structs, and return it. Below is a simplified version of my code:

type Person struct {
        Name   string `json:"name"`
        IsCool bool   `json:"is_cool"`
}

type Pet struct {
        Name     string `json:"name"`
        IsFluffy bool   `json:"is_fluffy"`
}

type Group struct {
        Name         string    `json:"name"`
        CreationDate time.Time `json:"creation_date"`
}

type generic interface{}

func FetchDB(dbName string) interface {
        var collection []generic
        var model      generic

        switch dbName {
        case "users":
                collection = new([]User)
                model      = new(User)
        }
        case "pets":
                collection = new([]Pet)
                model      = new(Pet)
        case "groups":
                collection = new([]Group)
                model      = new(Group)
        default:
                return nil
        }

        iter := client.DB(dbName).Get()

        for {
                entry, err := iter.Next()
                if err != nil {
                        break
                }
                entry.ToStruct(&model)

                collection = append(collection, model)
        }

        return collection
}

However, calling this function results in cannot use new([]User) (type *[]User) as type []generic in assignment. My first question is how I can create a "generic" slice, and assign a type to it. My second question is whether this is a correct way to set up this function—using the generic interface feels hacky!—and if it is not, what would be a more sensible way to then design this function.

2
You can't. There are no generics in Go. The best way to do this would be to pass an exemplar object from your caller to your function to be filled with data from the DB, much as sqlx.Get, json.Unmarshal, etc. do.Adrian

2 Answers

1
votes

You are using new, not make for slices. Those two are different. new allocates memory for the type (in this case, the slice, not the contents of the slice), whereas make makes an instance of that type (make([]User,0) will make a slice of Users).

You can make this work as follows:

collection:=make([]generic,0)
switch dbName {
   case "users":
       model      = new(User)
   case "pets":
       model      = new(Pet)
   case "groups":
       model      = new(Group)
   default:
       return nil
}

And your model is already a pointer and you want to store data where that pointer points to:

  entry.ToStruct(model)  // Not &model

You're returning an interface{}, instead you can return []generic, that saves you from one type assertion to get the array part of the value. But that's when your problems will start because there are no generic in Go, and you'll have to write many type assertions to get to the data you need.

There are several ways this can be done. One approach would be a callback function to collect data:

func FetchDB(dbName string, collector func(entry Entry) {
  iter := client.DB(dbName).Get()
  for {
        entry, err := iter.Next()
        collector(entry)
   }
}

And call it as:

users:=make([]User,0)
FetchDB("users",func(entry Entry) {
   var u User
   entry.ToStruct(&u)
   users=append(users,u) 
 })
1
votes

You can't. There are no generics in Go.

For now (Go 1.17, 2021)

But there is:

We propose defining a new package, slices, that will provide functions that may be used with slices of any type.
If this proposal is accepted, the new package will be included with the first release of Go that implements #43651 (we currently expect that that will be Go 1.18).

That would give you:

func Equal[T comparable](s1, s2 []T) bool
func EqualFunc[T1, T2 any](s1 []T1, s2 []T2, eq func(T1, T2) bool) bool
func Compare[T constraints.Ordered](s1, s2 []T) int
func CompareFunc[T any](s1, s2 []T, cmp func(T, T) int) int
func Index[T comparable](s []T, v T) int
func IndexFunc[T any](s []T, f func(T) bool) int
func Contains[T comparable](s []T, v T) bool
func Insert[S constraints.Slice[T], T any](s S, i int, v ...T) S
func Delete[S constraints.Slice[T], T any](s S, i, j int) S
func Clone[S constraints.Slice[T], T any](s S) S
func Compact[S constraints.Slice[T], T comparable](s S) S
func CompactFunc[S constraints.Slice[T], T any](s S, cmp func(T, T) bool) S
func Grow[S constraints.Slice[T], T any](s S, n int) S
func Clip[S constraints.Slice[T], T any](s S) S

This proposal just got implemented with CL 355253 (Oct. 2021).

This will be for Go 1.18... or, judging by the recent reaction (issue 48918) from Rob Pike (co-creator of the Go language), Go 1.19 or even Go 1.20:

The language changes have been worked on in some form for over a decade, but the library changes are very new, and we have no experience with the use of the new types in Go on which to base a strong case for their design.

Instead, I propose we still design, build, test, and use new libraries for slices, maps, channels, and so on, but start by putting them in the golang/x/exp repository.

That way, these new libraries - which are truly experimental at this stage - can be tested in production, but can be changed, adapted, and grown for a cycle or two, letting the whole community try them out, if they are interested and willing to accept a little instability, without requiring every detail of every component to be ready from day one.

Once they have soaked a bit, and updated through experience, we move them into the main repo as we have done with other externally-grown packages, but with the confidence that they work well in practice and are deserving of our compatibility promise.