接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。
接口
作用
在Go语言中接口(interface)是一种类型,一种抽象的类型。
接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
使用接口的原因:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Cat struct{} func (c Cat) Say() string { return "喵喵喵" } type Dog struct{} func (d Dog) Say() string { return "汪汪汪" } func main () { c := Cat{} fmt.Println("猫:" , c.Say()) d := Dog{} fmt.Println("狗:" , d.Say()) }
上面的代码中定义了猫和狗,并且它们都会叫,所以在main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?
这就是接口的作用,用于抽象出一种规则。
定义
Go语言提倡面向接口编程。
每个接口由数个方法组成,接口的定义格式如下:
1 2 3 4 5 type 接口类型名 interface { 方法名1 ( 参数列表1 ) 返回值列表1 方法名2 ( 参数列表2 ) 返回值列表2 … }
其中:
接口名:使用type
将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有字符串功能的接口叫Stringer
等。接口名最好要能突出该接口的类型含义。
方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
举个例子:
1 2 3 type writer interface { Write([]byte ) error }
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
实现
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表 。
我们来定义一个Sayer
接口:
1 2 3 4 type Sayer interface { say() }
定义dog
和cat
两个结构体:
1 2 type dog struct {}type cat struct {}
因为Sayer
接口里只有一个say
方法,所以我们只需要给dog
和cat
分别实现say
方法就可以实现Sayer
接口了。
1 2 3 4 5 6 7 8 func (d dog) say () { fmt.Println("汪汪汪" ) } func (c cat) say () { fmt.Println("喵喵喵" ) }
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
接口类型变量
接口类型变量能够存储所有实现了该接口的实例。
如上的猫狗例子,Sayer
类型的变量能够存储dog
和cat
类型的变量。
1 2 3 4 5 6 7 8 9 func main () { var x Sayer a := cat{} b := dog{} x = a x.say() x = b x.say() }
类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口。
1 2 3 4 5 6 7 8 type Sayer interface { say() } type Mover interface { move() }
dog既可以实现Sayer接口,也可以实现Mover接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type dog struct { name string } func (d dog) say () { fmt.Printf("%s会叫汪汪汪\n" , d.name) } func (d dog) move () { fmt.Printf("%s会动\n" , d.name) } func main () { var x Sayer var y Mover var a = dog{name: "旺财" } x = a y = a x.say() y.move() }
输出结果如下:
image-20211104202306678
多个类型实现同一接口
Go语言中不同的类型还可以实现同一接口
首先我们定义一个Mover
接口,它要求必须由一个move
方法。
1 2 3 4 type Mover interface { move() }
例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type dog struct { name string } type car struct { brand string } func (d dog) move () { fmt.Printf("%s会跑\n" , d.name) } func (c car) move () { fmt.Printf("%s速度70迈\n" , c.brand) }
这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move
方法就可以了。
1 2 3 4 5 6 7 8 9 func main () { var x Mover var a = dog{name: "旺财" } var b = car{brand: "保时捷" } x = a x.move() x = b x.move() }
输出结果如下:
image-20211104202244813
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type WashingMachine interface { wash() dry() } type dryer struct {}func (d dryer) dry () { fmt.Println("甩一甩" ) } type haier struct { dryer } func (h haier) wash () { fmt.Println("洗刷刷" ) }
接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 type Sayer interface { say() } type Mover interface { move() } type animal interface { Sayer Mover }
嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type cat struct { name string } func (c cat) say () { fmt.Println("喵喵喵" ) } func (c cat) move () { fmt.Println("猫会动" ) } func main () { var x animal x = cat{name: "花花" } x.move() x.say() }
空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 func main () { var x interface {} s := "Hello 沙河" x = s fmt.Printf("type:%T value:%v\n" , x, x) i := 100 x = i fmt.Printf("type:%T value:%v\n" , x, x) b := true x = b fmt.Printf("type:%T value:%v\n" , x, x) }
空接口可以作为函数的参数,可以接收任何类型的参数。
1 2 3 4 func show (a interface {}) { fmt.Printf("type:%T value:%v\n" , a, a) }
空接口作为map的值,使用空接口实现可以保存任意值的字典。
1 2 3 4 5 6 var studentInfo = make (map [string ]interface {})studentInfo["name" ] = "沙河娜扎" studentInfo["age" ] = 18 studentInfo["married" ] = false fmt.Println(studentInfo)
类型断言
一个接口的值(简称接口值)是由一个具体类型
和具体类型的值
两部分组成的。这两部分分别称为接口的动态类型
和动态值
。
1 2 3 4 var w io.Writerw = os.Stdout w = new (bytes.Buffer) w = nil
接口值图解
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
element:表示类型为interface{}
的变量
T:表示断言element
可能是的类型。
该语法返回两个参数,第一个参数value是element
转化为T
类型后的变量,第二个值ok是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
1 2 3 4 5 6 7 8 9 10 func main () { var x interface {} x = "Hello 沙河" v, ok := x.(string ) if ok { fmt.Println(v) } else { fmt.Println("类型断言失败" ) } }
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
这里在附上写得一个通用日志库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package mainimport ( "fmt" "io" "os" ) type Logger interface { writeLog(string ) } type fileLogger struct { filename string } func (fl *fileLogger) writeLog (msg string ) { var filePointer *os.File var writeErr error if checkFileIsExist(fl.filename) { filePointer, writeErr = os.OpenFile(fl.filename, os.O_APPEND|os.O_WRONLY, 0666 ) fmt.Println("日志文件存在" ) } else { filePointer, writeErr = os.Create(fl.filename) fmt.Println("日志文件不存在" ) } defer filePointer.Close() n, writeErr := io.WriteString(filePointer, msg+"\n" ) if writeErr != nil { panic (writeErr) } fmt.Printf("共写入%d个字节\n" , n) } func checkFileIsExist (filename string ) bool { if _, err := os.Stat(filename); os.IsNotExist(err) { return false } return true } type consoleLogger struct {} func (cl *consoleLogger) writeLog (msg string ) { fmt.Println(msg) } func write () { var logger Logger fileLogger := &fileLogger{"log.txt" } logger = fileLogger logger.writeLog("Hello" ) logger.writeLog("how are you" ) consoleLogger := &consoleLogger{} logger = consoleLogger logger.writeLog("Hello" ) logger.writeLog("how are you" ) } func main () { write() }