接口(interface)是一种类型,用来定义行为(方法)。golang
type Namer interface { my_method1() my_method2(para) my_method3(para) return_type ... }
但这些行为不会在接口上直接实现,而是须要用户自定义的方法来实现。因此,在上面的Namer接口类型中的方法my_methodN
都是没有实际方法体的,仅仅只是在接口Namer中存放这些方法的签名(签名 = 函数名+参数(类型)+返回值(类型)
)。c#
当用户自定义的类型实现了接口上定义的这些方法,那么自定义类型的值(也就是实例)能够赋值给接口类型的值(也就是接口实例)。这个赋值过程使得接口实例中保存了用户自定义类型实例。数据结构
例如:函数
package main import ( "fmt" ) // Shaper 接口类型 type Shaper interface { Area() float64 } // Circle struct类型 type Circle struct { radius float64 } // Circle类型实现Shaper中的方法Area() func (c *Circle) Area() float64 { return 3.14 * c.radius * c.radius } // Square struct类型 type Square struct { length float64 } // Square类型实现Shaper中的方法Area() func (s *Square) Area() float64 { return s.length * s.length } func main() { // Circle类型的指针类型实例 c := new(Circle) c.radius = 2.5 // Square类型的值类型实例 s := Square{3.2} // Sharpe接口实例ins1,它自身是指针类型的 var ins1 Shaper // 将Circle实例c赋值给接口实例ins1 // 那么ins1中就保存了实例c ins1 = c fmt.Println(ins1) // 使用类型推断将Square实例s赋值给接口实例 ins2 := s fmt.Println(ins2) }
上面将输出:指针
&{2.5} {3.2}
从上面输出结果中能够看出,两个接口实例ins1和ins2被分别赋值后,分别保存了指针类型的Circle实例c和值类型的Square实例s。code
另外,从上面赋值ins1和ins2的赋值语句上看:对象
ins1 = c ins2 := s
是否说明接口实例ins就是自定义类型的实例?实际上接口是指针类型(指向什么见下文)。这个时候,自定义类型的实例c、s称为具体实例,ins实例是抽象实例,由于ins接口中定义的行为(方法)并无具体的行为模式,而c、s中的行为是具体的。blog
由于接口实例ins也是自定义类型的实例,因此当接口实例中保存了自定义类型的实例后,就能够直接从接口上调用它所保存的实例的方法。例如:接口
fmt.Println(ins1.Area()) // 输出19.625 fmt.Println(ins2.Area()) // 输出10.24
这里ins1.Area()
调用的是Circle类型上的方法Area(),ins2.Area()
调用的则是Square类型上的方法Area()。这说明Go的接口能够实现面向对象中的多态:能够按需调用名称相同、功能不一样的方法。get
前面说了,接口类型是指针类型,可是它到底存放了什么东西?
接口类型的数据结构是2个指针,占用2个机器字长。
当将类型实例c
赋值给接口实例ins1
后,用println()
函数输出ins1和c,比较它们的地址:
println(ins1) println(c)
输出结果:
(0x4ceb00,0xc042068058) 0xc042068058
从结果中能够看出,接口实例中包含了两个地址,其中第二个地址和类型实例c的地址是彻底相同的。而第二个地址c
是Circle的指针类型实例,因此ins中的第二个值也是指针。
ins中的第一个是指针是什么?它所指向的是一个内部表结构iTable,这个Table中包含两部分:第一部分是实例c的类型信息,也就是*Circle
,第二部分是这个类型(Circle)的方法集,也就是Circle类型的全部方法(此示例中Circle只定义了一个方法Area())。
因此,如图所示:
注意,上图中的实例c是指针,是指针类型的Circle实例。
对于值类型的Square实例s
,ins2保存的内容则以下图:
实际上接口实例中保存的内容,在反射(reflect)中体现的淋漓尽致,reflect全部的一切都离不开接口实例保存的内容。
官方手册对Method Set的解释:https://golang.org/ref/spec#Method_sets
实例的method set决定了它所实现的接口,以及经过receiver能够调用的方法。
方法集是类型的方法集合,对于非接口类型,每一个类型都分两个Method Set:值类型实例是一个Method Set,指针类型的实例是另外一个Method Set。两个Method Set由不一样receiver类型的方法组成:
实例的类型 receiver -------------------------------------- 值类型:T (T Type) 指针类型:*T (T Type)或(T *Type)
也就是说:
(T Type)
组成(T Type)
和(T *Type)
这是什么意思呢?从receiver的角度去考虑:
receiver 实例的类型 --------------------------- (T Type) T 或 *T (T *Type) *T
上面的意思是:
从实现接口方法的角度上看:
(T *Type)
类型的,那么只有指针类型的实例*T
才算是实现了这个接口,由于这个方法不在值类型的实例T
方法集中(T Type)
类型的,那么值类型的实例T
和指针类型的实例*T
都算实现了这个接口,由于这个方法既在值类型的实例T
方法集中,也在指针类型的实例*T
方法集中举个例子。接口方法Area(),自定义类型Circle有一个receiver类型为(c *Circle)
的Area()方法时,说明实现了接口的方法,但只有Circle实例的类型为指针类型时,这个实例才算是实现了接口,才能赋值给接口实例,才能看成一个接口参数。以下:
package main import "fmt" // Shaper 接口类型 type Shaper interface { Area() float64 } // Circle struct类型 type Circle struct { radius float64 } // Circle类型实现Shaper中的方法Area() // receiver类型为指针类型 func (c *Circle) Area() float64 { return 3.14 * c.radius * c.radius } func main() { // 声明2个接口实例 var ins1, ins2 Shaper // Circle的指针类型实例 c1 := new(Circle) c1.radius = 2.5 ins1 = c1 fmt.Println(ins1.Area()) // Circle的值类型实例 c2 := Circle{3.0} // 下面的将报错 ins2 = c2 fmt.Println(ins2.Area()) }
报错结果:
cannot use c2 (type Circle) as type Shaper in assignment: Circle does not implement Shaper (Area method has pointer receiver)
它的意思是,Circle值类型的实例c2没有实现Share接口的Area()方法,它的Area()方法是指针类型的receiver。换句话说,值类型的c2实例的Method Set中没有receiver类型为指针的Area()方法。
因此,上面应该改为:
ins2 = &c2
再声明一个方法,它的receiver是值类型的。下面的代码一切正常。
type Square struct{ length float64 } // 实现方法Area(),receiver为值类型 func (s Square) Area() float64{ return s.length * s.length } func main() { var ins3,ins4 Shaper // 值类型的Square实例s1 s1 := Square{3.0} ins3 = s1 fmt.Println(ins3.Area()) // 指针类型的Square实例s2 s2 := new(Square) s2.length=4.0 ins4 = s2 fmt.Println(ins4.Area()) }
因此,从struct类型定义的方法的角度去看,若是这个类型的方法有指针类型的receiver方法,则只能使用指针类型的实例赋值给接口变量,才算是实现了接口。若是这个类型的方法全是值类型的receiver方法,则能够随意使用值类型或指针类型的实例赋值给接口变量。下面这两个对应关系,对于理解颇有帮助:
实例的类型 receiver -------------------------------------- 值类型:T (T Type) 指针类型:*T (T Type)或(T *Type) receiver 实例的类型 --------------------------- (T Type) T 或 *T (T *Type) *T
很常常的,咱们会直接使用推断类型的赋值方式(如ins2 := c2
)将实例赋值给一个变量,咱们觉得这个变量是接口的实例,但实际上并不必定。正如上面值类型的c2赋值给ins2,这个ins2将是从c2数据结构拷贝而来的另外一个副本数据结构,并不是接口实例,但这时经过ins2也能调用Area()方法:
c2 = Circle{3.2} ins2 := c2 fmt.Println(ins2.Area()) // 正常执行
之因此能调用,是由于Circle类型中有Area()方法,但这不是经过接口去调用的。
因此,在使用接口的时候,应当尽可能使用var先声明接口类型的实例,再将类型的实例赋值给接口实例(如var ins1,ins2 Shaper
),或者使用ins1 := Shaper(c1)
的方式。这样,若是赋值给接口实例的类型实例没有实现该接口,将会报错。
可是,为何要限制指针类型的receiver只能是指针类型的实例的Method Set呢?
看下图,假如指针类型的receiver能够组成值类型实例的Method Set,那么接口实例的第二个指针就必须找到值类型的实例的地址。但实际上,并不是全部值类型的实例都能获取到它们的地址。
哪些值类型的实例找不到地址?最多见的是那些简单数据类型的别名类型,若是匿名生成它们的实例,它们的地址就会被Go完全隐藏,外界找不到这个实例的地址。
例如:
package main import "fmt" type myint int func (m *myint) add() myint { return *m + 1 } func main() { fmt.Println(myint(3).add()) }
如下是报错信息:找不到myint(3)的地址
abc\abc.go:11:22: cannot call pointer method on myint(3) abc\abc.go:11:22: cannot take the address of myint(3)
这里的myint(3)
是匿名的myint实例,它的底层是简单数据类型int,myint(3)
的地址会被完全隐藏,只会提供它的值对象3。
对于普通方法,不管是值类型仍是指针类型的实例,都能正常调用,且调用时拷贝的内容都由receiver的类型决定。
func (T Type) method1 // 值类型receiver func (T *Type) method2 // 指针类型receiver
指针类型的receiver决定了不管是值类型仍是指针类型的实例,都拷贝实例的指针。值类型的receiver决定了不管是值类型仍是指针类型的实例,都拷贝实例自己。
因此,对于person数据结构:
type person struct {} p1 := person{} // 值类型的实例 p2 := new(person) // 指针类型的实例
p1.method1()
和p2.method1()
都是拷贝整个person实例,只不过Go对待p2.method1()
时多一个"步骤":将其解除引用。因此p2.method1()
等价于(*p2).method1()
。
p1.method2()
和p2.method2()
都拷贝person实例的指针,只不过Go对待p1.method2()
时多一个"步骤":建立一个额外的引用。因此,p1.method2()
等价于(&p1).method2()
。
而类型实现接口方法时,method set规则决定了类型实例是否实现了接口。
receiver 实例的类型 --------------------------- (T Type) T 或 *T (T *Type) *T
对于接口abc、接口方法method1()、method2()和结构person:
type abc interface { method1 method2 } type person struct {} func (T person) method1 // 值类型receiver func (T *person) method2 // 指针类型receiver p1 := abc(person) // 接口变量保存值类型实例 p2 := abc(&person) // 接口变量保存指针类型实例
p2.method1()
、p2.method2()
以及p1.method1()
都是容许的,都会经过接口实例去调用具体person实例的方法。
但p1.method2()
是错误的,由于method2()的receiver是指针类型的,致使p1没有实现接口abc的method2()方法。
将接口类型做为参数很常见。这时,那些实现接口的实例都能做为接口类型参数传递给函数/方法。
例如,下面的myArea()函数的参数是n Shaper
,是接口类型。
package main import ( "fmt" ) // Shaper 接口类型 type Shaper interface { Area() float64 } // Circle struct类型 type Circle struct { radius float64 } // Circle类型实现Shaper中的方法Area() func (c *Circle) Area() float64 { return 3.14 * c.radius * c.radius } func main() { // Circle的指针类型实例 c1 := new(Circle) c1.radius = 2.5 myArea(c1) } func myArea(n Shaper) { fmt.Println(n.Area()) }
上面myArea(c1)
是将c1做为接口类型参数传递给n,而后调用c1.Area()
,由于实现了接口方法,因此调用的是Circle的Area()。
若是实现接口方法的receiver是指针类型的,但倒是值类型的实例,将无法做为接口参数传递给函数,缘由前面已经解释过了,这种类型的实例没有实现接口。
以接口做为方法或函数的参数,将使得一切都变得灵活且通用,只要是实现了接口的类型实例,均可以去调用它。
用的很是多的fmt.Println()
,它的参数也是接口,并且是变长的接口参数:
$ go doc fmt Println func Println(a ...interface{}) (n int, err error)
每个参数都会放进一个名为a的Slice中,Slice中的元素是接口类型,并且是空接口,这使得无需实现任何方法,任何东西均可以丢到fmt.Println()中来,至于每一个东西怎么输出,那就要看具体状况:由类型的实现的String()方法决定。
接口能够嵌套,嵌套的内部接口将属于外部接口,内部接口的方法也将属于外部接口。
例如,File接口内部嵌套了ReadWrite接口和Lock接口。
type ReadWrite interface { Read(b Buffer) bool Write(b Buffer) bool } type Lock interface { Lock() Unlock() } type File interface { ReadWrite Lock Close() }
除此以外,类型嵌套时,若是内部类型实现了接口,那么外部类型也会自动实现接口,由于内部属性是属于外部属性的。