文章目录
前言
有时候你并不关心一个值的特定类型,你不需要关系它是什么,只需要知道它能做的事,就可以调用特定的接口
提示:以下是本篇文章正文内容,下面案例可供参考
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{}
类型叫做空接口,用来接收任何的值,不需要实现任何方法,所有类型都满足。我们可以写一个空接口,但是通常我们不能在空接口上面调用任何函数,因为空接口是没有任何方法的。那么空接口有什么用呢?你可以在空接口里面进行类型断言,这样就可以在一个函数里面处理不同的接口类型。
如有错误,欢迎指出!!!