接口
具体类型指定了它所含数据的精确布局,同时还暴漏了内部操作。
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语言框架