建设部网站官网建筑施工合同,胶州做淘宝的网站,免费创建属于自己的网站,WordPress 输入任意作者接口类型的类型字面量与结构体类型的看起来有些相似#xff0c;它们都用花括号包裹一些核心信息。只不过#xff0c;结构体类型包裹的是它的字段声明#xff0c;而接口类型包裹的是它的方法定义。
接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集…接口类型的类型字面量与结构体类型的看起来有些相似它们都用花括号包裹一些核心信息。只不过结构体类型包裹的是它的字段声明而接口类型包裹的是它的方法定义。
接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集合就是它的全部特征。
对于任何数据类型只要它的方法集合中完全包含了一个接口的全部特征即全部的方法那么它就一定是这个接口的实现类型。比如下面这样
type Pet interface {SetName(name string)Name() stringCategory() string
}上面代码声明了一个接口类型Pet它包含了 3 个方法定义方法名称分别为SetName、Name和Category。这 3 个方法共同组成了接口类型Pet的方法集合。
只要一个数据类型的方法集合中有这3个方法那么它就一定是Pet接口的实现类型。这是一种无侵入式的接口实现方式。这种方式还有一个专有名词叫Duck typing中文常译作鸭子类型。
怎样判定一个数据类型的某一个方法实现的就是某个接口类型中的某个方法呢
这有两个充分必要条件一个是“两个方法的签名需要完全一致”另一个是“两个方法的名称要一模一样”。显然这比判断一个函数是否实现了某个函数类型要更加严格一些。
type Pet interface {SetName(name string)Name() stringCategory() 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
}上面代码声明的类型Dog附带了3个方法。其中有两个值方法分别是Name和Category,另外还有一个指针方法SetName
也就是说它拥有Dog类型附带的所有值方法和指针方法。又由于这 3 个方法恰恰分别是Pet接口中某个方法的实现所以*Dog类型就成为了Pet接口的实现类型。
dog : Dog{little pig}
var pet Pet dog正因为如此可以声明并初始化一个Dog类型的变量dog然后把它的指针值赋给类型为Pet的变量pet。
对于一个接口类型的变量来说例如上面的变量pet我们赋给它的值可以被叫做它的实际值也称动态值而该值的类型可以被叫做这个变量的实际类型也称动态类型。
动态类型这个叫法是相对于静态类型而言的。对于变量pet来讲它的静态类型就是Pet并且永远是Pet但是它的动态类型却会随着我们赋给它的动态值而变化。 比如只有把一个 * Dog类型的值赋给变量pet之后该变量的动态类型才会是 * Dog。如果还有一个Pet接口的实现 类型 * Fish并且我又把一个此类型的值赋给了pet那么它的动态类型就会变为* Fish。 在我们给一个接口类型的变量赋予实际的值之前它的动态类型是不存在的。 当我们为一个接口变量赋值时会发生什么
为了突出问题我把Pet接口的声明简化了一下。
type Pet interface {Name() stringCategory() 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
}现在我先声明并初始化了一个Dog类型的变量dog这时它的name字段的值是littlepig。然后我把该变量赋给了一个Pet类型的变量pet。最后我通过调用dog的方法SetName把它的name字段的值改成了monster。
dog : Dog{little pig}
var pet Pet dog
dog.SetName(monster)在以上代码执行后pet变量的字段name的值会是什么
答案是pet变量的字段name的值依然是little pig。
问题解析
首先由于dog的SetName方法是指针方法所以该方法持有的接收者就是指向dog的指针值的副本因而其中对接收者的name字段的设置就是对变量dog的改动。那么当dog.SetName(“monster”)执行之后dog的name字段的值就一定是monster。如果你理解到了这一层那么请小心前方的陷阱。
为什么dog的name字段值变了而pet的却没有呢这里有一条通用的规则需要你知晓如果我们使用一个变量给另外一个变量赋值那么真正赋给后者的并不是前者持有的那个值而是该值的一个副本。
例如我声明并初始化了一个Dog类型的变量dog1这时它的name是little pig。然后我在把dog1赋给变量dog2之后修改了dog1的name字段的值。这时dog2的name字段的值是什么
dog1 : Dog{little pig}
dog2 : dog1
dog1.name monster答案是这时的dog2的name仍然会是little pig。
当你知道了这条通用规则之后确实可以把前面那道题做对。不过如果当我问你为什么的时候你只说出了这一个原因那么我只能说你仅仅答对了一半。
另外一半就需要从接口类型值的存储方式和结构说起了。我在前面说过接口类型本身是无法被值化的。在我们赋予它实际的值之前它的值一定会是nil这也是它的零值。
一旦它被赋予了某个实现类型的值它的值就不再是nil了。不过要注意即使我们像前面那样把dog的值赋给了petpet的值与dog的值也是不同的。这不仅仅是副本与原值的那种不同。
当我们给一个接口变量赋值的时候该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中。
严格来讲这样一个变量的值其实是这个专用数据结构的一个实例而不是我们赋给该变量的那个实际的值。所以我才说pet的值与dog的值肯定是不同的无论是从它们存储的内容还是存储的结构上来看都是如此。不过我们可以认为这时pet的值中包含了dog值的副本。
我们就把这个专用的数据结构叫做iface吧在 Go 语言的runtime包中它其实就叫这个名字。
iface的实例会包含两个指针一个是指向类型信息的指针另一个是指向动态值的指针。这里的类型信息是由另一个专用数据结构的实例承载的其中包含了动态值的类型以及使它实现了接口的方法和调用它们的途径等等。
总之接口变量被赋予动态值的时候存储的是包含了这个动态值的副本的一个结构更加复杂的值。
接口变量的值在什么情况下才真正为nil
先看段代码 var dog1 *Dogfmt.Println(The first dog is nil. [wrap1])dog2 : dog1fmt.Println(The second dog is nil. [wrap1])var pet Pet dog2if 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值的一部分而已。
便说一句这时的pet的动态类型就存在了是 * Dog。我们可以通过fmt.Printf函数和占位符%T来验证这一点另外reflect包的TypeOf函数也可以起到类似的作用。
我们把nil赋给了pet但是pet的值却不是nil。
这很奇怪对吗其实不然。在 Go 语言中我们把由字面量nil表示的值叫做无类型的nil。这是真正的nil因为它的类型也是nil的。虽然dog2的值是真正的nil但是当我们把这个变量赋给pet的时候Go 语言会把它的类型和值放在一起考虑。
这时 Go 语言会识别出赋予pet的值是一个 * Dog类型的nil。然后Go 语言就会用一个iface的实例包装它包装后的产物肯定就不是nil了。
只要我们把一个有类型的nil赋给接口变量那么这个变量的值就一定不会是那个真正的nil。因此当我们使用判等符号判断pet是否与字面量nil相等的时候答案一定会是false。
那么怎样才能让一个接口变量的值真正为nil呢要么只声明它但不做初始化要么直接把字面量nil赋给它。
怎样实现接口之间的组合
接口类型间的嵌入也被称为接口的组合。我在前面讲过结构体类型的嵌入字段这其实就是在说结构体类型间的嵌入。
接口类型间的嵌入要更简单一些因为它不会涉及方法间的“屏蔽”。只要组合的接口之间有同名的方法就会产生冲突从而无法通过编译即使同名方法的签名彼此不同也会是如此。因此接口的组合根本不可能导致“屏蔽”现象的出现。
与结构体类型间的嵌入很相似我们只要把一个接口类型的名称直接写到另一个接口类型的成员列表中就可以了。比如
type Animal interface {ScientificName() stringCategory() string
}
type Pet interface {AnimalName() string
}接口类型Pet包含了两个成员一个是代表了另一个接口类型的Animal一个是方法Name的定义。它们都被包含在Pet的类型声明的花括号中并且都各自独占一行。此时Animal接口包含的所有方法也就成为了Pet接口的方法。
Go 语言团队鼓励我们声明体量较小的接口并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。 这是因为相比于包含很多方法的大接口而言小接口可以更加专注地表达某一种能力或某一类特征同时也更容易被组合在一起。
Go 语言标准库代码包io中的ReadWriteCloser接口和ReadWriter接口就是这样的例子它们都是由若干个小接口组合而成的。以io.ReadWriteCloser接口为例它是由io.Reader、io.Writer和io.Closer这三个接口组成的。
这三个接口都只包含了一个方法是典型的小接口。它们中的每一个都只代表了一种能力分别是读出、写入和关闭。我们编写这几个小接口的实现类型通常都会很容易。并且一旦我们同时实现了它们就等于实现了它们的组合接口io.ReadWriteCloser。
即使我们只实现了io.Reader和io.Writer那么也等同于实现了io.ReadWriter接口因为后者就是前两个接口组成的。可以看到这几个io包中的接口共同组成了一个接口矩阵。它们既相互关联又独立存在。
文章学习自郝林老师的《Go语言36讲》