Go 语言中的 interface 接口简介【GO 基础】
〇、接口简介
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。也就是说,接口可以将一种或多种特征归纳到一起,其他不同的对象通过实现此接口,来表示可以具有此类特征,使得不同的类或模块之间进行通信和交互,而不需要了解彼此的具体实现细节,从而提高代码的可重用性和可维护性。此外,接口还可以帮助程序员更好地组织和管理代码。
基于接口如此优秀的基础条件,Go 语言当然也不会或缺它的身影,本文将结合示例来详细介绍下。
一、关于 GO 语言中的接口
1.1 接口的定义
Go 语言提倡面向接口编程。
在 Go 语言中接口(interface)是一种类型,一种抽象的类型。
interface 是一组 method 的集合,是 duck-type programming(鸭式编程,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由当前方法和属性的集合决定) 的一种体现。
接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。(我们并不关心对象是什么类型,或者它到底是不是鸭子,只关心行为)
Go 语言提倡面向接口编程。
任何类型的方法集中,只要实现了该接口中定义的全部对应方法,就表示它“实现”了该接口,无须在该类型上显式声明实现了哪个接口,这称为 Structural Typing。所谓对应方法,是指有相同名称、参数列表(不包括参数名)以及返回值。另外,该类型还可以有其他方法。
关于接口的特点有:
接口只有方法声明,没有实现,没有数据字段。
接口可以匿名嵌入其他接口,或嵌入到结构中。
对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
只有当接口存储的类型和对象都为 nil 时,接口才等于 nil。
接口调用不会做 receiver 的自动转换。
接口同样支持匿名字段方法。
接口也可实现类似 OOP 中的多态。
空接口可以作为任何类型数据的容器。
一个类型可实现多个接口。
接口命名习惯以 er 结尾。
接口的定义格式:
// 接口的定义,每个接口由一个或多个方法定义组成 type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 方法名2( 参数列表2 ) 返回值列表2 ... } // 接口名:使用 type 将接口定义为自定义的类型名。接口名最好要能突出该接口的类型含义。 // 例如,有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer 等。以 er 结尾。 // 方法名:当方法名【首字母是大写】且这个【接口类型名首字母也是大写】时, // 这个方法可以被接口所在的包(package)之外的代码访问。 // 参数列表、返回值列表:参数列表和返回值列表中的【参数变量名可以省略】。 // 示例: type writer interface{ Write([]byte) error } // 就上边声明的这个接口类型,实际上是看不出来它可以做什么, // 唯一知道的就是可以通过它的 Write 方法来做一些事情。
1.2 为什么要使用接口
知道了接口如何定义后,其实还有另一个疑问,那就是为啥要用接口,它能带来哪些便利呢?
如下分别以普通方式和接口方式示例:
package main import "fmt" type Sayer interface { Say() string } type Cat struct{} type Dog struct{} // 因为 Sayer 接口里只有一个 say 方法 // 所以我们只需要给 Dog 和 Cat 分别实现 say 方法就可以实现 Sayer 接口了 func (c Cat) Say() string { return "喵喵喵" } func (d Dog) Say() string { return "汪汪汪" } func main() { // 普通方式 c := Cat{} fmt.Println("猫:", c.Say()) d := Dog{} fmt.Println("狗:", d.Say()) // 接口方式 var ss Sayer // 接口类型变量,能够存储所有实现了该接口的实例 ss = Cat{} fmt.Println("猫:", ss.Say()) ss = Dog{} fmt.Println("狗:", ss.Say()) }
两种方式都可以达到同样的目的,但是很明显接口方式更具可扩展性,当后续又新增了猪牛羊等动物时,无需再新增变量,都可以赋值给接口 Sayer 对象的实例。
接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。
1.3 接口实现的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
例如,上一章节中的示例代码中,接口 Sayer 声明时,只包含一个方法,则 Cat、Dog 只要实现了 Say 方法就算实现了接口。
1.4 值接收者和指针接收者实现接口的区别
值接收者示例代码:
package main import "fmt" type Mover interface { Move() } type dog struct{} func (d dog) Move() { fmt.Println("狗会动") } func main() { var x Mover var wangcai = dog{} // 旺财是 dog 类型 x = wangcai // x 可以接收 dog 类型 x.Move() var fugui = &dog{} // 富贵是 *dog 类型 x = fugui // x 可以接收 *dog 类型 x.Move() }
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是 dog 结构体还是结构体指针 *dog 类型的变量都可以赋值给该接口变量。因为 Go 语言中有对指针类型变量求值的语法糖,dog 指针 fugui 内部会自动求值 *fugui。
但是,当接收者变更为指针后,接口对象就只能接收指针类型了,如果直接接收对象,则会报异常。如下指针接收者的示例代码:
package main import "fmt" type Mover interface { Move() } type dog struct{} func (d *dog) Move() { fmt.Println("狗会动") } func main() { var x Mover // var wangcai = dog{} // 旺财是 dog 类型 // x = wangcai // x 不可以接收 dog 类型 // cannot use wangcai (variable of type dog) as Mover value in assignment: dog does not implement Mover (method Move has pointer receiver)compilerInvalidIfaceAssign x.Move() var fugui = &dog{} // 富贵是 *dog 类型 x = fugui // x 可以接收 *dog 类型 x.Move() }
二、类型与接口的关系
2.1 一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。就可以分别定义 Sayer 接口和 Mover 接口,然后让 Dog 对象分别实现两个接口,代码如下:
package main import ( "fmt" ) // Sayer 接口 type Sayer interface { say() } // Mover 接口 type Mover interface { move() } type Dog struct { name string } // 实现 Sayer 接口 func (d Dog) say() { fmt.Printf("%s会叫\n", d.name) } // 实现 Mover 接口 func (d Dog) move() { fmt.Printf("%s会动\n", d.name) } func main() { var x Sayer var y Mover var a = Dog{name: "旺财"} // Dog 实现了两个接口后,就可以把其复制给各个接口的实例 x = a x.say() // 输出:旺财会叫 y = a y.move() // 输出:旺财会动 }
2.2 多个类型实现同一接口
Go 语言中不同的类型还可以实现同一接口。
下面一段示例代码,狗和汽车都可以移动,它们都可以实现 Mover 接口:
package main import ( "fmt" ) // Mover 接口 type Mover interface { move() } type Dog struct { name string } type Car struct { brand string } // Dog 类型实现 Mover 接口 func (d Dog) move() { fmt.Printf("%s会跑\n", d.name) } // Car 类型实现 Mover 接口 func (c Car) move() { fmt.Printf("%s速度七十迈\n", c.brand) } func main() { var x Mover var a = Dog{name: "旺财"} var b = Car{brand: "比亚迪"} x = a x.move() // 输出:旺财会跑 x = b x.move() // 输出:比亚迪速度七十迈 }
2.3 如何在未全部实现接口方法的条件下,实现赋值
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
要注意的是,嵌套的的结构体和原结构体中实现的方法,需要包含全部接口中的方法。
如下示例代码,声明一个洗衣机接口,包含‘洗’和‘甩干’两个方法。海尔洗衣机实现了‘洗’方法,同时其中的甩干器又实现了‘甩干’方法。最终结果就是海尔洗衣机实现了洗衣机的全部方法,即实现了洗衣机接口:
package main import ( "fmt" ) // WashingMachine 洗衣机 type WashingMachine interface { wash() dry() } // 甩干器 type dryer struct{} // 实现 WashingMachine 接口的 dry() 方法 func (d dryer) dry() { fmt.Println("甩一甩") } // 海尔洗衣机 type haier struct { dryer // 嵌入甩干器 } // 实现 WashingMachine 接口的 wash() 方法 func (h haier) wash() { fmt.Println("洗刷刷") } func main() { var x WashingMachine var a dryer // x = a // dryer 仅实现了接口的一个方法,不算实现了,无法赋值给 x // x.dry() var b = haier{a} x = b x.wash() // 输出:洗刷刷 }
WashingMachine 中的两个方法,在结构体 dryer 和 harer 中得到了全部实现。因此被嵌入的结构体 haier 的实例可以赋值给接口的实例。
2.4 多个接口可以嵌套
接口与接口间可以通过嵌套创造出新的接口。
如下示例代码,接口 animal 中嵌套了两个接口 Sayer 和 Mover:
package main import ( "fmt" ) // Sayer 接口 type Sayer interface { say() } // Mover 接口 type Mover interface { move() } // 接口嵌套 type animal interface { Sayer Mover } type cat struct { name string } func (c cat) say() { fmt.Println("喵喵喵") } func (c cat) move() { fmt.Println("猫会动") } func main() { var x animal var c = cat{name: "花花"} x = c x.move() x.say() }
三、空接口
3.1 空接口的定义
空接口是指没有定义任何方法的接口,因此,任何类型都实现了空接口。所以,空接口类型的变量可以存储任意类型的变量。
如下示例代码,定义一个空接口,分别将其他类型的变量赋值给空接口的实例:
package main import ( "fmt" ) func main() { var x interface{} // 定义一个空接口 x s := "a.string" x = s fmt.Printf("type:%T value:%v\n", x, x) // type:string value:a.string i := 100 x = i fmt.Printf("type:%T value:%v\n", x, x) // type:int value:100 b := true x = b fmt.Printf("type:%T value:%v\n", x, x) // type:bool value:true }
3.2 空接口的应用
可作为函数的参数:
package main import ( "fmt" ) // 空接口作为函数参数 func show(a interface{}) { fmt.Printf("type:%T value:%v\n", a, a) } func main() { var x interface{} // 定义一个空接口 x s := "a.string" x = s show(x) // type:string value:a.string i := 100 x = i show(x) // type:int value:100 b := true x = b show(x) // type:bool value:true }
空接口作为 map 的值:
使用空接口实现可以保存任意类型值的字典。
package main import ( "fmt" ) func main() { var studentInfo = make(map[string]interface{}) // 空接口作为 map 值 studentInfo["name"] = "李白" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo) // map[age:18 married:false name:李白] }
3.3 类型断言
空接口可以存储任意类型的值,那么从中取值的时候怎么判断值类型呢?下面来看下。
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T) // x:表示类型为 interface{} 的变量 // T:表示断言 x 可能是的类型
该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量,第二个值是一个布尔值,若为 true 则表示断言成功,为 false 则表示断言失败。
如下一段简单的示例代码:
package main import "fmt" func main() { var x interface{} x = "测试文本" // 当 x = 1 时,返回“类型断言失败” v, ok := x.(string) if ok { fmt.Println(v) } else { fmt.Println("类型断言失败") } }
如果断言失败,返回值 v 就是指定类型的默认值,比如 int 就是 0,bool 就是 false。
上面的示例中如果要断言多次就需要写多个 if 判断,这个时候我们可以使用 switch 语句来实现:
package main import ( "fmt" ) func main() { var x interface{} i := 10 x = i justifyType(x) } func justifyType(x interface{}) { switch v := x.(type) { case string: fmt.Printf("x is a string,value is %v\n", v) case int: fmt.Printf("x is a int is %v\n", v) case bool: fmt.Printf("x is a bool is %v\n", v) default: fmt.Println("unsupport type!") } } // 输出:x is a int is 10
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是:
只有当有两个或两个以上的具体类型,必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
参考:http://www.topgoer.com/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/%E6%8E%A5%E5%8F%A3.html

本文来自博客园,作者:橙子家,欢迎微信扫码关注博主【橙子家czzj】,有任何疑问欢迎沟通,共同成长!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步