接口
什么是接口?为什么需要接口?
你以前做过的最抽象的事是什么?我写了一个接口。
接口是只含有方法名,参数,返回值的没有实现体的方法集合。
有时候一些数据具有一个共同点,我们只需要让这些数据做他们共同点的事情。比如我定义了3个对象:鞭炮、手雷、原子弹,这些对象都有一个函数叫引爆,能爆就是它们的共同点,那么我就可以定义一个接口叫爆炸物,接口里有个引爆方法。
type explosive interface {//爆炸物
detonate() int//如果是爆炸物必须要有这个方法
}
type firecracker struct{}//鞭炮
func (firecracker) detonate() int {
fmt.Println("firecracker explode")
return 1
}
type grenade struct{}//手雷
func (grenade) detonate() int {
fmt.Println("grenade explode")
return 1000
}
type atomicBomb struct{}//原子弹
func (atomicBomb) detonate() int {
fmt.Println("atomicBomb explode")
return 10000000
}
func main() {
f := firecracker{}
g := grenade{}
a := atomicBomb{}
//现在有三种爆炸物放到bombs数组里,如果没有实现detonate会panic
bombs := []explosive{f, g, a}
totalDamage := 0
for _, bomb := range bombs {
//遍历数组引爆炸弹并统计照成的伤害
totalDamage += bomb.detonate()
}
fmt.Printf("damage:%d", totalDamage)
}
最后输出:
firecracker explode
grenade explode
atomicBomb explode
damage:10001001
不用接口,直接使用f.detonate(),g.detonate()可以达成一样的效果。但那样就得关心你拿到的具体是什么,而不是我就知道这东西能炸,我只管拿来引爆就行。
Golang的接口
go不是传统的面向对象语言。go的接口是隐式实现的,也叫非侵入式设计。不像java那样需要在类里写实现了什么接口。
高度面向对象的语言会让下层的对象和他们的分类,也就是接口或者抽象类死死地绑在一起。用面向对象思维设计项目大多都是从上层抽象开始设计的,在没开始设计下层的类的时候就已经先安排好了要继承哪个类或实现哪个接口。而这样可能会使原本这个类应该有的设计被改变,这样做的坏处就是会使得项目在庞大的继承树下变得愈发臃肿和低效。
非侵入式设计可以让接口和实现者解耦。创建新接口满足现有的具体类型而不会改变这些类型的定义,只需要接口里的全部方法在对象里有实现,那么对象就能实现该接口,而且不需要指定实现这个接口。
实现接口
type I interface {//定义接口I里有方法f
f(int)
}
type S struct{}//定义空结构体S
func (s S) f(i int) {//结构体S实现接口I的f方法
}
type P int//定义int类型结构P
func (p P) f(i int) {//int类型结构P实现接口I的f方法
}
只需要函数名,参数,返回值对应了接口的方法,那么S和P都能直接塞到接口I类型的对象里。
接口方法接收器值接收和指针接收
type I interface {
f(int)
}
type S struct{}
func (s *S) f(i int) {//这里改为了使用指针接收器
}
type P int
func (p P) f(i int) {
}
func main() {
var i I
var p P
var s S
i=s//报错
i=&s
i=p
i=&p
_=i
}
使用指针接收器的方法实现接口必须传递该方法的对象的指针。
一个类型可以实现多个接口
type I1 interface {
f1()
}
type I2 interface {
f2()
}
type S struct{}
func (s S) f1() {}
func (s S) f2() {}
S既可以实现I1也可以实现I2。
类型继承实现一个接口
type I interface {
f1()
f2()
}
type S struct{}
func (s S) f1() {}
type SF struct {
S
}
func (s SF) f2() {}
这里说的继承其实是通过类型嵌套实现。
在SF里直接写S就等同于SF直接拥有了S同级的方法和属性。
实现了接口I的是SF,S只实现了一半。
如果把SF改为下面这样会怎么样?
type SF struct {
s S
}
结果是SF不能实现接口I,因为这样S将作为SF的属性存在,f1就不是SF的方法。
如果要调用f1只能sf.s.f1()而不能sf.f1(),所以看能不能实现接口可以通过能不能直接调用函数判断。
接口继承被一个类型实现
type I interface {
f1()
}
type IF interface {
I
f2()
}
type S struct{}
func (s S) f1() {}
func (s S) f2() {}
实现了f1可以实现I接口,既实现了f1也实现了f2可以实现IF接口。
空接口
var i interface{}
var a any
不需要实现任何方法就能实现的接口,也就是任何类型都可以实现空接口,可以存储任意对象。
如果想要获取从空接口取出的对应对象,只能用类型断言(所有接口同理)。
类型断言
var a interface{}
a = 1
if i, ok := a.(int); ok {
fmt.Println(i)
}
单个断言判断,i是int值,断言成功ok是true
var a any
a = 1
switch v := a.(type) {
case string:
fmt.Println("string", v)
case int:
fmt.Println("int", v)
case any:
fmt.Println("any", v)
}
switch v := a.(type) {
case string:
fmt.Println("string", v)
case any:
fmt.Println("any", v)
case int:
fmt.Println("int", v)
}
输出:
int 1
any 1
在switch里面参数写v:=x.(type)可以进行类型匹配,在case里面的v类型直接就是case的类型(如果case后面接多个类型,那么case里面v的类型还是原来a的类型)
可见any和int的case都满足条件,哪个排在前面哪个被选择。
接口与nil
如何判断接口里是否有东西?也就是判断是不是nil。但这样判断可能会存在问题。
func main() {
var i interface{}
var s *int
i = s
fmt.Println(s == nil)
fmt.Println(s == i)
fmt.Println(i == nil)
fmt.Println(s, i)
}
以上代码输出:
true
true
false
问题出现了。s是nil,s与i判断一样,i不是nil,最后输出s和i也都是nil。(有种js的美)
那么这个i到底是不是nil?
答案是如是。
首先go的nil只能赋值给指针和接口,s是int类型的空指针,声明的时候默认是nil。i是空接口类型,创建时也是nil。s可以实现空接口,于是把s放到i里。但这里需要知道接口底层实现是有两个部分的:type和data。接口创建或者被赋值nil的时候type和data都是nil,s被放进i后s的值nil被放进data,但type变为了s的类型*int,只有type和data都是nil的时候跟nil对比才是一样的。而且接口输出的时候只会输出data的内容,很容易踩雷。
接口与反射
func main() {
var i interface{}
var s *int
fmt.Println(reflect.TypeOf(i))
fmt.Println(reflect.ValueOf(i))
i = s
fmt.Println(reflect.TypeOf(i))
fmt.Println(reflect.ValueOf(i))
}
输出:
把上面接口与nil的代码反射查看就能发现反射的type和data对应的就是接口的type和data,这样就能分辨接口里的东西是不是真nil了,不过反射会影响性能,最好还是在调用方法的时候碰到nil
接口与泛型
泛型是go1.18后加入的(any也是)
type I1[T any] interface {
f1(T) T
}
type I2 interface {
f2(int) int
}
type S[T any] struct{}
func (S[T]) f1(t T) T {
return t
}
func (t S[T]) f2(i int) int {
return i
}
func main() {
var i1 I1[int]
var i2 I2
s := S[int]{}
i1 = s
i2 = s
fmt.Println(i1.f1(1))
fmt.Println(i2.f2(1))
}
泛型类型如果需要用到泛型的方法,那这个方法的接口也得用泛型,如I1。如果方法不需要泛型则不需要泛型接口实现,如I2。泛型的类型约束可以不一样,但如果给泛型传递的类型不在实现接口的类型和接口类型的类型约束外,那么会报错。总之就是泛型接口和实现接口的泛型类型在声明时传递的泛型类型要保持一致。
一般接口
go1.18之后的接口被分为两种类型:基本接口(Basic interface)和一般接口(General interface)
前面介绍的所有接口都是基本接口,也就是只有方法的接口。
接口内存在类型的话就是一般接口。
type Int interface {
int | int8 | int16 | int32 | int64
}
type Float interface {
~float32|~float64//在类型前面加~的意思是已后面类型为底层的类型也包括
}
func Add[T Int|Float](a, b T) T {
return a + b
}
这是一般接口的最基本用法,被放在泛型的类型限制里。当然,不用接口直接把类型通过”|”写在泛型类型限制里面也行。
一般接口不像基本接口可以定义变量,一般接口只能被用在泛型的类型约束中。
当然一般接口是可以保留方法的。
type I interface {
int
f()
}
这种写法的定义和使用场景就很奇怪,一般来说用不到。因为不能直接变量声明一般接口,这种形式的接口只能在泛型里限制类型,然后作为泛型方法的参数使用。也就是本来接口直接作为函数参数,没有限制传入的实现接口的对象的载体,加了这层泛型接口后就能通过泛型来一起限制传入的实现了接口的对象类型。