• 发生时间:2022-09-09

  • 问题描述:在开发产线平台时,想将一个结构体的 slice 赋值给 interface slice,结果编译报错。(考虑到简洁性,下面使用示意代码)

    package main
    
    import "fmt"
    
    // Named represents anything that has a name.
    type Named interface {
        GetName() string
    }
    
    // Person has a name
    type Person struct {
        Name string
        Addr string
        Age  int
    }
    
    // GetName gets the name of a person.
    func (p Person) GetName() string {
        return p.Name
    }
    
    // showNames shows the names of a group.
    func showNames(names []Named) {
        for _, n := range names {
            fmt.Println(n.GetName())
        }
    }
    
    func main() {
        persons := []Person{Person{"guojing", "damo", 20}, Person{"huangrong", "taohuadao", 16}}
        showNames(persons)
    }
    
    $ go run named.go
    ./named.go:31:11: cannot use persons (type []Person) as type []Named in argument to showNames
    
  • 问题类别:软件开发

  • 原因分析:

    • 按照我的直觉,既然我们可以将 Person 类型的变量赋值给了 Named 变量,那么 []Person 类型的变量也应该可以赋值给 []Named 变量。
    • 根据编译器的报错信息,[]Person 赋值给 []Named 是不被允许的,这说明我的直觉是错误的。
    • 搜索网上资料,在 stack overflow QA1QA2 找到解释:
      • []Person[]Named 的内存布局不一样,这是因为 PersonNamed 的内存布局不一样: 前者的长度取决于其包含的字段,后者的长度固定为 2 words。
      • 如果要将 []Person 转换为 []Named,需要遍历 slice 的每个元素进行转换,其时间复杂度为 O(n)。
      • 由于其时间复杂度为 O(n),并且会导致创建一个新的 slice, Go 语言不允许隐式进行这种转换。
    • 此外,Google Group 从设计哲学、内部实现等方面讨论了这个问题,博客 Go Data Structures: Interfaces 则介绍了 interface{} 的实现原理。 阅读这些资料可以加深对这个问题的理解。
  • 解决方案:将 showNames 的输入参数类型改为 interface{},并使用 reflect 来获取其原来的类型。

    package main
    
    import (
        "fmt"
        "reflect"
    )
    
    // Named represents anything that has a name.
    type Named interface {
        GetName() string
    }
    
    // Person has a name
    type Person struct {
        Name string
        Addr string
        Age  int
    }
    
    // GetName gets the name of a person.
    func (p Person) GetName() string {
        return p.Name
    }
    
    // showNames shows the names of a group.
    func showNames(names interface{}) {
        if reflect.TypeOf(names).Kind() != reflect.Slice {
            return
        }
    
        v := reflect.ValueOf(names)
        for i := 0; i < v.Len(); i++ {
            if val, ok := v.Index(i).Interface().(Named); ok {
                fmt.Println(val.GetName())
            }
        }
    }
    
    func main() {
        persons := []Person{Person{"guojing", "damo", 20}, Person{"huangrong", "taohuadao", 16}}
        showNames(persons)
    }
    
  • 实施结果:程序可以编译通过,并正常运行。

    $ go run named.go
    guojing
    huangrong
    
  • 经验总结:

    • Go 语言中,不能将结构体 slice 赋值给 interface slice。
    • 如果要实现相关功能,Go 1.18 以前,可以使用 reflect 机制;Go 1.18 以后,可以使用泛型。

参考资料