Golang基础(6)

十一、你能做什么:接口

有时你并不关心一个值的特定类型。你不需要关心它是什么。你只需要知道它能做特定的事情。你能够在其上调用特定的接口。不需要关心是pen还是pencil,你仅仅需要一个Draw方法。不需要关心是Car还是Boat,你只需要一个Steer方法。
那就是Go接口的目标。它允许你定义能够保存任何类型的变量和函数参数,前提是它定义了特定的方法。
我们被思乡情绪淹没了,所以我们创建一个gadget包来帮助我们思乡。它包含一个模拟录音机的类型和另一个模拟播放器的类型。

package gadget

import "fmt"

type TapePlayer struct{
    Batteries string 
}

func (t TapePlayer) Play(song string) {
    fmt.Print1n("Playing", song) 
}
func (t TapePlayer) Stop(){
    fmt.Print1n("Stopped!")
}
type TapeRecorder struct{
    Microphones int
}

func (t TapeRecorder) Play(song string){
    fmt.Println("Playing", song)
}
func (t TapeRecorder) Record(){
    fmt.Println("Recording")
}
func (t TapeRecorder) Stop(){
    fmt.Println("Stopped!")
}

只能接受一种类型方法参数

我们定义一个playList函数,它接收一个TapePlayer值和一个在其上播放的一组歌名的切片。函数循环变量切片的每个歌名,并且将它传递给TapePlayer的Play方法。当它播放列表后,它调用TapePlayer的Stop方法。

package main 

import "github.com/headfirstgo/gadget"

func playList(device gadget.TapePlayer, songs []string) {
    for _,song :=range songs {
        device.Play(song)
    }
    device.Stop()
}

func main() {
    player:=gadget.TapePlayer{}
    mixtape:=[]string{"Jessie","Whip It","9 to 5"}
    playList(player,mixtape)
}

playList函数使用TapePlayer值工作正常。你可能希望它也可以使用TapeRecorder作为参数。(毕竟录音机和播放机基本相同,只是多了一个额外的录音函数)。但是playList函数需要一个TapePlayer类型。尝试传入任何其他类型。TapeRecorder类型定义了这个playList函数需要的所有方法,但是我们被卡在了playList函数只接受TapePlayer值。

接口

使用interface关键字定义一个接口类型,后面跟着一个花括号,内部含有一组方法,以及方法期望的参数和返回值。在Go中,一个接口被定义为特定值预期具有的一组方法。你可以把接口看作需要类型实现的一组行为。

type myInterface interface {
    methodWithoutParameter() // 方法
    methodWithParameter(float64)
    methodWithReturnValue() string
}

任何拥有接口定义的所有方法的类型被称作满足那个接口。一个满足接口的类型可以用在任何需要接口的地方。
方法名称、参数类型(可能没有)和返回值(可能没有)都需要与接口中定义的一致。除了接口中列出的方法之外,类型还可以有更多的方法,但是它不能缺少接口中的任何方法,否则就不满足那个接口。一个类型可以满足多个接口,一个接口(通常应该)可以有多个类型满足它。
下面的代码建立了一个实验性质的包,名为mypkg。它定义了一个有三个方法的名为MyInterface的接口。然后定义了一个名为MyType的类型,正好可MyInterface。

package mypkg

import "fmt"

type MyInterface interface {
    methodWithoutParameter() // 方法
    methodWithParameter(float64)
    methodWithReturnValue() string
}

type MyType int 
func(m MyType)MethodithoutParaneters() {
    fmt.Println("Wethodlithout Parameters called")
}
func(m Mylype)MethodWithParameter(f float64) {
    fmt.Println("VethodlithParameter called with",f)
}
func(m Mytype)MethodWithReturnValue() string {
    return"Hi from MethodllithReturnValue"
}
func(my Mylype)MethodNotInInterface() {
    fmt.Println("MethodNot InInterface called")
}

如果一个类型(MyType)包含接口中声明的所有方法,那么它可以在任何需要接口的地方使用,而不需要更多的声明。
一个接口类型的变量能够保存任何满足接口的类型的值。下面代码声明了一个MyInterface类型的名为value的变量,然后创建一个MyType的值并赋给value。(这是允许的,因为MyType满足MyInterface。)然后我们可以在value上调用接口的任意方法。

package main 
import(
    "fmt"
    "mypkg"
)

func main(){
    var value mypkg.MyInterface 
    value=mypkg.MyType(5) // MyType的值满足MyInterface我们可以将值赋给Myinterface变量
    // 可从调用MyInterface的任何方法
    value.MethodWithoutParameters()
    value.MethodWithParameter(127.3)
    fmt.Println(value.MethodWithReturnValue())
}

具体类型和接口类型

我们在之前章节中定义的所有类型都是具体类型。一个具体类型不仅定义了它的值可以做什么(你可以在其之上调用哪些方法),也定义了它们是什么:它们定义了保存值的数据的基础类型。接口类型并不描述是哪个值:它们不说它的基础类型是什么,或者数据是如何存储的。它们仅仅描述了这个值能做什么:它有哪些方法。
假设你需要记一个快速笔记。在你的桌子上,你有多种具体类型:Pen、Pencil和Marker。每种具体类型都定义了Write方法,你并不在意你抓取的是哪种类型。你仅仅是需要一个WritingInstrument:一个被任何具体类型满足的含有Write方法的接口类型。

分配满足接口的任何类型

当你有一个接口类型的变量时,它可以保存满足此接口的任何类型的值。
假设我们有Whistle和Horn类型,它们都有MakeSound方法。我们可以创建一个NoiseMaker接口来代替声明了MakeSound方法的任何类型。如果我们定义了NoiseMaker类型的toy变量,我们可以将Whistle和Horn值赋给它。(或者之后我们定义的任何类型,只要它定义了MakeSound方法。)

package main

import"fmt"

type Whistle string 

// 具有MakeSound方法
func(w Whistle)MakeSound(){
    fmt.Println("Tweet!")
}

type Horn string

// 同样具有MakeSound方法
func(h Horn)MakeSound(){
    fmt.Println("Honk!")
}

// 代表任何含有MakeSound方法类型
type NoiseMaker interface {
    MakeSound()
}

func main() {
    // 声明一个NoiseMaker变量
    var toy NoiseMaker
    // 将一个满足NoiseMaker的类型的值赋值给变量
    toy=Whistle("Toyco Canary")
    toy.MakeSound() //Tweet!
    toy=Horn("Toyco Blaster")
    toy.MakeSound() // Honk!
}

也能将函数的参数定义为接口类型。(毕竟,函数参数也就是变量。)如果声明一个play函数来接受NoiseMaker类型,我们可以传入任何包含了MakeSound方法的值来播放:

func play(n Noiseaker) {
    n. MakeSound()
}

func main(){
    play(Whistle("Toyco Canary")) //Tweet!
    play(Horn("Toyco Blaster")) // Honk!
}

只能调用接口中定义的方法

一旦你给一个接口类型的变量(或方法的参数)赋值,你就只能调用接口定义的方法。假设我们创建了Robot类型,除了有一个MakeSound方法外,还有一个Walk方法。我们在play函数中增加对Walk的调用,并且将Robot的值传入play。这样编译不会成功。因为NoiseMaker值没有Walk方法。

使用接口修复playList函数

在main包中,我们声明了一个Player接口。(也可以在gadget包中定义,但是把接口定义在调用的包中会更灵活。)我们指定接口需要有一个string参数的Play方法和一个无参的Stop方法。这意味着TapePlayer和TapeRecorder类型会满足Player接口。

package main 

import "github.com/headfirstgo/gadget"

type Player interface {
    Play()
    Stop()
}

// 修改为Player类型,可以接收含有Play和Stop方法的类型
func playList(device Player, songs []string) {
    for _,song :=range songs {
        device.Play(song)
    }
    device.Stop()
}

func main() {
    var player Player =gadget,TapePlayer{}
    mixtape:=[]string{"Jessie","Whip It","9 to 5"}
    playList(plyer,mixtape)
    player=gadget.TapeRecord()
    playList(player,mixtape)
}

类型断言

如果把一个具体类型的值赋给了接口类型的变量(包括函数参数),然后你就只能在其上调用接口的方法,不管具体类型还具有何种其他方法。
当你将一个具体类型的值赋给一个接口类型的变量时,类型断言让你能取回具体类型。

var noiseMaker NoiseMaker=Robot("Botoc Ambler")
var robot Robot =noiseMaker.(Robot) //接口值.(Robot)

简单来说,类型断言就像说某物像“我知道这个变量使用接口类型NoiseMaker,但是我很确信这个NoiseMaker实际上是Robot。”一旦你使用类型断言来取回具体类型的值,你可以调用那个类型上的方法,但这方法并不属于接口。

type Robot string 
func(r Robot)MakeSound(){
	fmt.Println("Beep Boop")
}
func (r Robot) Walk() {
	fmt.Println("Powering legs")
}
type NoiseMaker interface{
	MakeSound()
}

func main(){
    var noiseMaker Noisellaker=Robot("Botco Ambler")
    noiseMaker.MakeSound() // Beep Boop
    var robot Robot=noiseMaker.(Robot)
    robot.Walk() // Powering legs
} 

接着上面的方法,就像之前一样,我们传入一个TapeRecorder给TryOut,它被赋值给一个Player类型的参数。我们能够调用Player值的Play和Stop方法,因为这些都是Player接口的一部分。然后,我们使用一个类型断言来将Player转换回一个TypeRecorder。并且我们调用它上面的Record方法。

type Player interface{
    Play (string)
    Stop()
}

func Tryout(player Player){
    player.Play("Test Track")
    player.Stop()
// 保存TapeRecorder值
    recorder:=player.(gadget.TapeRecorder) // 使用类型断言来获得一个TapeRecorder值
    recorder.Record() // 调用仅仅定义在具体类型上的方法
}
func main(){
    TryOut(gadget.TapeRecorder{})
}

一切似乎都很正常……对于TapeRecorder。考虑到类型断言说TryOut的参数实际上是一个TapeRecorder,如果给TryOut传入TapePlayer会发生错误。

func main(){
    Tryout(gadget.TapeRecorder{})
    Tryout(gadget.TapePlayer{})
}

当类型断言失败时避免异常

如果类型断言被用于仅有一个返回值的情况,并且原始的类型不与断言的类型相同,程序会在运行时(不在编译时)出现异常:

var player Player=gadget.Tapeplayer{}
recorder:=player.(gadget.TapeRecorder)

如果类型断言被用于期待多个返回值的情况,它能有第二个可选的返回值来表明断言是否成功。(并且断言并不会在不成功时出现异常。)第二个值是一个bool,并且当原类型和断言类型相同时,返回true,否则返回false。你可以对于第二个返回值做任何操作,但是按照惯例,它通常被赋给一个名为ok的变量。

var player Player =gadget.TapePlayer{}
recorder,ok:=player.(gadget.TapeRecorder)
if ok {
    recorder.Record() // 如果原始类型是TapeRecorder,调用值上的Record.
} else {
    fmt.Println("Player was not a TapeRecorder")
}

error接口

我们曾经学习了如何创建自己的error值。我们说过,“一个错误值就是任何含有名为Error的方法的值,此方法返回string”。

err:=fmt.Error("a height of %0.2f is invalid",-2.33333) // 返回一个error值
fmt.Println(err.Error()) // 输出错误信息
fmt.Println(err) // 同样输出错误信息

一个包含任何值的具有特定方法的类型……听起来像是接口!

没错。error类型只是一个接口!看起来是这样的:

type error interface {
    Error() string
}

声明作为一个接口的error类型意味着,如果它具有一个返回string的Error方法,它就满足error接口,并且它是error的值。这意味着你能定义自己的类型并且用在任何需要error值的地方!

type ComedyError string // 定义一个以string为基础类型的类型
func (c ComedyError) Error() string { // 满足error接口
    return string(c) // Error方法需要返回一个string,所以做个类型转换
}

func main() {
    var err error // 声明一个error接口类型的变量
    err=ComedyError("What's a programmer's favorite beer? Logger!") // ComedyError满足error接口,所以我们能把ComedyError赋值给变量。
    fmt.Println(err)  // What's a programmer's favorite beer? Logger!
}

如果你需要一个error值,也需要追踪除了错误信息字符串之外更多的信息,你可以创建自己的满足error接口的类型并保存你需要的信息。

type OverheatError float64
func (o OverheatError) Error() string {
    return fmt.Sprint("Overheating by %0.2f degrees!",o) // 在错误信息中使用温度
}

func checkTemperature(actual float64, safe float64) error {
    excess:=actual-safe
    if excess > 0 {
        return OverheatError(excess)
    }
    return nil
}

func main() {
    var err error=checkTemperature(121.379,100.0)
    if err!=nil {
        log.Fatal(err) // Overheating by 21.38 degrees!
    }
}

问:为何我们可以在不同的包中使用error接口而不用导入它?它以小写字母开头。那意味着它是从声明的包中未导出的吗?error在哪个包中声明?
答:error类型像int或者string一样是一个“预定义标识符”,它不属于任何包。它是“全局块”的一部分,这意味着它在任何地方可用,不用考虑当前包信息。

Stringer接口

记得之前在第9章我们为了区分多种描述容积的单位而创建的Gallons、Liters和Milliliters类型吗?我们发现,毕竟很难区分它们。12加仑与12升和12毫升是完全不同的量级,但它们输出的时候看起来一样。如果输出的时候有非常精确的小数位,看起来也很不方便。

type Gallons float64
type Liters float64
type Milliters floats

func main() {
    fmt.Println(Gallons(12.092))
    fmt.Println(Liters(12.092))
    fmt.Println(Milliterss(12.092))
}

这样分别调用输出,只是在做一些重复性的劳动。它们的不同只体现在类型上。
那就是为什么fmt包定义了fmt.Stringer接口:允许任何类型决定在输出时如何展示。让其他类型满足Stringer接口很简单,只需要定义一个返回string类型的方法。接口定义如下所示:

type Stringer interface {
    String() string // 任何具有返回string的String方法的类型都是一个fmt.Stringer
}

type CoffeePot string
func (c CoffeePot) String() string { // 满足Stringer接口
    return string(c)+"coffee pot" // 方法需要返回一个string
}

func main() {
    coffeePot:=CoffeePot("LuxBrew")
    fmt.Println(coffeePot.String()) LuxBrew coffee pot
}

许多在fmt包中的函数都会判断传入的参数是否满足stringer接口,如果满足就调用String方法。这些函数包括Print、Println和Printf等。现在CoffeePot满足了Stringer,我们可以把CoffeePot值直接传入这些函数,并且CoffeePot的String方法的返回值会在输出时使用。
现在让Gallons、Liters和Milliliters类型都满足Stringer。我们将格式化值的代码移到与每种类型相关联的String方法。我们将用Sprintf函数代替Printf,并返回结果值。

type Gallons float64
func (g Gallons) String() string{
    return fmt.Sprintf("80.2f gal",g)
}

type Liters float64
func(l Liters) String() string { 
    return fmt.Sprintf("%0.2f L",1)
}

type Milliliters float64
func(m Milliliters) String() string {
    return fmt.Sprintf("%0.2f mL",m)
}

func main() {
    // 将每种类型的值传递给Println,每种类型的String方法的返回值在输出中使用
    fmt.Println(Gallons(12.0924)) 12.09 gal
    fmt.Println(Liters(12.0924)) 12.09 L
    fmt.Println(Milliliters(12.0924)) 12.09 mL
}

空接口

截止到现在,对于我们接触的大多数函数,只能使用指定的类型来调用它们。但是像fmt.Println这样的fmt函数可以接受任包类型!那是怎么做到的?

fmt.Println(3.1415,"A string", true)

让我们执行go doc来打开fmt.Println的文档并看看它的参数声明为何种类型:
go doc fmt Println

func Println (a …interface{}) (n int, err error)
Println formats using the default formats for its operands and writes to standard output. Spaces are always added betwen operands and a newline...

…意味着这是一个可变参数的函数,意味着它可以接受任何个数的参数。interface{}类型,接口声明定义了方法,类型必须实现这个方法才能满足接口。但是如果我们定义一个不需要任何方法的接口会怎么样?它会被任何类型满足!

// 空接口
type Anything interface {
    
}

如果你定义一个接收一个空接口作为参数的函数,你可以传入任何类型的值作为参数。但是先不要对你所有的函数使用空接口!如果你有一个空接口类型的值,你无法做任何操作。

func AcceptAnything(thing interface{}) {
    fmt.Println(thing)
    thing.MakeSound() // error,尝试在空接口值上调用方法
}

为了在空接口类型的值上调用方法,你需要使用类型断言来获得具体类型的值。

func AcceptAnything(thing interface{}) {
    fmt.Println(thing)
    whistle,ok:=thing.(Whistle) // 使用类型断言来获得Whistle
    if ok {
        whistle.MakeSound()
    }
}

func main() {
    AcceptAnything(3.1415)
    AcceptAnything(Whistle("Toyco Canary"))
}

在那种情况下,你最好写一个接收特定具体类型的函数。

func AcceptWhistle(whistle Whistle){
    fmt.Println(whistle)
    whistle.MakeSound()调用方法,不需要类型转换。
}

所以,当你定义自己的函数时,空接口的好处有限。但是你会一直将空接口与在fmt包中或者在其他地方的函数一起使用。

当你定义变量或者函数参数时,你通常明确知道你需要哪些值。你会使用具体类型像Pen、Car或者Whistle。有些时候,你只关心值能做什么。在这种情况下,你会定义接口类型,像WritingInstrument、Vehicle或MoiseMaker。你会将需要调用的方法定义为接口类型的一部分。然后你就可以赋值给变量或者调用你的函数而不用担心值的具体类型是什么。如果它有正确的方法,你就能使用它!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是浩浩子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值