文章目录
- 接口
- 接口的定义与使用
- 实现的意义
- 类型断言
接口
接口是Go语言的一种类型。简单上来讲,接口就是一系列方法的集合。通过定义接口,可以实现面向对象的多态,以及为反射提供支持。
我们可以把接口看做一个盒子,这个盒子可以装类型
与 该类型的值
接口的定义与使用
我们知道,Go语言里面声明一个接口,或者类型,有3种方式
- 定义型:
type Doer interface { Do() }
- 非定义型
interface { Do() }
- 非定义型别名
type Doer = interface { Do() }
别名方式在编译器看来,和原始类型是同一类型,相互可以赋值
下面我们来看看如何实现一个接口呢
type MyInt int
func (*MyInt) Do() {
fmt.Println("i am do ")
}
这样就实现了上面定义的那个接口了,这是一个指针接收者,可以定义一个MyInt
变量去调用 Do
方法,下面列举了定义该类型和使用该类型的方法
func main() {
a := new(MyInt)
b := MyInt(1)
var c MyInt
var d MyInt = 1
var e Doer = new(MyInt)
var f Doer = MyInt(1) // 编译出错,因为Do方法为指针接收,所以该接口变量无法接收一个值类型的值,只能接收指针类型的值
a.do()
b.do()
c.do()
d.do()
e.do()
}
除了使用指针接收方式,还可以使用值接收方式,比如像这样
func (MyInt) Do() {
fmt.Println("i am do ")
}
那么这两种有何区别呢,只需要记住,「指针接收」比「值接收」更严苛,区别就在于,使用「接口变量」能包裹的值的类型是值
还是指针
,所以上述定义为指针接收类型的方法的时候,接口变量就不能包裹值类型,所以上述f
变量那行会出现编译错误
说得通俗点,定义为值接收者
方法时,指针和值都可以调用,我们可以通过下面这两段代码测试一下
package main
import (
"fmt"
"reflect"
)
type A struct {
}
func (*A) Do() {
fmt.Println("call A do")
}
type B struct {
}
func (B) Do() {
fmt.Println("call B do")
}
func main() {
//A{}.Do() // 编译有错,无法使用值去调用指针接收者的方法
new(A).Do() // 正确
B{}.Do() // 值接收者时候,值和指针都能正确调用
new(B).Do() //
printMethodList(A{}) // output: 无打印
printMethodList(&A{}) // output: Method Name is Do()
printMethodList(B{}) // output: Method Name is Do()
printMethodList(&B{}) // output: Method Name is Do()
}
// 打印p拥有的方法集
func printMethodList(p interface{}) {
v := reflect.TypeOf(p)
for i := 0; i < v.NumMethod(); i++ {
fmt.Println("Method Name is " + v.Method(i).Name + "()")
}
}
在使用值接收
定义方法时候,go编译器替我们省略了一步
// new(B).Do() 为啥可以正确打印呢?这一步go编译器会做下面两步,属于一个语法糖
p := new(B)
(*p).DO
那么值接收和指针接收,除了以上说的那些,还有什么区别呢?
指针接收,指向的都是调用该方法的对象,对象只有一份;
值接收,是对源对象的拷贝,也就是说,每调用一个改对象的方法,则对其进行一次拷贝
那么什么时候使用值接收
, 什么时候使用指针接收呢
推荐使用值接收的情况:
- 内置类型(int string) 等
- slice,map,interface,channel 这些内置结构体类型
在使用 比如 map 这种内置结构体类型的时候,拷贝是只会拷贝其 header,相当于共享一份底层
推荐使用指针接收的情况:
- 自定义结构体类型
实现的意义
首先,一个类型,它拥有的所有方法的集合,称之为方法集。
如果这个方法集是某个接口的「超集」,我们就说这个类型实现了该接口,于是这个类型的值可以赋值给该接口变量。
所有类型都是「空接口」的实现,所以任意类型都能赋值给空接口变量
由于Go中的实现关系是隐式的,所以如果你声明了一个接口,某些类型可能「被动」地实现了你这个接口的方法,于是就可以将该类型的实例,赋值给此接口变量。
Go里所有的类型都实现了 interface{}
这个空接口,所以可以将任何值赋值给空接口变量,比如下面这段代码
var i interface{} = 123
有一个很重要的思想方法就是,当你要赋值给一个接口变量时,你这个类型只要包含了接口所定义的方法集,那么就可以赋值给它
func Add(a, b interface{}) {
if aa, ok := a.(interface{Do()}); ok { // 只要 a 这个类型有 Do 方法,则这里就可以编译通过
aa.Do()
// ...
}
}
类型断言
其实在Go语言里面,类型的断言,就类似于Java里的类型强转差不多(使用场景),目的是,将通用型接口变量,转向更定制化的方向。下面举个例子,不过有一点必须记住,::被断言的一定是接口类型::
type Sayer interface {
Say()
}
type People struct {
Name string
}
func (*People) Say() {
fmt.Println("我是一个人,我正在说话")
}
func main() {
var sayer Sayer = &People{} // 因为 People 实现了 Sayer接口,所以可以赋值给该接口的变量
sayer.Say() // 调用该接口方法,此时并不管关心具体实现方式是怎样的,只要实现了就行
people := sayer.(*People) // 此时你很肯定此接口变量包裹的就是 *People 类型,所以不需要第二个参数进行判断
}
上面简单使用了一下类型断言,它的表达式有2种
v1 := i.(Type) // 确定就是这个类型的时候,无需第二个参数进行判断
v2, ok := i.(Type) // 无法确定它的类型,ok 是个 bool 值,可以通过它判断是否实现
假如使用了第一种,类型又断言错了的话,会直接产生一个 panic,用第二种则不会,可以通过 ok 值来进行判断是否实现了该接口
有时候新手肯定会纳闷,啥时候该断言呢?我的接口类型是该放外面,还是括号里面呢。
其实只要记住,断言的目的。
咱们平时说「断言」二字,其实就是就算断定的意思,你很断定这个接口的动态类型,就是某个具体的类型。
断言的时候,可以不断言出具体的类型,也就是断言的类型可以是接口 i.(另一个接口类型B)
,但是 i 和 此接口类型,一定是可以装载同一个动态类型。i 是比较通用的接口类型,而 i 中的动态类型,一定也是实现了 接口B,只不过此时我们只需要使用 接口B 的方法,所以没必要将整个动态类型断言出来(当然断言出整个动态类型也没什么错)
如果还是懵逼,可以记住下面的公式
更通用的接口.(较为定制的类型)
左边的一定是更加通用的,右边一定是更加实现的:通用 -> 实现
记住这个公式,可以避免犯错,百试不爽
[未完待续]。。。