Avatar

本节包括 Go 的接口等内容。

Part 4 Go 接口
2000 words

接口(Interface)是 Go 提供的一种数据类型,用于定义行为的集合。它通过描述类型必须实现的方法,规定类型的行为。

1 定义接口h2

接口和结构体一样,都是数据类型。因此应该使用 type 关键字声明。

type Shape interface {
Area() float64
Perimeter() float64
}

和结构体类似。不过结构体内部是变量,而接口内部是方法(Method)。

这个接口定义了两个方法 Area()Perimeter(),用于求取周长和面积。你可能注意到这里只写了名字,而没有具体的实现逻辑。这是因为接口本身就是用来描述而不是实现

函数和方法的区别

函数和方法在底层设计上几乎是一样的,没有区别。它们唯一的区别在编程设计的角度:

  • 函数是独立的,不属于任何类型,适合通用、逻辑独立的操作;
  • 方法必须绑定到一个具体的类型上(通常是结构体),是类型的行为,适合与类型或数据紧密联系的操作。

而空接口 interface {} 是一个特殊的接口,它不需要任何方法即可被实现。这也就是说,所有的类型都实现了空接口。

空接口可以用来存储任意数据类型

var a interface{}
a = "Hello, World!"
a = 42
a = 3.14
a = true
Go 的 any 类型

在 Go 1.18+ 中,引入了 any 作为空接口的类型别名:

var a any

2 实现接口h2

2.1 接口的隐式实现h3

接口的方法必须要绑定到一个具体的类型上,通常是结构体。因此我们先定义一个圆的结构体:

type Circle struct {
Radius float64
}

这个圆只有一个属性,即半径。接下来,我们让它去实现 Shape 接口。

接口的隐式实现

Go 中的接口是隐式实现的。当一个结构体实现了某一接口的全部方法后,它就自动实现了这个接口。

这也叫做鸭子类型,因为“如果它走起来像鸭子,叫起来也像鸭子,那它就是一只鸭子”。

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}

这样 Circle 结构体就自动实现了 Shape 接口。既然接口是一种类型,那自然就能声明该类型的变量(接口变量)。我们可以这样做:

c := Circle{
Radius: 5
}
var s Shape = c
area := s.Area()
perimeter := s.Perimeter()

注意看 var s Shape = c,你会发现,cCircle 结构体类型,而 sShape 接口类型,这两种不同的类型居然可以赋值。

这是因为由于接口的隐式实现,接口变量可以存储实现了该接口的类型

而这里必须显式声明 s 的类型为 Shape,如果写成 s := c,类型推断会让 sCircle 类型。

2.2 接口值h3

既然提到了接口变量,或者叫接口值,就不得不提一下它的本质。接口变量(接口值)并不是一个简单的变量,而是一个由**动态类型(Dynamic Type)动态值(Dynamic Value)**组成的复合结构。

  • 动态类型:保存存储在接口中变量的原始类型信息;
  • 动态值:保存存储在接口中变量的具体内容,或是指向具体内容的指针。

例如在本例中,接口变量是 s,存储在接口中的变量是 c,因此 s 的动态类型是 Circle,动态值是 {radius: 5}

理解了接口值,就能理解接口值之间的比较:当两个接口值的动态类型一致、且动态值也相等时,两个接口值才相等

不可比较的类型

切片映射等类型不可被比较。如果将动态类型为切片或映射的接口值进行比较,会导致程序崩溃

特别地,接口值也有零值。当你声明一个接口变量但未赋值时,它的动态类型和值都是 nil

var s Shape // s 的类型和值都是 nil
fmt.Println(s == nil) // true

也因此,如果一个接口值不为 nil,并不意味着它的动态值不为 nil

var s *string = nil // s 是一个指向 string 的空指针
var i interface{} = s // 把空指针赋给接口
fmt.Println(i == nil) // 输出: false !!!

3 类型断言和类型切换h2

我们经常需要判断一个接口值的动态类型是否为我们期望的类型,然后才能进一步操作。这时候就需要用到类型断言类型切换

3.1 类型断言h3

类型断言(Type Assertion)用于将接口变量恢复为它的具体类型(Concrete Type)。一个简单的示例是这样的:

var i interface{} = "Hello Go"
s, ok := i.(string)
if ok {
fmt.Println("断言成功,字符串为:", s)
}

在这里,i 本来是一个任意类型的变量,在经过 i.(string) 断言后,得到的 s 就为字符串变量。

如果断言失败,例如 i.(int)ok 就会被赋值为 false,而 s 就会被赋予零值。

特别提醒

断言只能在接口上进行。如果你尝试对一个已知的具体类型进行断言,编译器会报错。

此外,如果接口值是 nil,那么任何类型的断言都会失败。

3.2 类型切换h3

如果你面临多种可能的类型,使用一连串的 if 断言会非常难看。这时应该使用 switch 的变体,即类型切换(Type Switch)

func doSomething(i any) {
switch v := i.(type) {
case int:
fmt.Printf("这是一个整数,它的平方是: %d\n", v*v)
case string:
fmt.Printf("这是一个字符串,长度是: %d\n", len(v))
case bool:
fmt.Printf("这是一个布尔值: %t\n", v)
default:
fmt.Println("未知类型")
}
}

4 多态h2

得益于鸭子类型,可以很方便地实现多态。例如,当我们要在一个支付系统中实现不同的支付方式,就可以使用接口来实现。

首先定义接口,这个接口要求实现一个支付函数 Pay()

type Payer interface {
Pay(amount int)
}

然后定义不同的支付渠道去实现这个接口:

type WeChat struct{}
func (w WeChat) Pay(amount int) {
fmt.Printf("微信支付了 %d 元\n", amount)
}
type Alipay struct{}
func (a Alipay) Pay(amount int) {
fmt.Printf("支付宝支付了 %d 元\n", amount)
}

这里 WeChatAlipay 结构体分别、各自、独立地实现了 Pay() 函数,同时也隐式实现了 Payer 接口。

然后声明一个结算函数,这个函数不关心你用的是微信还是支付宝,你只要是一个 Payer、有 Pay() 函数就行:

func Checkout(p Payer, money int) {
p.Pay(money)
}

当用户想要支付时,只需要选择对应的支付方式即可:

func main() {
payType := "wechat"
payAmount := 200
switch payType {
case "wechat":
payer := WeChat{}
Checkout(payer, payAmount)
case "alipay":
payer := Alipay{}
Checkout(payer, payAmount)
default:
fmt.Println("不支持的支付方式")
}
}

5 组合h2

接口可以通过嵌套组合,实现更复杂的行为描述。

例如对于文件系统,通常涉及到两种操作,而这两种操作组合可以得到只读、只写、读写三种类型。我们完全可以只实现读和写两种最基本的操作,而通过组合实现全部的三种类型:

type Read interface {
ReadData() string
}
type Write interface {
WriteData(data string)
}
type ReadWrite interface {
Read
Write
}

我们知道,Go 的接口是隐式自动实现的。那么当一个结构体定义了 ReadData() 方法和 WriteData() 方法,它就自动实现了 ReadWrite 接口。

但同时,它也会自动实现 Read 接口和 Write 接口。

type DataStore struct {
// existing code
}
func (ds DataStore) ReadData() string {
// existing code
}
func (ds DataStore) WriteData(data string) {
// existing code
}
func main() {
ds := DataStore{}
var rw ReadWrite = ds // 实现了 ReadWrite
var r Read = ds // 也实现了 Read
var w Write = ds // 也实现了 Write
r.ReadData()
w.WriteData("test")
}