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

什么是接口(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)

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容