接口
在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于字符串、切片、结构体等更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。
多态特性主要通过接口来体现
实现接口就是指实现了接口声明的所有方法
体现程序设计的多态和高内聚低耦合的思想
接口类型
接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。
相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么。
接口的定义
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
- 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
type Wrtier interface {
Write()
}
当你看到一个Writer
接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write
方法来做一些事情。
实现接口的条件
在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
package main
import "fmt"
type Wrtier interface {
Write()
}
type Student struct{
name string
}
func (s Student) Write(){
fmt.Println("aaa")
}
这样就称为Student实现了Writer接口
接口类型变量
一个接口类型的变量能够存储所有实现了该接口的类型变量。
package main
import (
"fmt"
)
type Person interface {
say()
}
type Student struct {
name string
}
func (s Student) say() {
fmt.Println("hi", s.name)
}
type Teacher struct {
name string
}
func (t *Teacher) say() {
fmt.Println("hello", t.name)
}
func main() {
var p Person //声明一个Person类型的变量p
p = Student{name: "tom"} //可以把Student类型变量直接赋值给p
p.say() //hi tom
p = &Teacher{name: "jack"}//可以把&Teacher类型变量直接赋值给p
p.say() //hello jack
}
Student和&Teacher类型均实现了Person接口,此时一个Person类型的变量就能够接收Student和&Teacher类型的变量。
类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
package main
import "fmt"
type AInterface interface {
a()
}
type BInterface interface {
b()
}
type stu struct {
}
func (s stu) a() {
fmt.Println("aaa")
}
func (s stu) b() {
fmt.Println("bbb")
}
func main() {
var s stu
var a AInterface = s
var b BInterface = s
a.a()//aaa
b.b()//bbb
}
多种类型实现同一接口
package main
import (
"fmt"
)
type Person interface {
say()
}
type Student struct {
name string
}
func (s Student) say() {
fmt.Println("hi", s.name)
}
type Teacher struct {
name string
}
func (t *Teacher) say() {
fmt.Println("hello", t.name)
}
func main() {
var p Person
p = Student{name: "tom"}
p.say() //hi tom
p = &Teacher{name: "jack"}
p.say() //hello jack
}
接口嵌套
接口与接口之间可以通过互相嵌套形成新的接口类型
type BInterface interface {
test01()
}
type CInterface interface {
test02()
}
type AInterface interface {
BInterface
CInterface
test03()
}
对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。
空接口
空接口的定义
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
package main
import "fmt"
type Any interface {
}
type Student struct {
}
func main() {
var a Any
a = "hello"
fmt.Printf("%T\n", a) //string
a = 100
fmt.Printf("%T\n", a) //int
a = true
fmt.Printf("%T\n", a) //bool
a = Student{}
fmt.Printf("%T", a) //main.Student
}
通常我们在使用空接口类型时不必使用type
关键字声明,可以像下面的代码一样直接使用interface{}
。
var a interface{} // 声明一个空接口类型变量a
空接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意值的字典。
var s = make(map[string]interface{})
s["name"] = "tom"
s["age"] = 19
s["score"] = 90.0
fmt.Println(s)//map[age:19 name:tom score:90]
接口值
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型
和动态值
。
package main
import (
"fmt"
)
type Person interface {
say()
}
type Student struct {
name string
}
func (s Student) say() {
fmt.Println("hi", s.name)
}
type Teacher struct {
name string
}
func (t *Teacher) say() {
fmt.Println("hello", t.name)
}
func main() {
}
首先,我们创建一个Person接口类型的变量p
。
var p Person
此时,接口变量p
是接口类型的零值,也就是它的类型和值部分都是nil
,
我们可以使用m == nil
来判断此时的接口值是否为空。
fmt.Println(p == nil)//true
对一个空接口值调用方法会产生panic
p.say()//panic: runtime error: invalid memory address or nil pointer dereference
接下来,我们将一个Student
结构体赋值给变量p
。
p = Student{name: "tom"}
此时,接口值p
的动态类型会被设置为Student
,动态值为结构体变量的拷贝。
然后,我们给接口变量p
赋值为一个*Teacher
类型的值。
var t Teacher
p = &t
此时,接口值p
的动态类型为*Teacher
,动态值为nil
。
**注意:**此时接口变量p
与nil
并不相等,因为它只是动态值的部分为nil
,而动态类型部分保存着对应值的类型。
fmt.Println(p == nil)//false
接口值是支持相互比较的,当且仅当接口值的动态类型和动态值都相等时才相等。
var (
x Person = new(Student)
y Person = new(Teacher)
)
fmt.Println(x == y) // false
但是有一种特殊情况需要特别注意,如果接口值保存的动态类型相同,但是这个动态类型不支持互相比较(比如切片),那么对它们相互比较时就会引发panic。
var z interface{} = []int{1, 2, 3}
fmt.Println(z == z) // panic: runtime error: comparing uncomparable type []int
指针接收者实现接口
在Go语言中,接口是一组方法的集合,而任何实现了这些方法的类型都可以被认为实现了该接口。接口方法可以通过指针接收者或值接收者来实现。使用指针接收者实现接口的好处在于它允许方法修改接收者的状态,并且避免了值拷贝带来的性能开销。
为什么使用指针接收者?
- 允许修改结构体的字段:如果方法需要修改结构体的字段值,那么必须使用指针接收者。
- 避免拷贝大结构体:使用值接收者时,结构体会被复制一份。如果结构体很大,使用指针接收者可以避免性能问题,因为只传递了指针而不是结构体的副本。
指针接收者和值接收者的区别:
指针接收者:当使用 指针类型作为方法的接收者时,必须通过指针类型调用该方法,否则会报错。
值接收者:如果方法的接收者是值类型,则即使是指针类型的变量也可以调用该方法(Go 会自动解引用)。
package main
import "fmt"
type U interface{
say()
}
type num struct{
}
func (n *num) say(){
}
func main() {
var n num
//var a U = n n没有实现say( ) 需要改成var a student = &n
var a U = &n
fmt.Println(a)
}
总结:
使用指针接收者实现接口时可以:
- 允许修改接收者对象的状态。
- 避免复制大结构体。
- 接口值可以通过指针类型调用其方法。
接口赋值时,注意类型的匹配。如果接口方法使用了指针接收者,必须通过结构体的指针来实现。
类型断言
语法格式
x.(T)
其中:
-
x:表示接口类型的变量
-
T:表示断言
x
可能是的类型。
```go
package main
import "fmt"
type Point struct {
x, y int
}
func main() {
var a interface{}
var point Point = Point{1, 2}
a = point // ok
//如何将a赋值给point
var b Point
//b = a 不可以
b = a.(Point) //可以
fmt.Println(a)//{1 2}
fmt.Println(b)//{1 2}
}
在进行类型断言时,如果类型不匹配就会panic,因此进行类型断言时,要确保原来空接口指向的就是断言类型
package main
import "fmt"
func main() {
var a float32 = 1.2
var b interface{}
b = a
c := b.(float32) // c :=b.(float64)错误
fmt.Printf("%T,%v", c, c)
}
输出 : float32 , 1.2
加入一段检测代码
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
package main
import "fmt"
func main() {
var a float32 = 1.2
var b interface{}
b = a
//检测
c,ok := b.(float32) //c,ok := b.(float64)
if ok { // 输出fail 程序继续执行
fmt.Println("success")
fmt.Printf("%T,%v",c,c)
}else{
fmt.Println("fail")
}
fmt.Println("程序继续执行")
}
输出:success float32 1.2
如果对一个接口值有多个实际类型需要判断,推荐使用switch
语句来实现。
示例:
编写一个函数,可以判断输入的参数是什么类型
package main
import "fmt"
type Student struct {
}
func TypeJudge(items ...interface{}) {
for index, v := range items {
switch v.(type) {
case bool:
fmt.Printf("第%d个数的类型为bool,值为%v\n", index+1, v)
case float32:
fmt.Printf("第%d个数的类型为float32,值为%v\n", index+1, v)
case float64:
fmt.Printf("第%d个数的类型为float64,值为%v\n", index+1, v)
case int, int32, int64:
fmt.Printf("第%d个数为整数,值为%v\n", index+1, v)
case string:
fmt.Printf("第%d个数的类型为string,值为%v\n", index+1, v)
case Student:
fmt.Printf("第%d个数的类型为Student,值为%v\n", index+1, v)
case *Student:
fmt.Printf("第%d个数的类型为*Student,值为%v\n", index+1, v)
default:
fmt.Printf("第%d个数无匹配的类型,值为%v\n", index+1, v)
}
}
}
func main() {
var n1 int = 2
var n2 float32 = 1.2
var n3 float64 = 4.5
var n4 bool
var name string = "jack"
n5 := 10
student := Student{}
s := &Student{}
TypeJudge(n1, n2, n3, n4, n5, name, student, s)
}
输出结果:
第1个数为整数,值为2
第2个数的类型为float32,值为1.2
第3个数的类型为float64,值为4.5
第4个数的类型为bool,值为false
第5个数为整数,值为10
第6个数的类型为string,值为jack
第7个数的类型为Student,值为{}
第8个数的类型为*Student,值为&{}
接口注意事项
1.接口本身不能创建实例,但是可以指向一个实现了该接口的自定义变量
type AInterface interface{
say( )
}
type student struct{
Name string
}
func (s student) say( ) {
fmt.Println("aaa")
}
func main( ){
var s student
var a AInterface = s
a.say( )
}
2.接口中所有的方法都没有方法体,即都是没有实现的方法
3.一个自定义类型将某个接口的方法都实现,我们说这个自定义类型实现了该接口
4.只要是自定义数据类型 ,就可以实现接口,不仅仅是结构体类型
type integer int
func (i integer) say( ){
fmt.Println("aaa",i)
}
func main(){
var i integer = 10
var b AInterface = i
b.say( )
5.一个自定义类型可以实现多个接口
6.Goland接口中不能有任何变量
在 Go 语言中,接口(interface)是一种类型,它定义了一组方法的集合,而并不直接包含变量。接口的目的是定义某些行为,而不是存储数据。因此,在接口的定义中不能包含变量。
7.一个接口(比如A接口)可以继承多个别的接口(比如B,C接口)这时如果要实现A接口就必须将B,C接口中的方法也全部实现
8.interface类型默认是一个指针(引用类型)如果没有对interface初始化就使用,那么会输出nil
package main
import "fmt"
type A interface {
say()
}
func main() {
var a A
fmt.Println(a)//<nil>
}
9.空接口interface{}没有任何方法,所以所有类型都实现了空接口,即可以把任何一个变量复制给空接口
type Student struct{}
type T interface {
}
func main() {
var s Student
var t T = s
fmt.Println(t)//{}
var n interface{} = s
var num1 float64 = 8.8
n = num1
fmt.Println(n)//8.8
}