Golang接口用法疑惑解析

Golang接口用法疑惑解析 我有一个函数,当我返回一个具体的切片时(在本例中是 schemas.Transaction 类型)可以正常工作。在这段代码中,results 是一个 *schemas.Transaction 的切片。我尝试将其转换为 []*schemas.Record 但失败了(okfalse)。当执行到 return results.([]*schemas.Record), nil 时,程序会发生恐慌。

以下是该函数:

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() 返回 (interface{}, error)。 如果我将返回值类型设置为 []*schemas.Transaction,它可以正常工作。

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
}

schemas.Record 的定义:

type Record interface {
	RecordID() string
}

我希望让 findTransactions 函数更通用,以便它能处理其他模式。如果我能让这个函数工作,那么调用它的函数就可以调用 RecordID(),因为其他模式都支持这个调用。


更多关于Golang接口用法疑惑解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

感谢,看来我需要为检索每种表类型准备一个单独的函数。

更多关于Golang接口用法疑惑解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在你这里的例子中,results 是一个接口值,所以你的类型断言是在断言它“包装”了一个 []*schema.Transaction。我在这里修改了一个最小示例 Go Playground - The Go Programming Language,你可以看到,即使通过 interface{} 作为中间阶段,也不允许将 []C 类型断言为 []I,反之亦然。

Go 不进行类型转换。这是与允许类型转换(换句话说,将一个值强行塞入新类型)的其他语言之间一个微妙但重要的区别。

在 Go 中,看起来像转换的操作实际上是一种类型断言results.([]*schema.Transactions) 断言 results 的动态类型与 []*schema.Transactions 完全相同。如果断言成立,你会得到一个 ok 值,并且可以将该值视为新类型。

results.([]*schema.Transactions)

你好 @rschluet

我不完全了解你场景的全部背景,所以下面的问题可能有点傻,但是……

……如果接口只有一个方法,并且这个方法只返回记录ID,为什么不直接让 FindTransaction 返回ID值呢?


编辑补充:我猜想 FindTransaction 的调用者可能还需要通过类型开关(type switches)来获取记录数据,对吗?

如果模式(schemas)的数量很少,并且所有模式都是预先已知的,可以考虑为每个模式创建一个 FindTransactionX 函数。

对于可能存在大量模式的情况,你可能需要研究一下SQL包和ORM框架。它们也面临着同样的问题,其中一些可能已经找到了合适的解决方案。

你好,感谢你的回复。我正在学习Go语言,并且非常想理解接口是如何工作的。我可以为每个表单独写一个函数,但我更想弄明白为什么这个类型转换行不通。我原本以为,只要底层类型满足该接口(在这个例子中就是实现了 RecordID() 方法),类型转换就应该成功,并且我可以在转换后的对象上调用 .RecordID() 方法。

为了提供更多背景信息,调用这个函数的是一个试图检索所有记录的函数,但底层数据库每次获取最多只能返回 10,000 条记录。因此,我们会使用上一次检索到的最后一条记录的 ID 来定位下一批数据。底层数据库是 textile.io 的 threadsdb。

但是,如果具体类型与接口切片中的内容匹配,类型转换就能正常工作。 以下代码运行正常:

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) {

只有当尝试将其转换为不同的接口类型时,似乎才会失败。因此,答案是:只能将接口转换为其实际的具体类型,而不能转换为该具体类型所实现的接口。

接口切片不能断言为实现类型的切片,反之亦然。接口值持有对实现该接口类型的值的引用。类型断言会检查内部值是否属于所请求的类型。切片本身不是接口,因此这种类型断言无法生效,原因有二。首先,接口切片可能包含实现该接口的不同类型的值,因此需要验证切片中的每个元素。其次,接口值并不等同于实现它的值,因此需要分配一个新的切片来容纳所包含的实现值。

根据我的理解,以下是您问题的一个简化版本:Go Playground - The Go Programming Language。我还添加了一个尝试的类型转换以及一个类型断言。

另外,您可以将接口值视为包装器,而将类型断言视为解包操作。这可能有助于您理解为什么接口的容器不能简单地解包为实现类型。在 Go 中有几处地方这种包装/解包是隐式进行的,这在一定程度上模糊了心智模型。

这并不意外,Go 语言没有继承,其泛型也并非 C++ 意义上的真正“泛型”。它实际上也没有 Java 那种意义上的“类型转换”。

这之所以行不通,是因为它非常复杂且代价高昂,很少有语言支持所谓的“隐式多态”。

[]*schemas.Transaction 是一个与 []*schemas.Record 完全不同的新类型。

这很像 []Cat[]Animal 是完全不同的类型,因为你可以将 []Dog 追加到 []Animal 中,而 Dog 不是 Cat,即使两者都是 Animal

这是一个复杂的问题,因为它有一组近乎无限的约束需要机器去推理。而你作为程序员可以更容易、更重要的是更可靠地进行推理。

在其他语言中,我可能会用一个循环或 stream.Map 之类的方法将 []*schemas.Transaction 转换为 []*schemas.Record,然后返回它。

为什么不直接返回原生类型,并将其传入你那个接受 *schemas.Record 的函数呢?单个 Transaction 可以作为 Record 传递。问题在于 []。单独来看,Transaction 是支持 Record 的。

这就是“泛型”花了很长时间的原因之一,就像大多数语言一样,它是事后添加的;为了将其引入 Go。协变类型参数 引入了复杂性,并带来了混淆,其代价永远不值得。

问题在于类型断言失败,因为 results[]*schemas.Transaction 类型,而不是 []*schemas.Record 类型。虽然 *schemas.Transaction 实现了 schemas.Record 接口,但 Go 语言中 []*schemas.Transaction[]schemas.Record 是两种不同的切片类型,不能直接进行类型断言。

要解决这个问题,你需要手动将 []*schemas.Transaction 转换为 []*schemas.Record。以下是修改后的代码:

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
    }

    // 首先断言为具体的切片类型
    transactionSlice, ok := results.([]*schemas.Transaction)
    if !ok {
        return nil, fmt.Errorf("unexpected result type")
    }

    // 手动转换为接口切片
    recordSlice := make([]schemas.Record, len(transactionSlice))
    for i, tx := range transactionSlice {
        recordSlice[i] = tx
    }

    return recordSlice, nil
}

如果你希望函数更通用,可以将其改为泛型函数(Go 1.18+):

func findRecords[T schemas.Record](q *db.Query, dbUniqueIdentifier database.DBUniqueIdentifier, 
    cli *client.Client, collection string, prototype T) ([]T, error) {
    
    results, err := cli.Find(context.Background(), ThreadID(dbUniqueIdentifier),
        collection, q, prototype)
    if err != nil {
        return nil, err
    }

    recordSlice, ok := results.([]T)
    if !ok {
        return nil, fmt.Errorf("unexpected result type")
    }

    return recordSlice, nil
}

调用示例:

// 查找交易记录
transactions, err := findRecords(q, dbID, cli, 
    data.TransactionCollection.Name, &schemas.Transaction{})

// 查找其他类型的记录
otherRecords, err := findRecords(q, dbID, cli, 
    "other_collection", &schemas.OtherType{})

这样就能保持类型安全,同时让函数更通用。

回到顶部