Schnittstelle
Ein konkreter Typ gibt das genaue Layout der darin enthaltenen Daten an und legt gleichzeitig interne Operationen offen.
Es gibt einen anderen Typ in GO namens: Interface.
Schnittstelle ist abstrakt
Eine Schnittstelle ist abstrakt und legt das Layout oder die interne Struktur der enthaltenen Daten nicht offen (weil dies nicht der Fall ist). Sie stellt nur null, eine oder mehrere Methoden bereit, die nicht implementiert sind.
Laienhaft gesagt, wenn Sie ein Interface bekommen, wissen Sie nicht, was es ist, aber Sie wissen, was es kann, und das ist es, was Sie nur interessiert: Egal, ob eine schwarze oder eine weiße Katze Mäuse fangen kann, es ist eine gute Katze.
Warum eine Schnittstelle verwenden
Schnittstellen können helfen, Verträge zwischen Anrufern zu definieren.
Sehen Sie sich das folgende Beispiel an:
// 定义了一个“恐惧”接口
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) // 我也不知道为啥,就是很可怕
}
Wir definieren eine Scary
Schnittstelle zusammen mit ihrem Implementierungstyp Dog
, Monster
, Woman
und einer Terrified
Funktion.
Terrified
Der Parameter der Funktion ist Scary
, hier können Sie sehen, dass Terrified
es egal ist, welchen spezifischen Typ Sie übergeben, solange Sie eine Schnittstelle übergeben , da sie nur die Methode Scary
verwenden möchte .Scary.Terror
Scary
Die Schnittstelle definiert den Terrified
Vertrag zwischen der Funktion und dem Aufrufer. Sie legt fest, dass der Aufrufer einen Scary
Parameter übergeben muss, der mit der Schnittstellensignatur und dem Verhalten (alle Methoden) konsistent ist.
Schnittstelle ist ein Typ
Eine Schnittstelle ist ein Typ, eine Schnittstelle ist ein Typ, eine Schnittstelle ist ein Typ, eine Schnittstelle ist ein Typ, eine Schnittstelle ist ein Typ
Schnittstelle implementieren
Ein Schnittstellentyp definiert eine Menge von Methoden (0, 1 oder mehr) Wenn ein Typ eine Schnittstelle implementieren möchte, muss er alle Methoden der Schnittstelle implementieren .
Schnittstellendefinition
Definieren Sie die Schnittstelle wie folgt:
Kann in Kombination ( PlayAndWatcher
), gemischt ( PlayAndWatcherTwo
) oder einzeln definiert ( PlayAndWatcherThree
) verwendet werden
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
}
Schnittstelle implementieren
Wenn ein Typ ein Interface implementiert, implementiert er alle Methoden des Interfaces .
zum Beispiel:
*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语言框架