Necken Sie einfach die Benutzeroberfläche von go, warum sollten Sie sie verwenden?

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 ScarySchnittstelle zusammen mit ihrem Implementierungstyp Dog, Monster, Womanund einer TerrifiedFunktion.

TerrifiedDer Parameter der Funktion ist Scary, hier können Sie sehen, dass Terrifiedes egal ist, welchen spezifischen Typ Sie übergeben, solange Sie eine Schnittstelle übergeben , da sie nur die Methode Scaryverwenden möchte .Scary.Terror

ScaryDie Schnittstelle definiert den TerrifiedVertrag zwischen der Funktion und dem Aufrufer. Sie legt fest, dass der Aufrufer einen ScaryParameter ü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.Readerio.Writerio.Closerio.ReaderWriter接口。

*bytes.Buffer实现了ReaderWriterReaderWriter接口,但没有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赋给接口。因为只有指针*ProfessionPlayerPlay()方法,因此也只有*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.Writerw=nil均是将w设置为空值。这意味着,接口的动态类型和动态值均为nil

一个接口值是否为nil取决于它的动态类型,所以现在w是一个nil接口值,可以用w==nilw!=nil来判断。

调用任何一个nil接口的方法都会panic

var w io.Writer
fmt.Println(w==nil)	// true
w = nil
fmt.Println(w==nil)	// true

image

接下来w=os.Stdout,把*os.File的类型的值赋给了w

这是一次隐式把具体类型转换为接口类型的操作,相当于显示操作io.Writer(*os.Stdout)

接口值的动态类型会设置为指针类型*os.File,动态值设置为os.Stdout的副本。

image

接下来w=new(bytes.Buffer),此时动态类型为*bytes.Buffer,动态值为则是指向新分配缓冲区的指针。

最后w=nil,相当于第一步的赋值,把动态类型和动态值都设置为nil

接口值的比较

==!=操作符来比较。

  1. 两个接口值都是nil
  2. 动态类型完全一致且动态值相等

那么,两个接口值相等。

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

不可比较的情况

当接口值的动态值是不可比较的类型时,则接口值不能比较。例如:SliceMapfunction

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"))
    }
}

debugfalse时,程序会崩溃,因为main调用f时,把一个类型为*bytes.Buffer的空指针赋给了out参数,这是out的动态值为nil但动态类型为*bytes.Buffer,绕过了out!=nil的检查。

要解决上面的问题,修改var buf *bytes.Buffervar buf io.Writer即可。

类型断言

类型断言可以检查操作数的动态类型是否满足指定的断言类型。写法为x.(T),表明检查x变量的动态类型是否符合指定的T的断言类型(断言类型包括具体类型和接口类型)。

一个失败的类型断言的估值结果为断言类型的零值。

事实上,对于一个动态类型为T的接口值i,方法调用i.m(...)等价于i.(T).m(...)

断言类型为具体类型

若断言类型T是具体类型,则检查x的动态类型是否就是T,如果检查成功,断言的接口就是x的动态值,如果检查失败,那么操作崩溃。可以看出类型断言也是一个从接口值中抽取动态值的操作。

例子:

NumFakeNum均实现了MagicNumber接口。 n的动态类型为Num

当断言类型为Num时,x获得正确的动态值,okTrue

当断言类型为FakeNum时,ynil, okFalse,实际上如果不加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实现了ManHeroSuperHero接口。

创建IronMan实例stark

var hero Hero=stark时,hero变量并不能使用Fly()方法,但hero.(SuperHero)断言成功后得出的变量x则可以调用x.Fly()stark的方法增多了;相反,superHero.(Hero)的断言结果变量y使得stark的方法变少了。

CaptainAmerica实现了ManHero接口,没有实现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)
}

还可以用在mapvalue

用在mapkey时,要注意接口动态值是否可以比较。

m := make(map[string]interface{})
m["string"] = "hello"
m["number"] = 18
m["bool"] = true
m["slice"] = []int{1,2,3}

其他

  1. 尽量小巧,方法数少点

  2. 不建议每次设计新包时先建接口再去实现。这是不必要的抽象。

  3. Go标准包中很多接口都以[Name]r的形式命名。

  4. 利用接口的查询特性来减少一些重复代码的编写。

    下面的例子,假如满足任意一个断言,就直接确定了字符串的格式化方法,不用重新编写。

    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语言框架

Supongo que te gusta

Origin juejin.im/post/7120941642202234894
Recomendado
Clasificación