接口
具体类型指定了它所含数据的精确布局,同时还暴漏了内部操作。
GO中还有另外一种类型称为:接口(interface)。
接口是抽象的
接口是抽象的,没有暴露所含数据的布局或者内部结构(因为它没有),它提供的仅仅是0个,一个或者多个没有实现的方法。
通俗的说,你拿到一个接口,你不知道它是什么,但是你知道它能做什么,这也是你只关心的,不管黑猫白猫能抓到老鼠就是好猫。
为什么要用接口
接口可以帮助定义调用者之间的约定。
查看下面的例子:
// 定义了一个“恐惧”接口
type Scary interface {
// 让人“害怕”的方法
Terror()
}
// 实现了Scary接口
type Dog struct{}
func (dog *Dog) Terror() {
fmt.Println("这狗叫的很凶,让人感到害怕")
}
type Monster struct{}
func (monster *Monster) Terror() {
fmt.Println("这个怪物让人看了一下感到害怕")
}
type Woman struct{}
func (woman *Woman) Terror() {
fmt.Println("我也不知道为啥,就是很可怕")
}
func Terrified(scaryCreature Scary) {
scaryCreature.Terror()
}
func main() {
dog := Dog{}
Terrified(&dog) // 这狗叫的很凶,让人感到害怕
monster := Monster{}
Terrified(&monster) // 这个怪物让人看了一下感到害怕
woman := Woman{}
Terrified(&woman) // 我也不知道为啥,就是很可怕
}
我们定义了一个Scary
接口,还有它的实现类型Dog
、Monster
、Woman
,以及一个Terrified
函数。
Terrified
函数的参数为Scary
,这里可以看出Terrified
并不在意你传入的具体类型是什么,只要你传入一个Scary
接口就可以了,因为它只想用到Scary.Terror
方法。
Scary
接口定义了Terrified
函数和调用者之间的约定,它约定了调用者必须传入一个与Scary
接口签名和行为(所有方法)一致的参数。
接口是一个类型
接口是一个类型,接口是一个类型,接口是一个类型,接口是一个类型,接口是一个类型
实现接口
一个接口类型定义了一套方法(0个,1个或者多个),如果一个类型要实现一个接口,就要实现这个接口的所有方法。
接口定义
定义接口如下:
可以组合使用(PlayAndWatcher
),可以混合使用(PlayAndWatcherTwo
),当然也可以单独定义(PlayAndWatcherThree
)
type Player interface {
Play() string
}
type Watcher interface {
Watch() string
}
type PlayAndWatcher interface {
Player
Watcher
}
type PlayAndWatcherTwo interface {
Play() string
Watcher
}
type PlayAndWatcherThree interface {
Play() string
Watch() string
}
实现接口
如果一个类型要实现一个接口,就要实现这个接口的所有方法。
比如:
*os.File
类型实现了io.Reader
、io.Writer
、io.Closer
和io.ReaderWriter
接口。
*bytes.Buffer
实现了Reader
、Writer
和ReaderWriter
接口,但没有Closer
接口,因为它没有Close
方法
仅当一个表达式满足实现了一个接口时,这个表达式才可以赋给这个接口(=
的右边的具体类型或者接口满足了=
左边的接口的定义时)。
// 具体类型
var w io.Writer // 定义了Write方法
w = os.Stdout // *os.File有Write方法,实现了io.Writer接口
w = new(bytes.Buffer) // *bytes.Buffer有Write方法,实现了io.Writer接口
// w = time.Second // 编译不通过,没有实现io.Writer接口
// 只满足部分接口
var rwc io.ReadWriteCloser
rwc = os.Stdout // *os.File有Write、Read、Close方法
// rwc = new(bytes.Buffer) // 编译不通过,没有实现io.ReadWriteCloser接口
// 将更高阶的接口赋值
w = rwc // 没问题, 因为io.ReadWriteCloser也定义了Write方法,满足io.Writer
// rwc = w // 编译不通过
_ = rwc
_ = w
方法的接收者类型:值接收者和指针接收者
对于具体类型T
,可以用T
作为接收者,也可以用*T
作为接收者,也可以两者混合使用来实现所有方法。
对于*T
作为接收者实参的方法,但接收者形参为T
时,可以简写成T.Method()
,当然也可以写成*T.Method()
。实际上,编译器会对变量进行取地址操作&T
,前提是必须是变量,否则可能会因为无法取地址而编译不通过。
SomeStruct{1}.SomeMethod() // 编译错误,临时变量无法取地址
var x = SomeStruct{1}
x.SomeMethod() // 没问题
对于T
作为接收者实参的方法,如果接收者形参为*T
的话,则可以直接使用T.Method()
,这是一个语法糖,编译器会自动插入一个隐式的*
操作符来取出指针指向的变量。
当实现Player
接口的方法用的是指针接收者*ProfessionalPlayer
时,不能把结构体ProfessionPlayer{}
赋给接口Player
,只能把结构体指针*ProfessionPlayer
赋给接口。因为只有指针*ProfessionPlayer
有Play()
方法,因此也只有*ProfessionPlayer
实现了Player
接口。
type Player interface {
Play() string
}
type ProfessionPlayer struct{}
func (player *ProfessionPlayer) Play() string {
return "ProfessionPlayer"
}
func main() {
// var _ Player = ProfessionPlayer{} // ProfessionPlayer does not implement Player (Play method has pointer receiver)
var _ Player = &ProfessionPlayer{}
}
接口封装性
查看下面的例子:
var oso = os.Stdout
oso.Write([]byte("howdoyoudo"))
oso.Close()
var w io.Writer
w = oso
w.Write([]byte("howdoyoudo"))
w.Close() // 编译错误, io.Writer找不到Close方法
尽管os.Stdout
含有Close
方法,当如果将它赋值给io.Writer
的变量的话,这个变量会找不到Close
方法
常用的定义规则
一般非空的接口通常由一个指针接收者来实现,特别是当一个接口中的方法暗示会修改接收者的情况下。
其次Go中的引用类型也可以选择不用指针接收者,即使当中有方法可以修改接收者。
基础类型也可以实现方法,例如time.Duration
实现了fmt.Stringer
。
例子
下面是不同类型实现同一个接口的例子
type Player interface {
Play() string
}
type ProfessionalPlayer struct {
Name string
}
func (pp *ProfessionalPlayer) Play() string {
return "闭着眼都通关了"
}
type Noob struct {
Name string
}
func (noob *Noob) Play() string {
return "玩个锤子"
}
接口值
概念上讲,接口值包含两部分:一个具体类型和该类型的一个值。称为接口的动态类型和动态值。
接口值的赋值过程
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
var w io.Writer
和w=nil
均是将w
设置为空值。这意味着,接口的动态类型和动态值均为nil
。
一个接口值是否为nil
取决于它的动态类型,所以现在w
是一个nil
接口值,可以用w==nil
和w!=nil
来判断。
调用任何一个nil
接口的方法都会panic
。
var w io.Writer
fmt.Println(w==nil) // true
w = nil
fmt.Println(w==nil) // true
接下来w=os.Stdout
,把*os.File
的类型的值赋给了w
。
这是一次隐式把具体类型转换为接口类型的操作,相当于显示操作io.Writer(*os.Stdout)
。
接口值的动态类型会设置为指针类型*os.File
,动态值设置为os.Stdout
的副本。
接下来w=new(bytes.Buffer)
,此时动态类型为*bytes.Buffer
,动态值为则是指向新分配缓冲区的指针。
最后w=nil
,相当于第一步的赋值,把动态类型和动态值都设置为nil
。
接口值的比较
用==
和!=
操作符来比较。
当
- 两个接口值都是
nil
- 动态类型完全一致且动态值相等
那么,两个接口值相等。
var a interface{} = 0
var b interface{} = 2
var c interface{} = 0
var d interface{} = 0.0
fmt.Println(a == b) // false
fmt.Println(a == c) // true
fmt.Println(a == d) // false
不可比较的情况
当接口值的动态值是不可比较的类型时,则接口值不能比较。例如:Slice
、Map
和function
。
var a interface{} = []int{1, 2, 3}
fmt.Println(a == a) // panic: runtime error: comparing uncomparable type []int
接口值的比较不强行要求接口类型相同(动态类型必须相同)
接口值的比较不强行要求接口类型相同(动态类型必须相同),只要可以从一个接口转换为另一个接口就可以比较。
var f *os.File
var a io.Writer = f
var b io.ReadWriter = f
fmt.Println(a == b) // true
但如果两个接口类型不是可以相互转换的,则编译不过。
var c io.Reader = f
fmt.Println(a == c) // 编译失败:invalid operation: a == c (mismatched types io.Writer and io.Reader)
比较总结
接口值仅含有可比较类型时,则可比较;
接口值含有不可比较类型时,则不可比较;
接口值含有复合类型且复合类型中包含不可比较类型时,则不可比较。
可比较:int、ifloat、string、bool、complex、ptr、channel、interface、array
不可比较:slice、map、function
含有空指针的非空接口
空的接口值(动态类型和动态值均为nil
)与仅仅动态值为nil
的接口值是不一样的,下面从GOPL的代码里面看看这个陷阱:
const debug=true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf) // 这里就是错误点
if debug {
// 使用buf...
}
}
// 如果out不是nil,那么会向其写入输出的数据
func f(out io.Writer) {
// ...其他代码...
if out != nil {
out.Write([]byte("done\n"))
}
}
当debug
为false
时,程序会崩溃,因为main调用f
时,把一个类型为*bytes.Buffer
的空指针赋给了out
参数,这是out
的动态值为nil
但动态类型为*bytes.Buffer
,绕过了out!=nil
的检查。
要解决上面的问题,修改var buf *bytes.Buffer
为var buf io.Writer
即可。
类型断言
类型断言可以检查操作数的动态类型是否满足指定的断言类型。写法为x.(T)
,表明检查x
变量的动态类型是否符合指定的T
的断言类型(断言类型包括具体类型和接口类型)。
一个失败的类型断言的估值结果为断言类型的零值。
事实上,对于一个动态类型为T
的接口值i
,方法调用i.m(...)
等价于i.(T).m(...)
。
断言类型为具体类型
若断言类型T
是具体类型,则检查x
的动态类型是否就是T
,如果检查成功,断言的接口就是x
的动态值,如果检查失败,那么操作崩溃。可以看出类型断言也是一个从接口值中抽取动态值的操作。
例子:
Num
和FakeNum
均实现了MagicNumber
接口。 n
的动态类型为Num
当断言类型为Num
时,x
获得正确的动态值,ok
为True
。
当断言类型为FakeNum
时,y
为nil
, ok
为False
,实际上如果不加ok
的返回值,操作会直接panic
。
type MagicNumber interface {
DoMagic()
}
type Num struct {
Number int
}
func (num *Num) DoMagic() {
fmt.Println("do some magic")
}
type FakeNum struct{ Number int }
func (fnum *FakeNum) DoMagic() {
fmt.Println("do some magic")
}
func main() {
var n MagicNumber = &Num{666}
x, ok := n.(*Num)
fmt.Printf("x: %+v, ok: %t \n", x, ok) // x: &{Number:666}, ok: true
y, ok := n.(*FakeNum)
fmt.Printf("y: %+v, ok: %t \n", y, ok) // y: <nil>, ok: false
}
断言类型为接口类型
如果断言类型为接口类型,那么类型断言检查x
的动态类型是否满足T
。如果检查成功,动态值并没有提取出来,结果仍然是一个接口值,原来接口值x
的类型和值没有变更,只是结果为接口类型T。
接口类型断言常用语把一个接口变为拥有另外一套方法的接口类型(通常方法会变多),但保留了接口值中的动态类型和动态值。
例子:
IronMan
实现了Man
、Hero
、SuperHero
接口。
创建IronMan
实例stark
在var hero Hero=stark
时,hero
变量并不能使用Fly()
方法,但hero.(SuperHero)
断言成功后得出的变量x
则可以调用x.Fly()
,stark
的方法增多了;相反,superHero.(Hero)
的断言结果变量y
使得stark
的方法变少了。
CaptainAmerica
实现了Man
、Hero
接口,没有实现SuperHero
接口是因为没有实现Fly()
方法。
创建CaptainAmeria
实例steve
。
在var hero2 Hero=steve
时,hero2
不能使用Fly()
方法,尝试断言hero2.(SuperHero)
时,断言失败,因为
CaptainAmerica
并没有实现SuperHero
接口。
type Man interface {
Walk()
}
type Hero interface {
Man
Fight()
}
type SuperHero interface {
Hero
Fly()
}
type CaptainAmerica struct {
Name string
}
func (steve *CaptainAmerica) Walk() {
fmt.Println("steve is walking")
}
func (steve *CaptainAmerica) Fight() {
fmt.Println("steve fight!")
}
type IronMan struct {
Name string
}
func (stark *IronMan) Walk() {
fmt.Println("stark is walking")
}
func (stark *IronMan) Fight() {
fmt.Println("stark fight!")
}
func (stark *IronMan) Fly() {
fmt.Println("stark fly!")
}
func main() {
var stark = &IronMan{Name: "stark"}
var hero Hero = stark
fmt.Printf("hero:%+v, type: %T \n", hero, hero)
// hero.Fly() // 编译失败,没有Fly()
x, ok := hero.(SuperHero)
fmt.Printf("x:%+v, type: %T, ok: %t \n", x, x, ok)
x.Fight()
var superHero SuperHero = stark
y, ok := superHero.(Hero)
fmt.Printf("y:%+v, type: %T, ok: %t \n", y, y, ok)
// y.Fly() // 编译失败,没有Fly()
var steve = &CaptainAmerica{Name: "steve"}
var hero2 Hero = steve
a, ok := hero2.(Man)
fmt.Printf("a:%+v, type: %T, ok: %t \n", a, a, ok)
b, ok := hero2.(SuperHero) // 失败, CaptainAmerica并没有实现Fly()方法,因此没有实现SuperHero接口
fmt.Printf("b:%+v, type: %T, ok: %t \n", b, b, ok)
}
空接口值断言
无论哪种类型断言,只要被操作数是空接口值,断言都会失败。
继续用上面的例子:
hero3
并未装载任何动态类型和动态值,是空的接口值,这时候它的断言都会失败,尽管Hero
接口已经实现了Man
接口。
var hero3 Hero
c, ok := hero3.(Man)
fmt.Printf("c:%+v, type: %T, ok: %t \n", c, c, ok)
类型分支: type-switch
简单形式
简单形式,x.(type)
是固定写法,而不是特定的类型。
类型分支的满足基于接口值的动态类型,其中nil
分支只用x==nil
是才满足,而default
分支则在其他分支都不满足时才运行。
与普通的switch
语句类似,type-switch
也是按顺序来判断的,因此按优先级排顺序是有必要考虑的。
另外,类型分支不允许用fallthrough
。
switch x.(type) {
case nil: // ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
拓展形式
在需要使用类型断言提取的结果值时,可以用拓展形式。
如下:
虽然重新定义了x
变量,但其实type-switch
会隐式创建一个词法块,因此这里并不会变量冲突。
switch x := x.(type) {
case nil: // ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
NIL作为接收者是合法的!
空接口有什么作用(0个方法)
定义如下:
type Empty interface{}
或者var empty interface{}
可以把任何值赋给空接口,要使用空接口中的值,要用类型断言。
// 空接口作为函数参数
func get(something interface{}) {
fmt.Printf("type:%T value:%v\n", something, something)
}
还可以用在map
的value
用在map
的key
时,要注意接口动态值是否可以比较。
m := make(map[string]interface{})
m["string"] = "hello"
m["number"] = 18
m["bool"] = true
m["slice"] = []int{1,2,3}
其他
-
尽量小巧,方法数少点
-
不建议每次设计新包时先建接口再去实现。这是不必要的抽象。
-
Go标准包中很多接口都以[Name]r的形式命名。
-
利用接口的查询特性来减少一些重复代码的编写。
下面的例子,假如满足任意一个断言,就直接确定了字符串的格式化方法,不用重新编写。
func formatOneValue(x interface{}) string { if err, ok := x.(error); ok { return err.Error() } if str, ok := x.(Stringer); ok { return str.String() } // 其他接口的断言 }
本文正在参加技术专题18期-聊聊Go语言框架