前面的文章中,了解到 Go 语言不是一种传统意义上的面向对象语言,因此 Go 没有类和继承的概念。
但是面向对象的功能很强大而且很实用, 前一篇文章中已经了解到可以通过嵌入类型来实现 Has-a 的关系。
这一篇文章将通过学习接口来看到 Go 通过结构体、方法和接口实现面向对象的功能。
在上一篇文章 《 12. Go 方法》 中,我们注意到命名了求矩形的面积的 area()
方法和圆面积的 area()
方法,在实际开发过程中,类似的关系还有很多。Go 语言通过 接口 类型将这些相似直接表示出来。
接口为 Go 语言实现了抽象的功能。接口的主体只能有方法声明和嵌入接口。接口中的方法没有函数体。只能在接口中定义方法签名,我们不能创建接口的对象,接口只为了实现,任何实现类型都需要提供方法的主体。
接口是定义一系列动作和行为的工具。它们帮助对象依赖抽象而不是其他对象的具体实现。我们可以通过对多个接口进行分组来组合不同的行为。
什么是接口
简单来说,接口是一组方法的组合,代表不同数据类型的常见行为。
通过接口,我们可以组织不同的方法组,适用于不同类型的对象。通过这样做,我们的程序可以依靠更高的抽象(接口),而不是具体的实现,允许其他方法与实现同一接口的各种不同对象一起工作。在面向对象的世界里,这个概念被称为依赖倒置原则(Dependency Inversion Principle,SOLID)。
在 Go 中,构建小型接口然后将它们组合在一起以向对象添加更多功能被认为是最佳实践。通过这种方式,您可以保持代码整洁并提高可重用性。
在 Go 中,我们可以在一个结构体(对象)实现其所有方法时自动推断它实现了一个接口。如果某个对象实现了某个接口的所有方法,就表示它实现了该“接口”,无须显式地在该类型上添加接口说明。
定义一个接口
假设我们把圆和矩阵抽象出来 -- 都是形状,因此可以实现一个形状 Shape
的接口,代码如下:
type Shape interface { perimeter() float64 area() float64 }
可以看到接口的定义也是 先使用 type
关键字,中间是接口名,后面是关键字 interface
。但接口里面的主体不是定义字段,而是定义一些“方法集”。
方法集是一系列方法,为了去实现这个接口。即只定义原型而不用实现。
总结,定义接口的基本格式如下:
type interfaceName interface { methodName1(参数列表) (返回值) methodName2(参数列表) (返回值) ... }
Shape
是一个非常简单的接口,它定义了一个 area()
的方法。此方法表示其他对象可以实现的动作或行为。
在矩形和圆结构体中,都有返回面积( float
)的方法,所以都可以实现 Shape
。
Note:
1. Go 语言中,接口命名时习惯性以“er”结尾,比如:Printer、Reader、Writer等,通常以动名词来命名。
2. Go 语言中,一个接口中包含的方法不应该过多,0~3 个即可。
例如:
package main import "fmt" // 定义结构体部分 type Person struct { Name string } type Student struct { Person School string } type Teacher struct { Person Department string } // 定义接口部分 type Speaker() interface { Talk() } type Learner interface { Talk() Study() } // 方法实现接口部分 func (p Person) Talk(){} func (s Student) Talk(){} func (t Student) Talk(){} func (s Student) Study(){}
接口组合
在 Go 语言中,除了类型可以匿名组合,接口也可以组合在一起。将一个接口匿名嵌入到另一个接口中,就是接口组合(Interface-combination)
例如:
type SpeakLearner interface { Speaker Learner }
在接口 SpeakLearner
中组合了 Speaker
和 Learner
两个接口,所以它既能做 Speaker
接口的所有事情,又能做 Learner
接口的所有事情。
接口的赋值
与其他面向对象编程语言不同,Go 语言的接口还可以赋值,如果定义了一个接口的变量,那么这个变量可以存储实现了这个接口的任意类型的对象。
例如上述的 Speaker
接口就可以存储对象 Person、Teacher 和 Student 的值。
package main import "fmt" // 定义结构体部分 type Person struct { Name string } type Student struct { Person School string } type Teacher struct { Person Department string } // 定义接口部分 type Speaker interface { Talk() } type Learner interface { Talk() Study() } // 对象方法实现 func (p Person) Talk() { fmt.Printf("Hi, 我是 %s. 你好~\n", p.Name) } func (s Student) Talk() { fmt.Printf("Hi, 我是 %s. 我是学生,正在 %s 学习.\n ", s.Name, s.School) } func (t Teacher) Talk() { fmt.Printf("Hi, 我是 %s. 我是老师,正在 %s 工作.\n", t.Name, t.Department) } func (s Student) Study() { fmt.Printf("我正在 %s 学习 Go 语言.\n", s.School) } func main() { p1 := Person{"张三"} t1 := Teacher{Person{"Lily"}, "计算机学院"} s1 := Student{Person{"Lee"}, "搬砖大学"} var s Speaker // 定义 Speaker 类型的变量 s s = p1 // s 能存储 People s.Talk() s = t1 // s 能存储 Teacher s.Talk() s = s1 // s 能存储 Student s.Talk() var l Learner l = s1 // l 能存储 Student l.Study() }
运行结果为:
Hi, 我是 张三. 你好~ Hi, 我是 Lily. 我是老师,正在 计算机学院 工作. Hi, 我是 Lee. 我是学生,正在 搬砖大学 学习. 我正在 搬砖大学 学习 Go 语言.
接口作为函数的参数
我们可以将接口类型作为函数的参数。
func totalArea(shapes ...Shape) float64 { var area float64 for _, s := range shapes { area += s.area() } return area }
可以这样调用函数:
fmt.Println(totalArea(&c, &r))
接口也可被用于字段:
type MultiShape struct { shapes []Shape }
我们甚至可以把 MultiShape
本身变成一个 Shape
,通过给它一个 它的面积方法。
func (m *MultiShape) area() float64 { var area float64 for _, s := range m.shapes { area += s.area() } return area }
现在, Multishape
可以包括 Circle
s, Rectangle
s 或者其他 MultiShape
s.
空接口
在 Go 语言中,任何数据类型都默认实现了空接口,空接口可以这样定义:
type InterfaceName interface{ }
空接口也就是包含 0 个方法的接口,所以可以使用空接口定义任何类型的变参函数,如果一个函数返回空接口,就可以返回任意类型的值。
interface{} // 函数 test1 返回 1 个interface{} func test1(a interface{}) {} // 函数 test2 可返回多个 interface{} func test2(a ... interface{}) {}
空接口可以存储结构体、字符串、整数等任何类型。空接口增强了代码的扩展性与通用性。
Notes:
平时使用的输入输出函数 fmt.Println 的参数就是一个空接口,正因为如此,Println 函数可以打印多种类型
func Println(a ...interface{}) (n int, err error)
Go 语言中的空接口的作用类似于 C 语言中的 void *
、Java/C# 中的 System.Object
。
接口的比较
两个接口可以通过 == 或 != 进行比较,例如:
var a, b interface{} fmt.Println(a == b )
接口的比较规则:
- 动态值为 nil 的接口变量总是相等的
- 如果只有 1 个接口为 nil,那么比较结果总是 false
- 如果两个接口不为 nil 且接口变量具有相同的动态类型和动态类型值,那么两个接口是相同的
- 如果接口存储的动态类型值是不可比较的,那么在运行时会报错
总结
- 一个接口中的方法没有主体。一个接口的所有方法都是纯粹的抽象的。
- 一个接口的方法可以有参数和返回类型。任何自定义类型都可以实现该接口。
- 没有明确的语法来实现一个接口。当任何类型定义了接口的所有方法,那么该类型就隐含实现了该接口。
- 一个实现了接口的类型也可以有其他方法
- 一个类型可以实现一个以上的接口
- 一个接口可以由许多类型实现
- 当一个接口没有任何方法时,它被称为空接口。一个空接口在默认情况下是由所有类型实现
- 一个接口可以作为结构的一个字段使用。使用时,我们需要在创建结构对象时传递接口的实际实现。创建结构体对象时传递接口的实际实现。
- 在 Go 中,多态性可以用一个接口来实现。