http://c.biancheng.net/golang/interface/
Go语言接口声明(定义)
Go语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。
但是Go语言里有非常灵活的接口概念,通过它可以实现很多面向对象的特性。很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。
这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。
接口声明的格式
每个接口类型由数个方法组成。接口的形式代码如下:
- type 接口类型名 interface{
- 方法名1( 参数列表1 ) 返回值列表1
- 方法名2( 参数列表2 ) 返回值列表2
- …
- }
对各个部分的说明:
- 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
- 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如:
- type writer interface{
- Write([]byte) error
- }
开发中常见的接口及写法
Go语言提供的很多包中都有接口,例如 io 包中提供的 Writer 接口:
- type Writer interface {
- Write(p []byte) (n int, err error)
- }
这个接口可以调用 Write() 方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)。
类似的,还有将一个对象以字符串形式展现的接口,只要实现了这个接口的类型,在调用 String() 方法时,都可以获得对象对应的字符串。在 fmt 包中定义如下:
- type Stringer interface {
- String() string
- }
Stringer 接口在Go语言中的使用频率非常高,功能类似于 Java 或者 C# 语言里的 ToString 的操作。
Go语言的每个接口中的方法数量不会很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。本章后面的小节中会介绍如何使用组合来扩充接口。
Go语言实现接口的条件
如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。T 可以是一个非接口类型,也可以是一个接口类型。
实现关系在Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go语言中没有类似于 implements 的关键字。 Go编译器将自动在需要的时候检查两个类型之间的实现关系。
接口定义后,需要实现接口,调用方才能正确编译通过并使用接口。接口的实现需要遵循两条规则才能让接口可用。
接口被实现的条件一:接口的方法与实现接口的类型方法格式一致
在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
为了抽象数据写入的过程,定义 DataWriter 接口来描述数据写入需要实现的方法,接口中的 WriteData() 方法表示将数据写入,写入方无须关心写入到哪里。实现接口的类型实现 WriteData 方法时,会具体编写将数据写入到什么结构中。这里使用file结构体实现 DataWriter 接口的 WriteData 方法,方法内部只是打印一个日志,表示有数据写入,详细实现过程请参考下面的代码。
数据写入器的抽象:
- package main
- import (
- "fmt"
- )
- // 定义一个数据写入器
- type DataWriter interface {
- WriteData(data interface{}) error
- }
- // 定义文件结构,用于实现DataWriter
- type file struct {
- }
- // 实现DataWriter接口的WriteData方法
- func (d *file) WriteData(data interface{}) error {
- // 模拟写入数据
- fmt.Println("WriteData:", data)
- return nil
- }
- func main() {
- // 实例化file
- f := new(file)
- // 声明一个DataWriter的接口
- var writer DataWriter
- // 将接口赋值f,也就是*file类型
- writer = f
- // 使用DataWriter接口进行数据写入
- writer.WriteData("data")
- }
代码说明如下:
- 第 8 行,定义 DataWriter 接口。这个接口只有一个方法,即 WriteData(),输入一个 interface{} 类型的 data,返回一个 error 结构表示可能发生的错误。
- 第 17 行,file 的 WriteData() 方法使用指针接收器。输入一个 interface{} 类型的 data,返回 error。
- 第 27 行,实例化 file 赋值给 f,f 的类型为 *file。
- 第 30 行,声明 DataWriter 类型的 writer 接口变量。
- 第 33 行,将 *file 类型的 f 赋值给 DataWriter 接口的 writer,虽然两个变量类型不一致。但是 writer 是一个接口,且 f 已经完全实现了 DataWriter() 的所有方法,因此赋值是成功的。
- 第 36 行,DataWriter 接口类型的 writer 使用 WriteData() 方法写入一个字符串。
运行代码,输出如下:
WriteData: data
本例中调用及实现关系如下图所示。
图:WriteWriter的实现过程
当类型无法实现接口时,编译器会报错,下面列出常见的几种接口无法实现的错误。
1) 函数名不一致导致的报错
在以上代码的基础上尝试修改部分代码,造成编译错误,通过编译器的报错理解如何实现接口的方法。首先,修改 file 结构的 WriteData() 方法名,将这个方法签名(第17行)修改为:
- func (d *file) WriteDataX(data interface{}) error {
编译代码,报错:
cannot use f (type *file) as type DataWriter in assignment:
*file does not implement DataWriter (missing WriteData method)
报错的位置在第 33 行。报错含义是:不能将 f 变量(类型*file)视为 DataWriter 进行赋值。原因:*file 类型未实现 DataWriter 接口(丢失 WriteData 方法)。
WriteDataX 方法的签名本身是合法的。但编译器扫描到第 33 行代码时,发现尝试将 *file 类型赋值给 DataWriter 时,需要检查 *file 类型是否完全实现了 DataWriter 接口。显然,编译器因为没有找到 DataWriter 需要的 WriteData() 方法而报错。
2) 实现接口的方法签名不一致导致的报错
将修改的代码恢复后,再尝试修改 WriteData() 方法,把 data 参数的类型从 interface{} 修改为 int 类型,代码如下:
- func (d *file) WriteData(data int) error {
编译代码,报错:
cannot use f (type *file) as type DataWriter in assignment:
*file does not implement DataWriter (wrong type for WriteData method)
have WriteData(int) error
want WriteData(interface {}) error
这次未实现 DataWriter 的理由变为(错误的 WriteData() 方法类型)发现 WriteData(int)error,期望 WriteData(interface{})error。
这种方式的报错就是由实现者的方法签名与接口的方法签名不一致导致的。
接口被实现的条件二:接口中所有方法均被实现
当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
在本节开头的代码中,为 DataWriter中 添加一个方法,代码如下:
- // 定义一个数据写入器
- type DataWriter interface {
- WriteData(data interface{}) error
- // 能否写入
- CanWrite() bool
- }
新增 CanWrite() 方法,返回 bool。此时再次编译代码,报错:
cannot use f (type *file) as type DataWriter in assignment:
*file does not implement DataWriter (missing CanWrite method)
需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。
Go语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。
实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译。
提示
传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。经常可以看到大型软件极为复杂的派生树。随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。
对于Go语言来说,非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。因此,使用GO语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”。
Go语言类型与接口的关系
在Go语言中类型和接口之间有一对多和多对一的关系,下面将列举出这些常见的概念,以方便读者理解接口与类型在复杂环境下的实现关系。
一个类型可以实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
网络上的两个程序通过一个双向的通信连接实现数据的交换,连接的一端称为一个 Socket。Socket 能够同时读取和写入数据,这个特性与文件类似。因此,开发中把文件和 Socket 都具备的读写特性抽象为独立的读写器概念。
Socket 和文件一样,在使用完毕后,也需要对资源进行释放。
把 Socket 能够写入数据和需要关闭的特性使用接口来描述,请参考下面的代码:
- type Socket struct {
- }
- func (s *Socket) Write(p []byte) (n int, err error) {
- return 0, nil
- }
- func (s *Socket) Close() error {
- return nil
- }
Socket 结构的 Write() 方法实现了 io.Writer 接口:
- type Writer interface {
- Write(p []byte) (n int, err error)
- }
同时,Socket 结构也实现了 io.Closer 接口:
- type Closer interface {
- Close() error
- }
使用 Socket 实现的 Writer 接口的代码,无须了解 Writer 接口的实现者是否具备 Closer 接口的特性。同样,使用 Closer 接口的代码也并不知道 Socket 已经实现了 Writer 接口,如下图所示。
图:接口的使用和实现过程
在代码中使用 Socket 结构实现的 Writer 接口和 Closer 接口代码如下:
- // 使用io.Writer的代码, 并不知道Socket和io.Closer的存在
- func usingWriter( writer io.Writer){
- writer.Write( nil )
- }
- // 使用io.Closer, 并不知道Socket和io.Writer的存在
- func usingCloser( closer io.Closer) {
- closer.Close()
- }
- func main() {
- // 实例化Socket
- s := new(Socket)
- usingWriter(s)
- usingCloser(s)
- }
usingWriter() 和 usingCloser() 完全独立,互相不知道对方的存在,也不知道自己使用的接口是 Socket 实现的。
多个类型可以实现相同的接口
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。
Service 接口定义了两个方法:一个是开启服务的方法(Start()),一个是输出日志的方法(Log())。使用 GameService 结构体来实现 Service,GameService 自己的结构只能实现 Start() 方法,而 Service 接口中的 Log() 方法已经被一个能输出日志的日志器(Logger)实现了,无须再进行 GameService 封装,或者重新实现一遍。所以,选择将 Logger 嵌入到 GameService 能最大程度地避免代码冗余,简化代码结构。详细实现过程如下:
- // 一个服务需要满足能够开启和写日志的功能
- type Service interface {
- Start() // 开启服务
- Log(string) // 日志输出
- }
- // 日志器
- type Logger struct {
- }
- // 实现Service的Log()方法
- func (g *Logger) Log(l string) {
- }
- // 游戏服务
- type GameService struct {
- Logger // 嵌入日志器
- }
- // 实现Service的Start()方法
- func (g *GameService) Start() {
- }
代码说明如下:
- 第 2 行,定义服务接口,一个服务需要实现 Start() 方法和日志方法。
- 第 8 行,定义能输出日志的日志器结构。
- 第 12 行,为 Logger 添加 Log() 方法,同时实现 Service 的 Log() 方法。
- 第 17 行,定义 GameService 结构。
- 第 18 行,在 GameService 中嵌入 Logger 日志器,以实现日志功能。
- 第 22 行,GameService 的 Start() 方法实现了 Service 的 Start() 方法。
此时,实例化 GameService,并将实例赋给 Service,代码如下:
- var s Service = new(GameService)
- s.Start()
- s.Log(“hello”)
s 就可以使用 Start() 方法和 Log() 方法,其中,Start() 由 GameService 实现,Log() 方法由 Logger 实现。
Go语言排序(借助sort.Interface接口)
排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要 15 行就可以搞定,但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。
幸运的是,sort 包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。
相比之下,Go语言的 sort.Sort 函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型 sort.Interface 来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。
一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是 sort.Interface 的三个方法:
- package sort
- type Interface interface {
- Len() int // 获取元素数量
- Less(i, j int) bool // i,j是序列元素的指数。
- Swap(i, j int) // 交换元素
- }
为了对序列进行排序,我们需要定义一个实现了这三个方法的类型,然后对这个类型的一个实例应用 sort.Sort 函数。思考对一个字符串切片进行排序,这可能是最简单的例子了。下面是这个新的类型 MyStringList 和它的 Len,Less 和 Swap 方法
- type MyStringList []string
- func (p MyStringList ) Len() int { return len(m) }
- func (p MyStringList ) Less(i, j int) bool { return m[i] < m[j] }
- func (p MyStringList ) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
使用sort.Interface接口进行排序
对一系列字符串进行排序时,使用字符串切片([]string)承载多个字符串。使用 type 关键字,将字符串切片([]string)定义为自定义类型 MyStringList。为了让 sort 包能识别 MyStringList,能够对 MyStringList 进行排序,就必须让 MyStringList 实现 sort.Interface 接口。
下面是对字符串排序的详细代码(代码1):
- package main
- import (
- "fmt"
- "sort"
- )
- // 将[]string定义为MyStringList类型
- type MyStringList []string
- // 实现sort.Interface接口的获取元素数量方法
- func (m MyStringList) Len() int {
- return len(m)
- }
- // 实现sort.Interface接口的比较元素方法
- func (m MyStringList) Less(i, j int) bool {
- return m[i] < m[j]
- }
- // 实现sort.Interface接口的交换元素方法
- func (m MyStringList) Swap(i, j int) {
- m[i], m[j] = m[j], m[i]
- }
- func main() {
- // 准备一个内容被打乱顺序的字符串切片
- names := MyStringList{
- "3. Triple Kill",
- "5. Penta Kill",
- "2. Double Kill",
- "4. Quadra Kill",
- "1. First Blood",
- }
- // 使用sort包进行排序
- sort.Sort(names)
- // 遍历打印结果
- for _, v := range names {
- fmt.Printf("%s\n", v)
- }
- }
代码输出结果:
1. First Blood
2. Double Kill
3. Triple Kill
4. Quadra Kill
5. Penta Kill
代码说明如下:
- 第 9 行,接口实现不受限于结构体,任何类型都可以实现接口。要排序的字符串切片 []string 是系统定制好的类型,无法让这个类型去实现 sort.Interface 排序接口。因此,需要将 []string 定义为自定义的类型。
- 第 12 行,实现获取元素数量的 Len() 方法,返回字符串切片的元素数量。
- 第 17 行,实现比较元素的 Less() 方法,直接取 m 切片的 i 和 j 元素值进行小于比较,并返回比较结果。
- 第 22 行,实现交换元素的 Swap() 方法,这里使用Go语言的多变量赋值特性实现元素交换。
- 第 29 行,由于将 []string 定义成 MyStringList 类型,字符串切片初始化的过程等效于下面的写法:
- names := []string {
- "3. Triple Kill",
- "5. Penta Kill",
- "2. Double Kill",
- "4. Quadra Kill",
- "1. First Blood",
- }
- 第 38 行,使用 sort 包的 Sort() 函数,将 names(MyStringList类型)进行排序。排序时,sort 包会通过 MyStringList 实现的 Len()、Less()、Swap() 这 3 个方法进行数据获取和修改。
- 第 41 行,遍历排序好的字符串切片,并打印结果。
Go语言接口的嵌套组合
在Go语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口。
一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用。
系统包中的接口嵌套组合
Go语言的 io 包中定义了写入器(Writer)、关闭器(Closer)和写入关闭器(WriteCloser)3 个接口,代码如下:
- type Writer interface {
- Write(p []byte) (n int, err error)
- }
- type Closer interface {
- Close() error
- }
- type WriteCloser interface {
- Writer
- Closer
- }
代码说明如下:
- 第 1 行定义了写入器(Writer),如这个接口较为常用,常用于 I/O 设备的数据写入。
- 第 5 行定义了关闭器(Closer),如有非托管内存资源的对象,需要用关闭的方法来实现资源释放。
- 第 9 行定义了写入关闭器(WriteCloser),这个接口由 Writer 和 Closer 两个接口嵌入。也就是说,WriteCloser 同时拥有了 Writer 和 Closer 的特性。
在代码中使用接口嵌套组合
在代码中使用 io.Writer、io.Closer 和 io.WriteCloser 这 3 个接口时,只需要按照接口实现的规则实现 io.Writer 接口和 io.Closer 接口即可。而 io.WriteCloser 接口在使用时,编译器会根据接口的实现者确认它们是否同时实现了 io.Writer 和 io.Closer 接口,详细实现代码如下:
- package main
- import (
- "io"
- )
- // 声明一个设备结构
- type device struct {
- }
- // 实现io.Writer的Write()方法
- func (d *device) Write(p []byte) (n int, err error) {
- return 0, nil
- }
- // 实现io.Closer的Close()方法
- func (d *device) Close() error {
- return nil
- }
- func main() {
- // 声明写入关闭器, 并赋予device的实例
- var wc io.WriteCloser = new(device)
- // 写入数据
- wc.Write(nil)
- // 关闭设备
- wc.Close()
- // 声明写入器, 并赋予device的新实例
- var writeOnly io.Writer = new(device)
- // 写入数据
- writeOnly.Write(nil)
- }
代码说明如下:
- 第 8 行定义了 device 结构体,用来模拟一个虚拟设备,这个结构会实现前面提到的 3 种接口。
- 第 12 行,实现了 io.Writer 的 Write() 方法。
- 第 17 行,实现了 io.Closer 的 Close() 方法。
- 第 24 行,对 device 实例化,由于 device 实现了 io.WriteCloser 的所有嵌入接口,因此 device 指针就会被隐式转换为 io.WriteCloser 接口。
- 第 27 行,调用了 wc(io.WriteCloser接口)的 Write() 方法,由于 wc 被赋值 *device,因此最终会调用 device 的 Write() 方法。
- 第 30 行,与 27 行类似,最终调用 device 的 Close() 方法。
- 第 33 行,再次创建一个 device 的实例,writeOnly 是一个 io.Writer 接口,这个接口只有 Write() 方法。
- 第 36 行,writeOnly 只能调用 Write() 方法,没有 Close() 方法。
为了整理思路,将上面的实现、调用关系使用图方式来展现,参见图 1 和图 2。
1) io.WriteCloser的实现及调用过程如图 1 所示。
图1:io.WriteCloser 的实现及调用过程
2) io.Writer 的实现调用过程如图 2 所示。
图2:io.Write 的实现及调用过程
给 io.WriteCloser 或 io.Writer 更换不同的实现者,可以动态地切换实现代码。
Go语言类型分支(switch判断空接口中变量的类型)
type-switch 流程控制的语法或许是Go语言中最古怪的语法。 它可以被看作是类型断言的增强版。它和 switch-case 流程控制代码块有些相似。 一个 type-switch 流程控制代码块的语法如下所示:
- switch t := areaIntf.(type) {
- case *Square:
- fmt.Printf("Type Square %T with value %v\n", t, t)
- case *Circle:
- fmt.Printf("Type Circle %T with value %v\n", t, t)
- case nil:
- fmt.Printf("nil value: nothing to check?\n")
- default:
- fmt.Printf("Unexpected type %T\n", t)
- }
输出结构如下:
Type Square *main.Square with value &{5}
变量 t 得到了 areaIntf 的值和类型, 所有 case 语句中列举的类型(nil 除外)都必须实现对应的接口,如果被检测类型没有在 case 语句列举的类型中,就会执行 default 语句。
如果跟随在某个 case 关键字后的条目为一个非接口类型(用一个类型名或类型字面表示),则此非接口类型必须实现了断言值 x 的(接口)类型。
类型断言的书写格式
switch 实现类型分支时的写法格式如下:
- switch 接口变量.(type) {
- case 类型1:
- // 变量是类型1时的处理
- case 类型2:
- // 变量是类型2时的处理
- …
- default:
- // 变量不是所有case中列举的类型时的处理
- }
对各个部分的说明:
- 接口变量:表示需要判断的接口类型的变量。
- 类型1、类型2……:表示接口变量可能具有的类型列表,满足时,会指定 case 对应的分支进行处理。
使用类型分支判断基本类型
下面的例子将一个 interface{} 类型的参数传给 printType() 函数,通过 switch 判断 v 的类型,然后打印对应类型的提示,代码如下:
- package main
- import (
- "fmt"
- )
- func printType(v interface{}) {
- switch v.(type) {
- case int:
- fmt.Println(v, "is int")
- case string:
- fmt.Println(v, "is string")
- case bool:
- fmt.Println(v, "is bool")
- }
- }
- func main() {
- printType(1024)
- printType("pig")
- printType(true)
- }
代码输出如下:
1024 is int
pig is string
true is bool
代码第 9 行中,v.(type) 就是类型分支的典型写法。通过这个写法,在 switch 的每个 case 中写的将是各种类型分支。
代码经过 switch 时,会判断 v 这个 interface{} 的具体类型从而进行类型分支跳转。
switch 的 default 也是可以使用的,功能和其他的 switch 一致。
使用类型分支判断接口类型
多个接口进行类型断言时,可以使用类型分支简化判断过程。
现在电子支付逐渐成为人们普遍使用的支付方式,电子支付相比现金支付具备很多优点。例如,电子支付能够刷脸支付,而现金支付容易被偷等。使用类型分支可以方便地判断一种支付方法具备哪些特性,具体请参考下面的代码。
电子支付和现金支付:
- package main
- import "fmt"
- // 电子支付方式
- type Alipay struct {
- }
- // 为Alipay添加CanUseFaceID()方法, 表示电子支付方式支持刷脸
- func (a *Alipay) CanUseFaceID() {
- }
- // 现金支付方式
- type Cash struct {
- }
- // 为Cash添加Stolen()方法, 表示现金支付方式会出现偷窃情况
- func (a *Cash) Stolen() {
- }
- // 具备刷脸特性的接口
- type CantainCanUseFaceID interface {
- CanUseFaceID()
- }
- // 具备被偷特性的接口
- type ContainStolen interface {
- Stolen()
- }
- // 打印支付方式具备的特点
- func print(payMethod interface{}) {
- switch payMethod.(type) {
- case CantainCanUseFaceID: // 可以刷脸
- fmt.Printf("%T can use faceid\n", payMethod)
- case ContainStolen: // 可能被偷
- fmt.Printf("%T may be stolen\n", payMethod)
- }
- }
- func main() {
- // 使用电子支付判断
- print(new(Alipay))
- // 使用现金判断
- print(new(Cash))
- }
代码说明如下:
- 第 6~19 行,分别定义 Alipay 和 Cash 结构,并为它们添加具备各自特点的方法。
- 第 22~29 行,定义两种特性,即刷脸和被偷。
- 第 32 行,传入支付方式的接口。
- 第 33 行,使用类型分支进行支付方法的特性判断。
- 第 34~37 行,分别对刷脸和被偷的特性进行打印。
运行代码,输出如下:
*main.Alipay can use faceid
*main.Cash may be stolen
Go语言error接口:返回错误信息
错误处理在每个编程语言中都是一项重要内容,通常开发中遇到的分为异常与错误两种,Go语言中也不例外。本节我们主要来学习一下Go语言中的错误处理。
在C语言中通过返回 -1 或者 NULL 之类的信息来表示错误,但是对于使用者来说,如果不查看相应的 API 说明文档,根本搞不清楚这个返回值究竟代表什么意思,比如返回 0 是成功还是失败?
针对这样的情况,Go语言中引入 error 接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含 error。error 处理过程类似于C语言中的错误码,可逐层返回,直到被处理。
error 基本用法
Go语言中返回的 error 类型究竟是什么呢?查看Go语言的源码就会发现 error 类型是一个非常简单的接口类型,如下所示:
- // The error built-in interface type is the conventional interface for
- // representing an error condition, with the nil value representing no error.
- type error interface {
- Error() string
- }
error 接口有一个签名为 Error() string 的方法,所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述,在使用 fmt.Println 打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。
一般情况下,如果函数需要返回错误,就将 error 作为多个返回值中的最后一个(但这并非是强制要求)。
创建一个 error 最简单的方法就是调用 errors.New 函数,它会根据传入的错误信息返回一个新的 error,示例代码如下:
- package main
- import (
- "errors"
- "fmt"
- "math"
- )
- func Sqrt(f float64) (float64, error) {
- if f < 0 {
- return -1, errors.New("math: square root of negative number")
- }
- return math.Sqrt(f), nil
- }
- func main() {
- result, err := Sqrt(-13)
- if err != nil {
- fmt.Println(err)
- } else {
- fmt.Println(result)
- }
- }
运行结果如下:
math: square root of negative number
上面代码中简单介绍了使用 errors.New 来返回一个错误信息,与其他语言的异常相比,Go语言的方法相对更加容易、直观。
自定义错误类型
除了上面的 errors.New 用法之外,我们还可以使用 error 接口自定义一个 Error() 方法,来返回自定义的错误信息。
- package main
- import (
- "fmt"
- "math"
- )
- type dualError struct {
- Num float64
- problem string
- }
- func (e dualError) Error() string {
- return fmt.Sprintf("Wrong!!!,because \"%f\" is a negative number", e.Num)
- }
- func Sqrt(f float64) (float64, error) {
- if f < 0 {
- return -1, dualError{Num: f}
- }
- return math.Sqrt(f), nil
- }
- func main() {
- result, err := Sqrt(-13)
- if err != nil {
- fmt.Println(err)
- } else {
- fmt.Println(result)
- }
- }
运行结果如下:
Wrong!!!,because "-13.000000" is a negative number