0

I have a small example where I try to populate a []Entry (where Entry is an interface) slice inside a function, this works fine when the argument is a single Entry, but when trying to pass a slice of entries I cannot figure my way through the pointers.

package temp

import (
    "encoding/json"

    uuid "github.com/satori/go.uuid"
)

type BaseEntry struct {
    ID uuid.UUID
}

func (entry *BaseEntry) GetID() uuid.UUID {
    return entry.ID
}

type Entry interface {
    GetID() uuid.UUID
}

func GetByCollection(collectionName string, entries []Entry) {
    // This could be a lookup in a database or filesystem

    collections := map[string][]string{
        "books": []string{
            `{"ID": "c8cc718d-94a9-4605-a4bb-05d5aa067fd6", "Title": "Nils Holgersson", "Author": "d89e4a0a-6a65-4754-9090-f8b6c7d6eeec"}`,
            `{"ID": "46ad379e-17f2-4961-b615-d4301160f892", "Title": "Ronja rövardotter", "Author": "a1bba636-0700-474f-8fe8-2ad3ec9d061c"}`,
        },
        "authors": []string{
            `{"ID": "d89e4a0a-6a65-4754-9090-f8b6c7d6eeec", "Name": "Selma Lagerlöf"}`,
            `{"ID": "a1bba636-0700-474f-8fe8-2ad3ec9d061c", "Name": "Astrid Lindgren"}`,
        },
    }

    if collections[collectionName] != nil {
        for _, entryData := range collections[collectionName] {
            entry := BaseEntry{}
            json.Unmarshal([]byte(entryData), &entry)
            *entries = append(*entries, &entry)
        }
    }
}

And the tests where I try to use this:

    package temp_test

    import (
        "testing"

        "github.com/mojlighetsministeriet/next/temp"
        uuid "github.com/satori/go.uuid"
        "github.com/stretchr/testify/assert"
    )

    type Book struct {
        temp.BaseEntry
    }

    func TestPopulate(test *testing.T) {
        books := []Book{}
        temp.GetByCollection("books", &books)
        assert.NotEqual(test, uuid.Nil, books[0].GetID())
    }

Depending if I declare GetByCollection(collectionName string, entries []Entry) or GetByCollection(collectionName string, entries *[]Entry) I get either:

# github.com/mojlighetsministeriet/next/temp
./main.go:39:4: invalid indirect of entries (type []Entry)

or

# github.com/mojlighetsministeriet/next/temp_test
./main_test.go:17:32: cannot use &books (type *[]Book) as type * 
[]temp.Entry in argument to temp.GetByCollection

How should I write so that I can choose the entry type by calling GetByCollection with say either a []Book or []Author slice that get populated?

I probably need to refactor somehow, in the end I like something similar to GORM's Query method (http://gorm.io/docs/query.html#Query) db.Find(&users) but like GetByCollection("books", &books)

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
tirithen
  • 3,219
  • 11
  • 41
  • 65
  • 1
    You should use `entries *[]Entry` as the function's argument. But note that, in Go, `[]Book` is not assignable/convertable to `[]Entry` even if `Book` implements `Entry`, you'll have to loop over `[]Book` to create a `[]Entry` slice. – mkopriva Jun 10 '18 at 11:38
  • ... see here https://stackoverflow.com/a/12754757/965900 – mkopriva Jun 10 '18 at 11:39
  • i dont think its a good way to test your function, why dont you just implement your interface and pass it to your test ?! – CallMeLoki Jun 10 '18 at 11:48
  • Thanks @mkopriva for the link, seems like I could get something out of that, and thanks for the clarification about the Entry interface -> Book conversion explanation. Somehow this worked when I sent in one &Book and took it in as an Entry and did json.Unmarshal(dataString, book). But when I sent in *[]Entry or []Entry I was not able to append to the slice. I could see if it works with returning a new slice instead. – tirithen Jun 10 '18 at 13:42
  • Thanks @danicheeta, what I'm trying to achieve is to have one method that lets you read/query/delete JSON files (in the example I reduced that code to some strings) as a mini database experiment, so I can't know before hand which structs I need to support other than that they will need to implement Entry. Do you have other suggestions on how I could test my function? – tirithen Jun 10 '18 at 13:44
  • @ThunderCat One file per entry like 0b27557a-00a5-46e6-8cf2-fbf40f19ca28.json and so on. – tirithen Jun 10 '18 at 14:22

1 Answers1

2

Reflection is required to work with different slice types. Here's how to do it:

func GetByCollection(collectionName string, result interface{}) {    
    collections := map[string][]string{
        "books": []string{
            `{"ID": "c8cc718d-94a9-4605-a4bb-05d5aa067fd6", "Title": "Nils Holgersson", "Author": "d89e4a0a-6a65-4754-9090-f8b6c7d6eeec"}`,
            `{"ID": "46ad379e-17f2-4961-b615-d4301160f892", "Title": "Ronja rövardotter", "Author": "a1bba636-0700-474f-8fe8-2ad3ec9d061c"}`,
        },
        "authors": []string{
            `{"ID": "d89e4a0a-6a65-4754-9090-f8b6c7d6eeec", "Name": "Selma Lagerlöf"}`,
            `{"ID": "a1bba636-0700-474f-8fe8-2ad3ec9d061c", "Name": "Astrid Lindgren"}`,
        },
    }

    slice := reflect.ValueOf(result).Elem()
    elementType := slice.Type().Elem()

    for _, entryData := range collections[collectionName] {
        v := reflect.New(elementType)
        json.Unmarshal([]byte(entryData), v.Interface())
        slice.Set(reflect.Append(slice, v.Elem()))
    }
}

Call it like this:

var books []Book
GetByCollection("books", &books)

Playground example

Charlie Tumahai
  • 113,709
  • 12
  • 249
  • 242