golang reflect反射(二):interface接口的理解

陈卧虫 · · 1083 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

什么是接口(interface)

接口(interface)首先来说它是一种数据类型,里面存的数据是一组方法的集合,它只关心所包含的方法,不关心属性,也就是说属性是被它所忽略掉的。只要一个对象实现了接口定义的所有方法,那么这个对象就实现了这个接口。有点绕,举个例子:

// 定义一个接口,里面有一个Print方法
type MyInterface interface{
    Print()
}

// 定义一个结构体
type MyStruct struct {   
}

// 实现Print方法
func (me MyStruct) Print() {
    doSomething()
}

// 定义一个参数为MyInterface接口的函数
func TestFunc(x MyInterface) {
    fmt.Println(x)
}

func main() {
    var me MyStruct
    // 因为MyStruct实现了MyInterface定义的所有方法,它就实现了MyInterface接口
    TestFunc(me)  //value
    
    var y MyInterface // 定义MyInterface接口
    var m MyStruct      
    
    y = m    // 因为MyStruct实现了MyInterface定义的所有方法,所以可以转换
}

从上面的例子可以看出,接口重点是方法,只要实现了定义的方法就可以调用,换一种数据类型仍然可以。

// 定义一个接口
type MyInterface interface{
    Print()
}
// 定义一个新的数据类型
type myStr string
// 实现Print方法
func (s myStr) Print() {
    doSomething2()
}

func TestFunc(x MyInterface) {
    fmt.Println(x)
}

func main() {
    var s myStr
    // 它也可以被传入当做参数
    TestFunc(s)
}

现在应该有所理解interface的重点了,interface其实就是一种抽象类型,它是针对具体的类型,如int、map、slice或者自己定义的类型。具体类型是有具体的值,具体的类型,具体的方法的实现。但接口只是定义包含的方法,这些方法并不是由接口直接实现的,而是通过用户定义的类型来实现该方法,比如上面的例子中的MyStruct,myStr。

存在既有道理,接口的优势就是通用,

这个像是动态语言里的“鸭子类型”,一个对象只要”看起来像鸭子,走起来像鸭子“,那么它就可以被看成是鸭子。

// 定义一个鸭子接口
type Duck interface{
    Walk()
}

// 定义一个猪结构体
type Pig struct {
}
// 实现Walk方法
func (p Pig) Walk() {
    fmt.Println("pig walk")
}

// 定义一个狗结构体
type Dog struct {
}
// 实现Walk方法
func (d Dog) Walk() {
    fmt.Println("dog walk")
}

// 再定义一个必须传入鸭子作为参数的函数
func DuckWalk(x Duck) {
    x.Walk()
}

func main() {
    var p Pig 
    var d Dog

    // 猪和够都可以被传入当做参数
    DuckWalk(p)   //正常
    DuckWalk(d)   //正常
}

这就是鸭子类型的优点,只要实现了定义的方法就能够调用,并不要考虑具体的方法实现是否一样。

一个函数的传入参数如果被规定为一种具体类型,那么你就只能传入该类型的数据作为参数。再定义一个函数PigWalk,参数是Pig结构体类型:

func PigWalk(p Pig)  {
    p.Walk()
}

如果你传入类型不是Pig, 就会报错:

func main() {
    var d Dog

    PigWalk(d)

}

# command-line-arguments
.\exe_time.go:37:9: cannot use d (type Dog) as type Pig in argument to PigWalk

判断interface存储的变量是哪种类型

一个对象转换成一个接口时,会失去它原来的类型,go提供了一种判断方式,断言:value, ok := em.(T)em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示此次言断是否成功

var i interface{}
var s string
// 将s转换为一个空接口类型
i = s

if v, ok := i.(string); ok {
    fmt.Println(i, "is string type")
} else {
    fmt.Println(i, "isn't string type")
}

ok是true表示i存储的是string类型,false则不是,这就是类型言断(Type assertions)

如果需要区分多种类型,可以使用switch断言,能够一次性区分多种类型,但是只能在switch中使用:

switch t := i.(type) {
case string:
    fmt.Println("i store string", t)
case int:
    fmt.Println("i store int", t)
}

空接口

不带任何方法的interface就是一个空接口:empty interface

type empty interface{}

因为空接口不带任何方法,那就是它没有要求 ,既然没有要求那么所有的方法都可以啦,因此所有的类型实现了空接口。

举例:我们常用的fmt.println()函数参数就是空接口,任何类型都可以传入:

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}

既然empty interface可以接受任何类型的参数,空接口的slice([]interface{})是否也可以接受任何类型的slice呢?试一下吧

func printSlice(ins []interface{}) { 
    for _, in := range ins {
        fmt.Println(in)
    }
}

func main(){
    names := []string{"chen", "wo", "chong"}
    printSlice(names)
}

结果:

# command-line-arguments
.\test_interface_slice.go:13:12: cannot use names (type []string) as type []interface {} in argument to printSlice

报错了,行不通,说明没有帮助我们自动把 slice 转换成 interface{} 类型的 slice,所以出错了。go 不会对 类型是interface{} 的 slice 进行转换 。为什么 go 不帮我们自动转换,一开始我也很好奇,最后终于在 go 的 wiki 中找到了答案 https://github.com/golang/go/wiki/InterfaceSlice 大意是 interface{} 会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的N*2,但是 []T 的长度是N*sizeof(T),两种 slice 实际存储值的大小是有区别的(文中只介绍两种 slice 的不同,至于为什么不能转换猜测可能是 runtime 转换代价比较大)。

但是我们可以手动进行转换来达到我们的目的。

var dataSlice []int = []int{1,2,3,4,5,6}
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice)) // 创建该长度的[]interface{}
// 手动遍历
for i, d := range dataSlice {
    interfaceSlice[i] = d
}

接口的接收者

什么是接收者,在实现一个接口时,一个对象必须要实现接口提供的所有方法,而实现了所有方法的对象就可以称为方法的接收者( receiver )。

func main() {
    
    var a animal
    
    var p pig
    a=p
    a.Show()
    
    //使用另外一个类型赋值
    var d dog
    a=d
    a.Show()
}

type animal interface {
    Show()
}

type pig int
type dog int

func (p pig) Show(){
    fmt.Println("papapa")
}

func (d dog) Show(){
    fmt.Println("wanwan")
}

实现方法的时候,可以通过对象的指针实现,也可以通过对象的值来实现,所以方法的接受者有两种,指针接收者(pointer receiver),值接收者(value receiver)。

type Sheep int
// 值接收者
func (s Sheep) Show(){
    fmt.Println("mi-1")
}
// 指针接收者
func (s *Sheep) Show(){
    fmt.Println("mi-2")
}

它们两个还是有区别的,

如果对象通过value实现了该方法, 那就是这个对象的值实现了这个接口;

如果是对象通过pointer实现了该方法,那就是这个对象的指针实现了这个接口;

receiver是pointer

type animal interface {
    Show()
}

type bird int
// 指针接收者
func (b *bird) Show() {
    fmt.Println("bird")
}

func ShowFunc(a animal)  {
    a.Show()
}

用对象的pointer调用, 结果正常

func main() {
    var b bird
    ShowFunc(&b) 
}

但用对象的value调用,就会报错

ShowFunc(b) 
# command-line-arguments
.\haha.go:7:10: cannot use b (type bird) as type animal in argument to ShowFunc:
    bird does not implement animal (Show method has pointer receiver)

receiver是value

type animal interface {
    Show()
}

type cat int
// 指针接收者
func (c cat) Show() {
    fmt.Println("cat")
}

func ShowFunc(a animal)  {
    a.Show()
}

func main() {
    var c cat
    ShowFunc(c) // value
    ShowFunc(&c) // pointer
}

值接收者( value receiver)实现接口,无论是 pointer 还是 value 都可以正确执行。

为什么呢?

在go执行过程中,如果是按 pointer 调用,go 会自动进行转换,因为有了pointer 总是能得到指针指向的value 是什么;如果是 value 调用,go 将无从得知 value 的原始值是什么,因为 value 是份拷贝,它在内存里的地址已经变化了。go 会把指针进行隐式转换得到 value,但反过来则不行

通过这个例子我们可以得出结论:

Methods Receivers Values
(t T) T and *T
(t *T) *T

实体类型以值接收者实现接口的时候,不管是实体类型的值,还是实体类型值的指针,都实现了该接口

实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口。

其次我们我们以实体类型是值还是指针的角度看。

Values Methods Receivers
T (t T)
*T (t T) and (t *T)

上面的表格可以解读为:类型的值只能实现值接收者的接口;指向类型的指针,既可以实现值接收者的接口,也可以实现指针接收者的接口。

如果觉得对您有所帮助的话,点个赞呗~,让我能够有写下去的动力。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:陈卧虫

查看原文:golang reflect反射(二):interface接口的理解

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1083 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传