Golang之接口详细讲解

学习笔记之Golang基础之接口



前言

在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型。因为接口类型与其他数据类型不同,它是没法被实例化的。更具体地说,我们既不能通过调用new函数或make函数创建出一个接口类型的值,也无法用字面量来表示一个接口类型的值。对于某一个接口类型来说,如果没有任何数据类型可以作为它的实现,那么该接口的值就不可能存在。


一、golang中的鸭子类型

go 复制代码
type Pet interface {
	SetName(name string)
	Name() string
	Category() string
}

这里声明了一个接口类型Pet,它包含了 3 个方法定义,方法名称分别为SetName、Name和Category。这 3 个方法共同组成了接口类型Pet的方法集合。只要一个数据类型的方法集合中有这 3 个方法,那么它就一定是Pet接口的实现类型。这是一种无侵入式的接口实现方式 。这种方式还有一个专有名词,叫"Duck typing ",中文常译作"鸭子类型"。

二、* 怎么判断一个数据类型是一个接口的实现?

1.两个充要条件:

一个是"两个方法的签名需要完全一致 ",另一个是"两个方法的名称要一模一样"。

2.代码讲解

代码如下(示例):

go 复制代码
type Pet interface {
	SetName(name string)
	Name() string
	Category() string
}

type Dog struct {
	name string // 名字。
}

func (dog *Dog) SetName(name string) {
	dog.name = name
}

func (dog Dog) Name() string {
	return dog.name
}

func (dog Dog) Category() string {
	return "dog"


func main() {
	// 示例1。
	dog := Dog{"little pig"}
	_, ok := interface{}(dog).(Pet)
	fmt.Printf("Dog implements interface Pet: %v\n", ok)
	_, ok = interface{}(&dog).(Pet)
	fmt.Printf("*Dog implements interface Pet: %v\n", ok)
	fmt.Println()

	// 示例2。
	var pet Pet = &dog
	fmt.Printf("This pet is a %s, the name is %q.\n",
		pet.Category(), pet.Name())
}
}

运行结果为:

go 复制代码
Dog implements interface Pet: false
*Dog implements interface Pet: true

This pet is a dog, the name is "little pig".

我声明的类型Dog附带了 3 个方法。其中有 2 个值方法,分别是Name和Category,另外还有一个指针方法SetName。 这就意味着:Dog类型本身的方法集合中只包含了 2 个方法,也就是所有的值方法。而它的指针类型*Dog方法集合却包含了 3 个方法 ,也就是说,它拥有Dog类型附带的所有值方法和指针方法。又由于这 3 个方法恰恰分别是Pet接口中某个方法的实现,所以**Dog类型就成为了Pet接口的实现类型。*


三:接口变量的动态值、动态类型、静态类型都是什么?

对于一个接口类型的变量来说,例如上面的变量pet

go 复制代码
var pet Pet = &dog

我们赋给它的值可以被叫做它的动态值 ,而该值的类型可以被叫做这个变量的动态类型 比如:

我们把取址表达式&dog的结果值赋给了变量pet,这时这个结果值就是变量pet的动态值 , 而此结果值的类型*Dog就是该变量的动态类型

动态类型这个叫法是相对于静态类型而言的。 对于变量pet来讲,它的静态类型就是Pet,并且永远是Pet, 但是它的动态类型却会随着我们赋给它的动态值而变化。

比如,只有我把一个Dog类型的值赋给变量pet之后,该变量的动态类型才会是 Dog。如果还有一个Pet接口的实现类型Fish,并且我又把一个此类型的值赋给了pet,那么它的动态类型就会变为Fish。还有,在我们给一个接口类型的变量赋予实际的值之前,它的动态类型是不存在的。

四:当为一个接口变量赋值会发生什么???

还是接上上一个问题的demo代码再对两个类型进行以下操作

go 复制代码
dog := Dog{"little pig"}
var pet Pet = dog
dog.SetName("monster")

那么问题就来了:在以上代码执行后,pet变量的字段name的值会是什么?

经典答案:仍然是little pig 解析:

理由1:

go 复制代码
func (dog *Dog) SetName(name string) {
	dog.name = name
}

由于dog的SetName方法是指针方法,所以该方法持有的接收者就是指向dog的指针值的副本,因而其中对接收者的name字段的设置就是对变量dog的改动。那么当dog.SetName("monster")执行之后,dog的name字段的值就一定是"monster"。

为什么dog的name字段值变了,而pet的却没有呢?这里有一条通用的规则:如果我们使用一个变量给另外一个变量赋值,那么真正赋给后者的,并不是前者持有的那个值,而是该值的一个副本。 再举一个类似例子:

go 复制代码
dog1 := Dog{"little pig"}
dog2 := dog1
dog1.name = "monster"

这时的dog2的name仍然会是"little pig"。 这也是刚刚那条规则而体现:如果我们使用一个变量给另外一个变量赋值,那么真正赋给后者的,并不是前者持有的那个值,而是该值的一个副本。

但是 如果重新给Pet接口加上了SetName()方法,然后让*Dog类型实现了该Pet接口,然后声明并初始化了一个d,将d的地址&d赋值给Pet类型的接口变量:

go 复制代码
d := Dog{name: "little dog"}
var pet Pet = &d
d.SetName("big dog")

运行后发现输出不仅d的name字段变为了"big dog",同样pet接口变量也变成了"big dog"。 在此时我是不是可以说,传递给pet变量的同样是&d的一个指针副本,因为传递的是副本,所以无论是指针还是值,都可以说是浅复制;且由于传递的是指针(虽然是副本),但还是会对指向的底层变量做修改。

理由2:

这就需要从接口类型值的存储方式和结构说起了: 接口类型本身是无法被值化的。在我们赋予它实际的值之前,它的值一定会是nil,这也是它的零值。

反过来讲,一旦它被赋予了某个实现类型的值,它的值就不再是nil了。不过要注意,即使我们像前面那样把dog的值赋给了pet,pet的值与dog的值也是不同的。这不仅仅是副本与原值的那种不同。当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中。严格来讲,这样一个变量的值其实是这个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值。所以我才说,pet的值与dog的值肯定是不同的,无论是从它们存储的内容,还是存储的结构上来看都是如此。不过,我们可以认为,这时pet的值中包含了dog值的副本。我们就把这个专用的数据结构叫做iface吧,在 Go 语言的runtime包中它其实就叫这个名字。 iface的实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。 这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径,等等

总之:接口变量被赋予动态值的时候,存储的是包含了这个动态值的副本的一个结构更加复杂的值。 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

五:拓展内容:

问题 1:接口变量的值在什么情况下才真正为nil?

先看这样一段代码

go 复制代码
var dog1 *Dog
fmt.Println("The first dog is nil. [wrap1]")
dog2 := dog1
fmt.Println("The second dog is nil. [wrap1]")
var pet Pet = dog2
if pet == nil {
  fmt.Println("The pet is nil. [wrap1]")
} else {
  fmt.Println("The pet is not nil. [wrap1]")
}

我先声明了一个*Dog类型的变量dog1,并且没有对它进行初始化。这时该变量的值是什么?显然是nil。 然后我把该变量赋给了dog2,后者的值此时也必定是nil,对吗?

现在问题来了:当我把dog2赋给Pet类型的变量pet之后,变量pet的值会是什么?答案是nil吗?

显然不会是的,当我们把dog2的值赋给变量pet的时候,dog2的值会先被复制,不过由于在这里它的值是nil,所以就没必要复制了。然后,Go 语言会用我上面提到的那个专用数据结构iface的实例包装这个dog2的值的副本,这里是nil。虽然被包装的动态值是nil,但是pet的值却不会是nil,因为这个动态值只是pet值的一部分而已。

所以总之: 要么只声明它但不做初始化,要么直接把字面量nil赋给它。这样才能让它与nil相等

问题 2:怎样实现接口之间的组合?

先看代码

go 复制代码
type Animal interface {
  ScientificName() string
  Category() string
}

type Pet interface {
  Animal
  Name() string
}

与结构体类型间的嵌入很相似,我们只要把一个接口类型的名称直接写到另一个接口类型的成员列表中就可以了

**对此关于程序设计方面的。用好小接口和接口组合总是有益的,我们可以以此形成接口矩阵,进而搭起灵活的程序框架。如果在实现接口时再配合运用结构体类型间的嵌入手法,那么接口组合就可以发挥更大的效用。

六:总结重点知识:

弄清楚的是,接口变量的动态值、动态类型和静态类型都代表了什么。这些都是正确使用接口变量的基础。当我们给接口变量赋值时,接口变量会持有被赋予值的副本,而不是它本身。

接口变量的值并不等同于这个可被称为动态值的副本。它会包含两个指针,一个指针指向动态值,一个指针指向类型信息。即文中所说的**"iface"**

除非我们只声明而不初始化,或者显式地赋给它nil,否则接口变量的值就不会为nil。

相关推荐
Vfw3VsDKo2 小时前
Maui 实践:Go 接口以类型之名,给 runtime 传递方法参数
开发语言·后端·golang
是真的小外套3 小时前
第十五章:XXE漏洞攻防与其他漏洞全解析
后端·计算机网络·php
ybwycx5 小时前
SpringBoot下获取resources目录下文件的常用方法
java·spring boot·后端
小陈工5 小时前
Python Web开发入门(十一):RESTful API设计原则与最佳实践——让你的API既优雅又好用
开发语言·前端·人工智能·后端·python·安全·restful
小阳哥AI工具5 小时前
Seedance 2.0使用真人参考图生成视频的方法
后端
IeE1QQ3GT6 小时前
使用ASP.NET Abstractions增强ASP.NET应用程序的可测试性
后端·asp.net
Full Stack Developme6 小时前
SpringBoot多线程池配置
spring boot·后端·firefox
sxhcwgcy8 小时前
SpringBoot 使用 spring.profiles.active 来区分不同环境配置
spring boot·后端·spring
稻草猫.10 小时前
Spring事务操作全解析
java·数据库·后端·spring
希望永不加班10 小时前
SpringBoot 整合 MongoDB
java·spring boot·后端·mongodb·spring