什么是接口
接口(interface)是一种抽象类型,它没有暴露内部结构,所提供的仅仅是一些方法。
go语言里面的接口是隐式实现的。
对于一个类型,我们不需要知道它实现了哪个接口,只要它实现了接口的所有方法即可。
- 方法的名称必须一致
- 方法的签名必须一致
接口是go语言中的面向对象编程的多态实现方式
多态:指某一类物种都有同样的功能,但每个实例表现出来的样式不同
举例:动物都会叫,但猫叫起来是:喵喵喵,狗叫起来是:汪汪汪,但他们都会“叫”
接口定义
type 名称 interface {
方法的定义
函数名称(形参列表) 返回值
}
举例:
package main
import "fmt"
type ChefInterface interface {
Cook() bool
FavCook(foodName string) bool
}
type Chef struct {
Name string
Age int
}
func (c Chef) Cook() bool {
fmt.Println(c.Name + "的菜做好了")
return true
}
func (c Chef) FavCook(foodName string) bool {
fmt.Println(c.Name + "的拿手菜" + foodName + "做好了")
return true
}
type SeniorChef struct {
Name string
Age int
}
func (s SeniorChef) Cook() bool {
fmt.Println("高级厨师" + s.Name + "的菜做好了")
return true
}
func (s SeniorChef) FavCook(foodName string) bool {
fmt.Println("高级初始" + s.Name + "的特技料理" + foodName + "做好了")
return true
}
func main() {
li := Chef{
Name: "李",
Age: 28,
}
li.FavCook("红烧肉")
li.Cook()
wang := SeniorChef{
Name: "老王",
Age: 29,
}
var ci ChefInterface = wang
ci.Cook()
ci.FavCook("蛋炒饭")
//李的拿手菜红烧肉做好了
//李的菜做好了
//高级厨师老王的菜做好了
//高级初始老王的特技料理蛋炒饭做好了
}
非入侵式接口
在Go语言中,只要一个结构体实现了某接口中定义的所有方法,这个结构体就实现了该接口。
实现接口例子:
type ChefInterface interface {
Cook() bool
FavCook(foodName string) bool
}
type Chef struct {
...
}
func (c Chef) Cook() bool {
...
}
func (c Chef) FavCook(foodName string) bool {
...
}
实现接口后,在goland中是可以看到特殊标识的
非实现例子
type ChefInterface interface {
Cook() bool
FavCook(foodName string) bool
}
type SeniorChef struct {
...
}
func (s SeniorChef) Cook() bool {
...
}
上面例子中结构体只实现了1个接口中的方法,所以该结构体并没有实现该接口,调用时也会出现提示
这样实现接口的好处是
- 不用为了实现一个接口而导入一个包
- 学到包的时候需要再理解一下
- 想要实现一个接口,直接实现它包含的方法即可
- 在写结构体时,无需去想应该怎么实现接口设计的问题,这点在大型复杂的项目中尤为重要
- 因为interface和struct是分离的,可以先实现struct再实现interface
指针接收者实现接口
对于go语言中的类型方法中的类型接收者(python中的self)就是类型实例的本身,如果一个接收者频繁调用类型中的方法,那么会大量占用内存空间(非引用类型的传参是实参副本),如果使用指针接收者,这样就是每次都是使用接收者的地址,也就是接收者本身。
看下实例
package main
import "fmt"
type ChefInterface3 interface {
Cook() bool
FavCook(foodName string) bool
}
type Chef3 struct {
Name string
Age int
}
func (c *Chef3) Cook() bool {
fmt.Printf("当前实例的内存地址为: %p\n", c)
return true
}
func (c *Chef3) FavCook(foodName string) bool {
fmt.Printf("当前实例的内存地址为: %p\n", c)
return true
}
type SeniorChef3 struct {
Name string
Age int
}
func (s SeniorChef3) Cook() bool {
fmt.Printf("当前实例的内存地址为: %p\n", &s)
return true
}
func (s SeniorChef3) FavCook(foodName string) bool {
fmt.Printf("当前实例的内存地址为: %p\n", &s)
return true
}
func main() {
li := Chef3{
Name: "李",
Age: 28,
}
fmt.Printf("li实例地址为: %p\n", &li)
li.FavCook("红烧肉")
li.Cook()
wang := Chef3{
Name: "老王",
Age: 29,
}
fmt.Printf("wang实例地址为: %p\n", &wang)
var ci ChefInterface3 = &wang
ci.Cook()
ci.FavCook("蛋炒饭")
wang2 := SeniorChef3{
Name: "老王",
Age: 29,
}
fmt.Printf("wang2实例地址为: %p\n", &wang2)
var sci ChefInterface3 = wang2
sci.Cook()
sci.FavCook("蛋炒饭")
//li实例地址为: 0xc000004078
//当前实例的内存地址为: 0xc000004078
//当前实例的内存地址为: 0xc000004078
//wang实例地址为: 0xc000004090
//当前实例的内存地址为: 0xc000004090
//当前实例的内存地址为: 0xc000004090
//wang2实例地址为: 0xc0000040a8
//当前实例的内存地址为: 0xc0000040c0
//当前实例的内存地址为: 0xc0000040d8
}
接口的嵌套
Go语言可以使用组合的方式得到新的接口
有3中方式,他们的效果相同
-
type AInterface interface { F1() int F2() int } type BInterface interface { F3() int } type CInterface interface { AInterface BInterface } // 这种嵌入式接口,和结构体的嵌入式类似 // 这种方式最简洁
-
type CInterface interface { F1() int F2() int F3() int } // 全部方法的定义
-
type CInterface interface { F1() int F2() int BInterface } // 部分嵌套
我们可以任意组合已有的接口,或者新增接口,达到扩展的需求。
需要注意的地方
虽然说:一个类型实现了一个接口要求的所有方法,这个类型就实现了这个接口
,但是,有时候要注意方法接收者的类型。
一个类型,它的部分方法接收者是它的类型,还有部分是它的类型的指针,通过类型的变量是可以直接调用指针类型的方法,因为编译器隐式地完成了取址的操作。
package main
import "fmt"
type ChefInterface3 interface {
Cook() bool
FavCook(foodName string) bool
}
type TestInterface interface {
ChefInterface3
ThreeCook() bool
}
type Chef3 struct {
Name string
Age int
}
func (c *Chef3) Cook() bool {
fmt.Printf("Cook当前实例的内存地址为: %p\n", c)
return true
}
func (c *Chef3) FavCook(foodName string) bool {
fmt.Printf("FavCook当前实例的内存地址为: %p\n", c)
return true
}
func (c Chef3) ThreeCook() bool {
fmt.Printf("ThreeCook当前实例的内存地址为: %p\n", &c)
return true
}
func main() {
wang := Chef3{
Name: "老王",
Age: 29,
}
fmt.Printf("wang实例地址为: %p\n", &wang)
var ci TestInterface = &wang
ci.Cook()
ci.FavCook("蛋炒饭")
ci.ThreeCook()
//wang实例地址为: 0xc000004078
//Cook当前实例的内存地址为: 0xc000004078
//FavCook当前实例的内存地址为: 0xc000004078
//ThreeCook当前实例的内存地址为: 0xc000004090
}
同样是上面的结构体和接口,我们调用时使用实例来调用,就会报错
func main() {
wang := Chef3{
Name: "老王",
Age: 29,
}
fmt.Printf("wang实例地址为: %p\n", &wang)
var ci TestInterface = wang // 此时goland会在这里报红提示
ci.Cook()
ci.FavCook("蛋炒饭")
ci.ThreeCook()
//# command-line-arguments
//.\接口2.go:64:25: cannot use wang (variable of type Chef3) as type TestInterface in variable declaration:
//Chef3 does not implement TestInterface (Cook method has pointer receiver)
}
当我们定义一个结构体,里面的方法接收者类型包含了类型和指针类型时,编辑器会提示:不建议这样混用
操作接收者的属性时需要注意的地方
在使用方法操作实例的属性时,需要使用指针接收者,否则修改的是实例的副本
package main
import "fmt"
type ChefInterface2 interface {
GetHonor() string
}
type Chef30 struct {
Name string
Age int
Honor string
}
func (c Chef30) GetHonor() string {
fmt.Printf("GetHonor方法时的地址 %p \n", &c)
return c.Honor
}
func (c *Chef30) SetHonor(title string) {
fmt.Printf("SetHonor方法时的地址 %p \n", c)
c.Honor = title
}
func main() {
niu := Chef30{
Name: "牛师傅",
Age: 18,
Honor: "米其林轮胎",
}
fmt.Printf("实例niu 的地址 %p \n", &niu)
fmt.Println(niu.GetHonor())
var ci ChefInterface2 = niu
fmt.Printf("接口ci 的地址 %p \n", &ci)
niu.SetHonor("普通轮胎")
fmt.Println(niu.GetHonor())
fmt.Println(ci.GetHonor())
}
实例niu 的地址 0xc0000c0450
GetHonor方法时的地址 0xc0000c0480
米其林轮胎
接口ci 的地址 0xc000088230
SetHonor方法时的地址 0xc0000c0450
GetHonor方法时的地址 0xc0000c04e0
普通轮胎
GetHonor方法时的地址 0xc0000c0510
米其林轮胎
接口值
一个接口类型的值由两部分组成
- 具体类型(动态类型)
- 类型的值(动态值)
在Go语言中,类型仅仅是编译时的概念,所以类型不是一个值。
类型描述符(string、int)可以提供每个类型的具体信息。对于接口类型的值,类型部分一般用对应的类型描述符来表达。
如果接口类型的值为nil,那么这个接口的动态类型为nil,如果为nil时调用接口里面的方法会报错,所以使用前需要判断一下是否为nil。
接口的类型是动态的,取决于运行时赋给接口的类型
var ci TestInterface = &wang
fmt.Printf("%T\n", ci)
// *main.Chef3
var ci2 TestInterface
fmt.Printf("%T\n", ci2)
// <nil>
ci2.ThreeCook() // 对于动态值为nil的接口调用方法会报错
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0xcdcd76]
goroutine 1 [running]:
main.main()
C:/Users/49273/Documents/Study/go-study/project/接口2.go:69 +0x116
error接口
在Go语言中error接口定义如下:
type error interface {
Error() string
}
type errorString struct { // 定义了一个结构体
s string // 内部成员
}
func New(text string) error {
return &errorString{text}
}
// 实现Error方法的是*errorString指针,不是类型errorString,其目的是:在调用New函数时分配的error实例变量都不相等
func (e *errorString) Error() string{
return e.s
}
类型断言
可以使用i.(Type)
来检查接口是否属于某个类型,但非接口类型不能做类型断言
-
Type为具体类型时
-
检查成功:v=类型,ok=true
-
检查失败:v=空类型,ok=false
-
-
Type为接口类型时
- 检查成功:v=接口动态类型,ok=true
- 检查失败:v=nil,ok=false
package main
import (
"fmt"
)
type Interface1 interface {
F1() int
}
type Interface2 interface {
F2() string
}
type s12 struct {
Name string
}
func (s s12) F1() int {
return 1
}
type s13 struct {
Name string
}
func (s s13) F1() int {
return 1
}
func main() {
var ti Interface1
ti = s12{Name: "lilei"}
value, ok := ti.(s12)
fmt.Println(value, ok)
//{lilei} true
value1, ok1 := ti.(s13)
fmt.Println(value1, ok1)
//{} false
value2, ok2 := ti.(Interface1)
fmt.Println(value2, ok2)
//{lilei} true
value3, ok3 := ti.(Interface2)
fmt.Println(value3, ok3)
//<nil> false
}
类型分支
空接口可以保存任何类型的值。
package main
import "fmt"
func SayHi(i interface{}) {
fmt.Printf("%v, %T \n", i, i)
}
func main() {
var i interface{}
SayHi(i)
//<nil>, <nil>
i = 77
SayHi(i)
//77, int
i = "阿里巴巴"
SayHi(i)
//阿里巴巴, string
}
我们可以使用switch分支来根据不同的类型断言结果执行不同的业务逻辑
var x interface{}
x = "abc"
switch x.(type) {
case string:
fmt.Println("string")
case int:
fmt.Println("int")
}
动态类型、动态值、静态类型
上面我们把一个ci变量初始化成了接口类型,下面用到这个的时候简称ci
当我们给ci变量赋值类型时,赋给ci变量的值就是动态值,值的类型就是动态类型。
由于这些是在运行时才能确定的,所以也叫实际值和实际类型
对于ci变量来说它的静态类型永远是初始化时的接口类型,但是动态类型会随着动态值得变化而变化。如果还有其他类型(如Interface2)实现了接口方法,并赋值给了变量ci,那么它得动态类型就是*Interface2
go的多态
我们声明一个接口时,只是对一个方法进行了声明,由结构体去具体实现这个接口,而每个对象都有处理这个方法的不同的逻辑,从而达到多态的效果。
多态的优点优点:
- 灵活:每个对象都实现了相同接口的方法,从调用者的角度来看,只要提供不同的对象,就可以调用同样的方法,实现不同的业务逻辑操作,提高使用效率。
- 简化:多态简化了编写和修改代码的过程,尤其在处理大量对象的运算和操作时,这点非常重要。
评论区