前言
Go 语言中的接口是其核心特性之一, Go 的接口不像许多面向对象语言中的接口那样需要显式地声明"实现"某个接口, 而是通过 隐式实现 来实现接口的契约。这篇文章就深入探讨一下 Go 的接口类型。
1、接口的基础概念
1.1 接口的定义
接口是一组方法的集合,一个类型实现了接口所要求的所有方法,就自动实现了该接口,无需显式声明。 感觉和 Python 的灵活的子类化机制有点类似。
接口的定义形式如下:
scss
type InterfaceName interface {
Method1() returnType
Method2() returnType
// ...
}
Go 中的接口和其他语言的接口有所不同,最主要的特点是接口的 隐式实现。 即使没有在结构体中声明"实现了某个接口",只要结构体实现了接口中的所有方法,那么它就自动实现了该接口。
1.2 接口的基本应用
看下面这个例子:
go
type Pet interface {
Name() string
Category() string
}
type Dog struct {
name string
}
func (dog Dog) Name() string {
return dog.name
}
func (dog Dog) Category() string {
return "dog"
}
func main() {
dog := Dog{"little pig"}
fmt.Printf("The dog's name is %q.\n", dog.Name()) // The dog's name is "little pig".
var pet Pet = dog
fmt.Printf("This pet is a %s, the name is %q.\n", pet.Category(), pet.Name()) // This pet is a dog, the name is "little pig".
}
代码解析:
- 在上面的代码中,
Pet
是一个接口,包含了两个个方法Name
和Category
。 Dog
类型实现了Name
和Category
方法,自动满足了Pet
接口,因此可以声明并初始化一个Dog
类型的变量dog
, 并把这个变量赋给一个Pet
类型的变量pet
。- 虽然
Dog
没有显式声明"实现了Pet
接口,而是因为实现了接口中的方法,因此自动符合接口。
2、空接口
Go 语言中有一个非常重要的接口类型------空接口 (interface{}
)。 空接口可以存储任何类型的值,因为空接口没有方法,也就没有任何的限制。因此,空接口通常用于存储不确定类型的数据。
2.1 空接口的使用
scss
func printAnything(i interface{}) {
fmt.Println(i)
}
func main() {
printAnything(42) // 输出:42
printAnything("Hello") // 输出:Hello
printAnything(3.14) // 输出:3.14
}
代码解析:
printAnything
是一个函数,接受一个参数i
,它的类型是interface{}
,即空接口。main
函数中,分别传入整数值、字符串值、浮点数值进行测试
2.2 类型断言
虽然空接口可以接受任何类型的值,但有时我们需要从空接口中恢复出具体的类型,这时我们就需要使用类型断言。
go
package main
import "fmt"
func assertType(i interface{}) {
v, ok := i.(int) // 类型断言,检查 i 是否是 int 类型
if ok {
fmt.Println("Integer value:", v)
} else {
fmt.Println("Not an integer!")
}
}
func main() {
assertType(42) // 输出:Integer value: 42
assertType("Hello") // 输出:Not an integer!
}
在这个例子中, i.(int)
是类型断言,它判断 i
是否是 int
类型,并将其转换为 int
类型。 如果不是 int
类型,断言会失败。
3、接口和多态
接口是 Go 语言实现多态的关键。通过接口,一个函数可以接受不同类型的参数,而无需关心这些类型的具体实现。
3.1 多态的示例
1.2
模块中代码进行改造,如下:
go
type Pet interface {
Name() string
Category() string
}
type Dog struct {
name string
}
type Cat struct {
name string
}
func (dog Dog) Name() string {
return dog.name
}
func (dog Dog) Category() string {
return "dog"
}
func (dog Cat) Name() string {
return dog.name
}
func (dog Cat) Category() string {
return "cat"
}
func PrintPet(pet Pet) {
fmt.Printf("This pet is a %s, the name is %q.\n", pet.Category(), pet.Name())
}
func main() {
dog := Dog{"tom-dog"}
cat := Cat{"tom-cat"}
PrintPet(dog) // This pet is a dog, the name is "tom-dog".
PrintPet(cat) // This pet is a cat, the name is "tom-cat".
}
代码解析:
Dog
和Cat
都实现了Name
和Category
方法,因而都满足了Pet
接口。PrintPet
函数接受Pet
类型的参数,并可以接受任何实现了Pet
接口的类型。 这样,函数就表现出多态的行为。
4、接口类型的零值和 nil
在 Go 中,接口类型的零值是 nil
,这意味着接口变量可以是 nil
,表示它既没有绑定类型,也没有具体的值。
4.1 接口零值的示例
go
type Pet interface {
Name() string
}
type Dog struct {
name string
}
func (d Dog) Name() string {
return d.name
}
func main() {
var pet Pet
fmt.Println(pet == nil) // true
var dog *Dog
fmt.Println(dog == nil) // true
pet = dog
fmt.Println(pet == nil) // false
}
代码解析:
- 接口变量
pet
默认值为nil
,即它没有绑定任何具体类型。 - 如果接口变量
pet
指向一个实现了接口的类型,那么pet
就不再是nil
。
4.2 深入理解为啥dog == nil
,赋值给 pet
, pet
不为nil
?
先理解两个名词:动态值 和 动态类型。
上面代码中,我们把指针变量 dog
的值赋给变量 pet
,这个结果值就是变量 pet
的动态值,而结果值的类型 *Dog
就是 变量 pet
的动态类型。对于变量 pet
来讲,它的静态类型永远是 Pet
,但它的动态类型是随着赋给它的值而变化的。
当我们把 dog
的值赋值给变量 pet
时, dog
的值会先被复制,这里是 nil
, 没必要复制。然后,Go 会使用专用 数据结构 iface
的实例包装这个dog
的值的副本,虽然被包装的动态值是 nil
,但 pet
的值不会是 nil
,因为 这个动态值只是 pet
值的一部分而已, pet
的动态类型已经存在了,就是 *dog
.
我们可以使用 reflect
包来验证一下:
css
t := reflect.TypeOf(pet)
fmt.Println("type:", t) // type: *main.Dog
v := reflect.ValueOf(pet)
fmt.Println("value:", v) // value: <nil>
最后
这篇文章主要阐述了 Go
语言接口类型的几个特点:隐式实现、多态、空接口等,另外深入探讨了接口变量赋值过程发生了什么,也就是 4.2
中的内容。 接口的这些特性使得 Go
语言在处理多态和可扩展性方面非常强大,能够支持各种灵活的设计模式。