Head First Go -第11部分接口


前言

有时候你并不关心一个值的特定类型,你不需要关系它是什么,只需要知道它能做的事,就可以调用特定的接口


提示:以下是本篇文章正文内容,下面案例可供参考

1. 具有相同方法的两种不同类型

现在我们创建一个 gadget 包,里面包含一个模拟录音机的类型和另一个模拟播放器的类型。这两种类型都有两个共同的方法

package gadget

import "fmt"

//模拟播放器
type TapePlayer struct {
	Batteries string
}

func (t TapePlayer) Play(song string) {
	fmt.Println("Playing ", song)
}

func (t TapePlayer) Stop() {
	fmt.Println("Stopped!")
}

//模拟录音器
type TapeRecorder struct {
	Microphones int
}

func (t TapeRecorder) Play(song string) {
	fmt.Println("Playing ", song)
}

func (t TapeRecorder) Stop() {
	fmt.Println("Stopped!")
}

func (t TapeRecorder) Record() {
	fmt.Println("Recording!")
}



2. 只能接收一种类型的方法参数

下面是使用的方法

package main

import "mycode/gadget"

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

func main(){
	player := gadget.TapePlayer{}
	mixtape := []string{"aaa", "bbb", "ccc"}
	playList(player, mixtape)
	//Playing  aaa
	//Playing  bbb
	//Playing  ccc
	//Stopped!
}

从这里我们可以看到,playList方法中传入 player 是可以的,但是由于TapeRecorder 也有相同的方法,我们想要传入TapeRecorder 类型的数据,这样是不行的,因为这个函数需要的是 TapePlayer 类型的参数,所以下面我们引入接口。



3. 接口定义

使用 interface关键字定义一个接口类型,后面一个花括号,内部有一些方法,以及方法期望的参数和返回值

type myInterface interface {
	methodWithoutParameters()      //无参
	methodWithParameters(float64)  //有参
	methodWithReturnValue() string //有返回值
}

一个类型可以满足多个接口,一个接口(通常应该)可以有多个类型满足它



4. 定义满足接口的类型

package gadget

import "fmt"

type MyInterface interface {
	MethodWithoutParameters()      //无参
	MethodWithParameters(float64)  //有参
	MethodWithReturnValue() string //有返回值
}

type MyType int

func (m MyType) MethodWithoutParameters() {
	fmt.Println("没有参数和返回值的接口方法")
}

func (m MyType) MethodWithParameters(f float64) {
	fmt.Println("有参数没有返回值的方法, 参数: ", f)
}

func (m MyType) MethodWithReturnValue() string {
	fmt.Println("没有参数有返回值的方法")
	return "success"
}

func (m MyType) other(){
	
}

上面的代码就是一个满足接口的类型,在其他语言中可能需要显示定义某个类型实现某个接口,比如 java 的implement,但是在Go是不需要的,如果一个类型满足接口的所有方法,那么这个类型就自动满足了这个接口。

下面是调用接口的方法:

package main

import (
	"fmt"
	"mycode/gadget"
)

func main(){

	var value gadget.MyInterface
	value = gadget.MyType(5)

	value.MethodWithParameters(2.0)		//有参数没有返回值的方法, 参数:  2
	value.MethodWithoutParameters()		//没有参数和返回值的接口方法
	fmt.Printf("value.MethodWithReturnValue(): %v\n", value.MethodWithReturnValue())
	//value.MethodWithReturnValue(): success
	//没有参数有返回值的方法

}



5. 只能调用接口中的方法

现在我们定义一个接口,然后这个接口定义一个方法,这个方法是eat方法,同时我们定义两个类型,people和animal来实现这些方法

package main

import "fmt"

type Action interface {
	eat()
}

type People string

func (p People) eat() {
	fmt.Println(p, "吃食物")
}

type Animal string

func (a Animal) eat() {
	fmt.Println(a, "吃食物")
}

func (a Animal) fly() {
	fmt.Println(a, "在飞")
}

//方法使用接口作为参数
func ac(a Action) {
	a.eat()
}

func main() {
	ac(People("人"))	//人 吃食物
	ac(Animal("鸟"))	//鸟 吃食物
}

可以看到ac方法接收一个接口类型的作为参数,而People和Animal 都有eat这个方法,所以这两个类型是满足接口类型的,可以把这两个类型传递给ac作为参数。但是注意的是Animal里面有一个fly方法,这个方法不是接口的方法,所以ac中a.fly()这种写法是错误的



6. 我们使用接口修复2中的方法

package gadget

import "fmt"

//模拟播放器
type TapePlayer struct {
	Batteries string
}

func (t TapePlayer) Play(song string) {
	fmt.Println("Playing ", song)
}

func (t TapePlayer) Stop() {
	fmt.Println("Stopped!")
}

//模拟录音器
type TapeRecorder struct {
	Microphones int
}

func (t TapeRecorder) Play(song string) {
	fmt.Println("Playing ", song)
}

func (t TapeRecorder) Stop() {
	fmt.Println("Stopped!")
}

func (t TapeRecorder) Record() {
	fmt.Println("Recording!")
}

package main

import "mycode/gadget"

//main函数中定义接口
type Player interface {
	Play(string)
	Stop()
}

//播放歌曲
func playList(device Player, songs []string) {
	for _, song := range songs {
		device.Play(song)
	}
	device.Stop()
}

func main() {
	//另外一个包的两个类型实现这两个方法,那么久都可以使用了
	player := gadget.TapePlayer{}
	mixtape := []string{"aaa", "bbb", "ccc"}
	playList(player, mixtape)

	recorder := gadget.TapeRecorder{}
	playList(recorder, mixtape)
}

接口名必须大写开头吗?如果你希望接口是可以导出的,就要设置为大写开头的。



7. 指针接收器

如果一个类型声明了指针接收器的方法,那么你就只能将那个类型的指针传递给接口变量

package main

import "fmt"

type Switch string

func (s *Switch) toggle() {
	if *s == "on" {
		*s = "off"
	} else {
		*s = "on"
	}
	fmt.Println(s)
}

type Toggleable interface{
	toggle()
}

func main() {
	s := Switch("off")
	var t Toggleable = s	//这种写法是错误的,因为这个类型声明了指针接口接收器,所以我们要把指针传递给接口
	t.toggle()
	t.toggle()
}

下面是正确写法 var t Toggleable = &s

package main

import "fmt"

type Switch string

func (s *Switch) toggle() {
	if *s == "on" {
		*s = "off"
	} else {
		*s = "on"
	}
	fmt.Println(*s)
}

type Toggleable interface{
	toggle()
}

func main() {
	var s Switch = Switch("off")
	var t Toggleable = &s
	t.toggle()	//on
	t.toggle()	//off
}



8. 类型断言

什么是类型断言?我们从上面的人和动物的例子中可以得知,一个类型包含了这个接口的所有方法,那么这个类型可以作为接口的一个实现,但是比如上面的动物有一个 fly 方法是接口没有的,这时候我们把动物作为接口参数传递给接口的时候是调用不了 fly 方法的,因为接口只能调用接口的方法。类型断言意思就是我可以获取传递给接口的类型,其实类似于 java 中的接口实现关系,我可以在java中用括号具体获取到某一个实现接口的类型,在Go语言中则是使用接口断言。

语法:var 名字 具体类型 = 接口变量.(确定的类型)

package main

import "fmt"

type Action interface {
	eat()
}

type People string

func (p People) eat() {
	fmt.Println(p, "吃食物")
}

type Animal string

func (a Animal) eat() {
	fmt.Println(a, "吃食物")
}

func (a Animal) fly() {
	fmt.Println(a, "在飞")
}

//方法使用接口作为参数
func ac(a Action) {
	a.eat()
}

func main() {
	//接口类型接收
	var action Action = Animal("鸟")
	action.eat()	//鸟 吃食物
	
	//断言这个接口的类型是一个Animal类型
	var animal Animal = action.(Animal)
	//然后调用animal类型的fly方法
	animal.fly()
}

注意一下,我们使用断言的前提是你得知道这个是什么类型



8. 断言失败和异常避免

如果我们断言的类型和返回的类型不一致,那么就会报错

func TryOut(action Action) {
	action.eat()
	a := action.(Animal)
	a.fly()
}

现在有一个TryOut方法,里面是传入一个类型进行转换,如果我们传入了 People类型,那么就会报错,因为类型不匹配转换失败。我们可以参数转换的一个返回值err,当err == false 的时候证明转化失败

func TryOut(action Action) {
	action.eat()
	a, err := action.(Animal)
	if err == false{
		log.Fatal("类型转换失败")
	}else{
		a.fly()
	}

}

到最后想说明的一点就是,类型断言的时候要判断一下返回值看成不成功



9. error 接口和 Stringer 接口

经过上面的例子,我们知道了接口的特性,只需要某个自定义类型具有某个接口的所有方法,那么这个类型相当于实现了这个接口。那么对于Go来说,Error() 接口和 Stringer() 接口就是,我们可以自己定义我们的错误信息以及输出格式

//接口
type Stringer interface{
	String() string
}

type error interface{
	Error() string
}

下面是自定义输出错误格式:

package main

import "fmt"

type MyError string

func (m MyError) Error() string {
	return string(m)
}

func main() {
	var err error = MyError("My Error")
	fmt.Println(err)		//My Error
}

同样的对于stringer函数,我们希望按照我们自己的输出格式进行输出,注意stringer函数是fmt包下的:

package main

import "fmt"

type MyError string

func (m MyError) Error() string {
	return string(m)
}

type MyStringer string

func (m MyStringer) String() string{
	return string(m) + "- define by me"
}

func main() {
	ms := MyStringer("自定义stringer")
	fmt.Println(ms.String())			//自定义stringer- define by me
}

那么对于error接口,为什么我们可以直接使用而不用导入呢?error是小写字母开头的,那意味者它是从声明的包中未导出的吗?error是在哪个包声明的?

error类型就像int或者string一样是一个“预定义标识符”,它不属于任何包。它是全局块的一部分,这意味着它可以在任何地方可用,不需要考虑当前包的信息。



10. 空接口

fmt.Println 可以接收任何类型的参数,这是怎么做到的?我们可以使用go doc命令来看看
在这里插入图片描述

type any = interface{}

我们可以看到,接收的参数是多个 interface{}, 这种类型就是空接口类型,空接口类型的作用就是可以被任何类型满足!我们到源码去看看。我们会发现Printf能接收任意类型,然后传递给Fprintf进行输出处理。

func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

func Printf(format string, a ...any) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

interfce{}类型叫做空接口,用来接收任何的值,不需要实现任何方法,所有类型都满足。我们可以写一个空接口,但是通常我们不能在空接口上面调用任何函数,因为空接口是没有任何方法的。那么空接口有什么用呢?你可以在空接口里面进行类型断言,这样就可以在一个函数里面处理不同的接口类型。





如有错误,欢迎指出!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值