Confused about interfaces

I have a function which works when I return a concrete slice, in this case of type schemas.Transaction. In this code, results is a slice of *schemas.Transaction. I try to cast it as []*schemas.Record and it fails (ok is false). I get a panic when it executes:
return results.([]*schemas.Record), nil
Here is the function:

func findTransactions(q *db.Query, dbUniqueIdentifier database.DBUniqueIdentifier, cli *client.Client) ([]*schemas.Record, error) {

	results, err := cli.Find(context.Background(), ThreadID(dbUniqueIdentifier),
		data.TransactionCollection.Name, q, &schemas.Transaction{})
	if err != nil {
		return nil, err
	}

	instanceSlice, ok  := results.([]*schemas.Record)
	if ok {
		instance := instanceSlice[len(instanceSlice)-1]
		fmt.Println(instance)
	}

	return results.([]*schemas.Record), nil
}

cli.Find() returns (interface{}, error)
If I have the return value set to '[]*schemas.Transaction` it works fine.

Definition of schemas.Transaction:

type Transaction struct {
	ID              string  `json:"_id"`
	DebitAccountID  string  `json:"debit-account-id"`
	CreditAccountID string  `json:"credit-account-id"`
	Amount          float64 `json:"amount"`
	Description     string
	Date            time.Time
	FiscalYear      int
	FiscalPeriod    int
	CreatedAt       int64 `json:"created_at"`
}

func (transaction Transaction) RecordID() string {
	return transaction.ID
}

Definition of schemas.Record:

type Record interface {
	RecordID() string
}

I would like to make the findTransactions function more generic so it can handle other schemas. If I can make this work, the function calling this one could call RecordID() as the other schemas all support this call.

Hi @rschluet,

I lack the complete context of your scenario, so the following question might be stupid, but…

…if the interface has only one method, and this method only returns the record ID, why not having FindTransaction return the ID value directly?


Edited to add: I suppose the callers of FindTransaction shall also be able to retrieve the record data via type switches, correct?

If the number of schemas is small, and all schemas are known in advance, consider having a FindTransactionX function for each of the schemas.

For large amounts of possible schemas, you might want to look into SQL packages and ORM frameworks. They face the same problem, and some of them might actually have found a suitable solution.

Hi, thanks for your reply. I’m just learning Go and really want to understand how interfaces work. I could write a function for each table but I want to understand why this cast doesn’t work. I thought that as long as the underlying type satisfies that interface, (in this case just RecordID()), the cast should work and I could call .RecordID() on it.

For more context, this function is called by a function trying to retrieve all of the records, but the underlying database is limited to 10,000 records per fetch, so the ID of the last record retrieved is used to seek the next batch. The underlying database is threadsdb by textile.io.

A slice of interfaces cannot be asserted to be a slice of implementations or vice versa. An interface value holds a reference to a value that implements that interface type. A type assertion checks that the inner value is of the type requested. A slice is not an interface, so that type assertion won’t work for two reasons. A slice of interfaces may contain values of different types that implement the interface, so it would need to verify each element of the slice. Second, a interface value is not identical to the value that implements it, so a new slice needs to be allocated that holds the contained implementing values.

Here’s a minimal version of your problem as I understand it: Go Playground - The Go Programming Language. I also added an attempted type conversion as well as a type assertion.

One more thing, you can think of interface values as wrappers and type assertions as an unwrapping operation. That may help you understand why containers of interfaces cannot be simply unwrapped as the implementing types. There are several places in Go where this wrapping/unwrapping is implicit, so that somewhat obfuscates the mental model.

2 Likes

But the casting works if the concrete type matches what is in the slice of interface{}.
This works fine:

func populateResults(collectionType data.CollectionType, p *data.GeneralLedgerPage, results interface{},
	sorted bool) error {

	if collectionType == data.CollectionTransaction {
		log.Println("populateResults for collectionType: and year: period: report type:", collectionType, p.FiscalYear, p.FiscalPeriod, p.ReportType.Name())
		p.Transactions = make([]schemas.Transaction, 0)
		if results != nil {
			log.Println("populateResults: p.DivisionCode: ", p.DivisionCode)
			for _, v := range results.([]*schemas.Transaction) {

It only seems to fail if I cast it as a different interface type. So the answer is that you can only cast an interface to its actual concrete type, and not to an interface which the concrete type implements.

Go does not do type casts. This is a subtle but important difference to other languages that allow type casting - in other words, to shoehorn a value into a new type.

In Go, what looks like a cast is actually a type assertion. results.([]*schema.Transactions) asserts that the dynamic type of results is identical to []*schema.Transactions. If the assertion holds, you get an ok and can treat the value as the new type.

3 Likes

In your example here, results is an interface value, so your type assertion is asserting that it “wraps” a []*schema.Transaction. I modified the minimal example here Go Playground - The Go Programming Language, and you can see it does not allow a []C to be type asserted to a []I or vice versa even when passing through an intermediate stage as interface{}.

1 Like

Thanks, looks like I will need to have a separate function for retrieving each table type.

Rick, glad to see you’re still around on the forum and learning new stuff! I’m wondering if generics might help you not repeat yourself here. Let’s take your findTransactions function and rewrite it (in a stripped-down version; I’ll get to that later) as a generic function called genericFind:

func genericFind[T Record]() ([]*T, error) {
	// Dummy client
	cli := Client{}
	// Create a dummy interface to pass in to Find
	var t T
	// Safe because of our Record constraint
	fmt.Println("Record ID:", t.RecordID())
	results, err := cli.Find(context.Background(), "Some Collection Name", &t)
	if err != nil {
		return nil, err
	}
	instanceSlice, ok := results.([]*T)
	if !ok {
		return nil, errors.New("Couldn't assert type")
	}
	return instanceSlice, nil
}

Note that I put a type constraint on T so it’s safe to use t.RecordID(). For more info, check this blog post out. I also created a dummy Client struct and implemented a stripped down version of Find from the library it looks like you are using. Here’s the client:

// Dummy client
type Client struct{}

… and here’s Find:

// Stripped down find. See also:
// https://github.com/textileio/go-threads/blob/a0a34935e68d1b25d3310d2e44c3671468d9abff/api/client/client.go#L542
func (c *Client) Find(ctx context.Context, collectionName string, dummy interface{}) (interface{}, error) {
	return processFindReply(dummy)
}

… which calls processFindReply:

// Stripped down processFindReply. See also:
// https://github.com/textileio/go-threads/blob/a0a34935e68d1b25d3310d2e44c3671468d9abff/api/client/client.go#L690
func processFindReply(dummy interface{}) (interface{}, error) {
	sliceType := reflect.TypeOf(dummy)
	elementType := sliceType.Elem()
	length := 2
	results := reflect.MakeSlice(reflect.SliceOf(sliceType), length, length)
	for i := 0; i < length; i++ {
		target := reflect.New(elementType).Interface()
		val := results.Index(i)
		val.Set(reflect.ValueOf(target))
	}
	return results.Interface(), nil
}

OK so let’s assume we have the following structs that both implement our Record interface (and thus satisfy our generic type constraint):

type Transaction struct {
	ID                      string
	OnlyExistsOnTransaction string
}

func (transaction Transaction) RecordID() string {
	return transaction.ID
}

type OtherTransaction struct {
	OtherTransactionID       string
	OtherTransactionProperty string
}

func (ot OtherTransaction) RecordID() string {
	return ot.OtherTransactionID
}

We can use the same function to retrieve different record types like this:

func main() {
	// First find transactions
	transactions, err := genericFind[Transaction]()
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, v := range transactions {
		fmt.Println("RecordID:", v.RecordID())
		fmt.Println("OnlyExistsOnTransaction:", v.OnlyExistsOnTransaction)
	}

	// Also find other transactions
	otherTransactions, err := genericFind[OtherTransaction]()
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, v := range otherTransactions {
		fmt.Println("RecordID:", v.RecordID())
		fmt.Println("OtherTransactionProperty:", v.OtherTransactionProperty)
	}
}

Hope this helps! Also regarding interfaces, check out this excellent blog post:

1 Like

This is not unexpected, Go does not have inheritance and the generics are not really “generics” in the sense of C++. It also does not actually have “casting” in the same sense that say, Java has.

The reason this does not work, is it is very complicated and expensive and very few languages support what is called “implicit polymorphism”.

an []*schemas.Transaction is a completely new and different type than []*schemas.Record

Much like in the a []Cat is a completely new and different type than []Animal because you can append a []Dog to []Animal and a Dog is not a Cat even if both are Animal.

It is complicated problem to solve because it has a pretty infinite set of constraints that the machine has to reason about. That you as the programming can reason about much more easily and more importantly reliably.

In other languages, I would just transform the []*schemas.Transaction into []*schemas.Record with a loop or stream.Map or whatever, and return that.

Why not just return the native and pass it into your function that accepts *schemas.Record a Transaction will pass as a Record. The issue is the []. Individually Transaction supports Record.

This is one reason “generics” has take an while, like most languages it is added to after the fact; to make it into Go. covariant type parameters introduce complexity and confuse that is never worth the cost.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.