目 录CONTENT

文章目录

Go学习系列13-接口

cplinux98
2022-11-19 / 0 评论 / 0 点赞 / 987 阅读 / 3,121 字 / 正在检测是否收录...

什么是接口

接口(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中是可以看到特殊标识的

image-20221116110839310

非实现例子

type ChefInterface interface {
	Cook() bool
	FavCook(foodName string) bool
}

type SeniorChef struct {
...
}

func (s SeniorChef) Cook() bool {
...
}

上面例子中结构体只实现了1个接口中的方法,所以该结构体并没有实现该接口,调用时也会出现提示

image-20221116110957819

这样实现接口的好处是

  • 不用为了实现一个接口而导入一个包
    • 学到包的时候需要再理解一下
  • 想要实现一个接口,直接实现它包含的方法即可
  • 在写结构体时,无需去想应该怎么实现接口设计的问题,这点在大型复杂的项目中尤为重要
    • 因为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)
}

当我们定义一个结构体,里面的方法接收者类型包含了类型和指针类型时,编辑器会提示:不建议这样混用

image-20221116142826251

操作接收者的属性时需要注意的地方

在使用方法操作实例的属性时,需要使用指针接收者,否则修改的是实例的副本

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的多态

我们声明一个接口时,只是对一个方法进行了声明,由结构体去具体实现这个接口,而每个对象都有处理这个方法的不同的逻辑,从而达到多态的效果。

多态的优点优点:

  • 灵活:每个对象都实现了相同接口的方法,从调用者的角度来看,只要提供不同的对象,就可以调用同样的方法,实现不同的业务逻辑操作,提高使用效率。
  • 简化:多态简化了编写和修改代码的过程,尤其在处理大量对象的运算和操作时,这点非常重要。
0

评论区