本节包括 Go 的接口等内容。
接口(Interface)是 Go 提供的一种数据类型,用于定义行为的集合。它通过描述类型必须实现的方法,规定类型的行为。
1 定义接口h2
接口和结构体一样,都是数据类型。因此应该使用 type 关键字声明。
type Shape interface { Area() float64 Perimeter() float64}和结构体类似。不过结构体内部是变量,而接口内部是方法(Method)。
这个接口定义了两个方法 Area() 和 Perimeter(),用于求取周长和面积。你可能注意到这里只写了名字,而没有具体的实现逻辑。这是因为接口本身就是用来描述而不是实现。
函数和方法在底层设计上几乎是一样的,没有区别。它们唯一的区别在编程设计的角度:
- 函数是独立的,不属于任何类型,适合通用、逻辑独立的操作;
- 方法必须绑定到一个具体的类型上(通常是结构体),是类型的行为,适合与类型或数据紧密联系的操作。
而空接口 interface {} 是一个特殊的接口,它不需要任何方法即可被实现。这也就是说,所有的类型都实现了空接口。
空接口可以用来存储任意数据类型。
var a interface{}
a = "Hello, World!"a = 42a = 3.14a = trueany 类型在 Go 1.18+ 中,引入了 any 作为空接口的类型别名:
var a any2 实现接口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 = carea := s.Area()perimeter := s.Perimeter()注意看 var s Shape = c,你会发现,c 是 Circle 结构体类型,而 s 是 Shape 接口类型,这两种不同的类型居然可以赋值。
这是因为由于接口的隐式实现,接口变量可以存储实现了该接口的类型。
而这里必须显式声明 s 的类型为 Shape,如果写成 s := c,类型推断会让 s 是 Circle 类型。
2.2 接口值h3
既然提到了接口变量,或者叫接口值,就不得不提一下它的本质。接口变量(接口值)并不是一个简单的变量,而是一个由**动态类型(Dynamic Type)和动态值(Dynamic Value)**组成的复合结构。
- 动态类型:保存存储在接口中变量的原始类型信息;
- 动态值:保存存储在接口中变量的具体内容,或是指向具体内容的指针。
例如在本例中,接口变量是 s,存储在接口中的变量是 c,因此 s 的动态类型是 Circle,动态值是 {radius: 5}。
理解了接口值,就能理解接口值之间的比较:当两个接口值的动态类型一致、且动态值也相等时,两个接口值才相等。
切片和映射等类型不可被比较。如果将动态类型为切片或映射的接口值进行比较,会导致程序崩溃。
特别地,接口值也有零值。当你声明一个接口变量但未赋值时,它的动态类型和值都是 nil。
var s Shape // s 的类型和值都是 nilfmt.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)}这里 WeChat 和 Alipay 结构体分别、各自、独立地实现了 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")}