简单撩一撩go的Interface,为什么要用它呢

·  阅读 3300

接口

具体类型指定了它所含数据的精确布局,同时还暴漏了内部操作。

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接口,还有它的实现类型DogMonsterWoman,以及一个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.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语言框架

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改