[toc] > Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。但是 Go 语言里有非常灵活的`接口`概念,通过它可以实现很多面向对象的特性 # 1. 什么是interface(接口) `简单地说,interface是一组method的组合,但是这些method不包含(实现)代码,我们通过interface来定义对象的一组行为。` > 接口里不能包含变量 # 2. 接口声明 ``` type Namer interface { Method1(参数列表1) 返回值列表1 Method2(参数列表2) 返回值列表 ... } ``` 示例 ``` // 变量名未忽略 type writer interface{ Write(p []byte) (n int, err error) } // 变量名被忽略 type writer interface{ Write([]byte) (int, error) } ``` # 3. 接口规范 - 接口类型名:方法的接口名,<font color=red>由方法名加 [e]r 后缀组成</font>,例如 Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头。 - 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。 - 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略, - Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法 - 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态和高内聚低偶合的思想 # 4. 接口嵌套(接口继承) > 在Go语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口。 接口与接口嵌套组合而成了新接口,只要接口的所有方法被实现,则这个接口中的所有嵌套接口的方法均可以被调用 `比如接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法` ``` type ReadWrite interface { Read(b Buffer) bool Write(b Buffer) bool } type Lock interface { Lock() Unlock() } type File interface { ReadWrite Lock Close() } ``` > <font color=red>一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现 # 5. 接口实现 如果一个类型实现了一个接口里的所有方法,那么这个类型就实现了这个接口 如下面示例,类型fileHandle 实现了DataWrite接口: ``` type DataWrite interface { Write(data interface{}) error } type fileHandle struct { .... } func (f fileHandle)Write(data interface{}) error { fmt.Println("文件写入中....") return nil } ``` # 6.类型与接口的关系 类型和接口之间有一对多和多对一的关系。 ## 6.1 一对多 一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 ``` // 定义Writer接口 type Writer interface{ Write([]byte) int } // 定义Reader接口 type Reader interface{ Read() } // 定义一个File类型 type File struct { } // File类型 实现Writer接口 func (f File)Write(p []byte) int { return 0 } // File类型 定义Reader接口 func (f File)Read() { } ``` ## 6.2 多对一 一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。也就是说,使用者并不关心某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。 - 示例分析 Service接口定义了两个方法:一个是开启服务的方法:Start(),一个是输出日志的方法:Log()。使用GameService结构体来实现Service,GameService结构只能实现Start()方法,Service接口中的Log()方法被日志器(Logger)实现了,将Logger嵌入到GameService中,则达成了GameService结构体实现Service接口,从而能最大程度地避免代码冗余,简化代码结构。 详细实现过程如下: ``` 01 // 一个服务需要满足能够开启和写日志的功能 02 type Service interface { 03 Start() // 开启服务 04 Log(string) // 日志输出 05 } 06 07 // 日志器 08 type Logger struct { 09 } 10 11 // 实现Service的Log()方法 12 func (g *Logger) Log(l string) { 13 14 } 15 16 // 游戏服务 17 type GameService struct { 18 Logger // 嵌入日志器 19 } 20 21 // 实现Service的Start()方法 22 func (g *GameService) Start() { 23 } ``` 代码说明如下: - 第2行,定义服务接口,一个服务需要实现Start()方法和日志方法。 - 第8行,定义能输出日志的日志器结构。 - 第12行,为Logger添加Log()方法,同时实现Service的Log()方法。 - 第17行,定义GameService结构 - 第18行,在Game Service中嵌入Logger日志器,以实现日志功能。 - 第22行,Game Service的Start()方法实现了Service的Start()方法。 此时,实例化GameService,并将实例赋给Service,代码如下: ``` var s Service = new(Game Service) s.Start() s.Log("hello") ``` s就可以使用Start()方法和Log()方法,其中,Start()由GameService实现,Log()方法由Logger实现。 # 7. 接口和类型间转换 Go语言中使用接口断言(type assertions)将接口转换成另外一个接口,也可以将接口转换为另外的类型。接口的转换在开发中非常常见,使用也非常频繁。 ## 7.1 类型断言的格式 如果发生接口未实现时,将会把ok置为false,t置为T类型的0值。正常实现时,ok为true。 这里ok可以被认为是:`i接口是否实现T类型的结果`。 类型断言的基本格式如下: ``` t,ok := i.(T) ``` - i 代表接口变量。 - T 代表转换的目标类型。 - t 代表转换后的变量。 ## 7.2 将接口转为其他接口 `实现某个接口的类型同时实现了另外一个接口,此时可以在两个接口间转换。` - 代码示例 鸟和猪具有不同的特性,鸟可以飞,猪不能飞,但两种动物都可以行走。如果使用结构体实现鸟和猪,让它们具备自己特性的Fly()和Walk()方法就让鸟和猪各自实现了飞行动物接口(Flyer)和行走动物接口(Walker)。 将鸟和猪的实例创建后,被保存到interface{}类型的map中。interface{}类型表示空接口,意思就是这种接口可以保存为任意类型。对保存有鸟或猪的实例的interface{}变量进行断言操作,如果断言对象是断言指定的类型,则返回转换为断言对象类型的接口;如果不是指定的断言类型时,断言的第二个参数将返回false。 实现代码如下: ``` 01 package main 02 03 import "fmt" 04 05 // 定义飞行动物接口 06 type Flyer interface { 07 Fly() 08 } 09 10 // 定义行走动物接口 11 type Walker interface { 12 Walk() 13 } 14 15 // 定义鸟类 16 type bird struct { 17 } 18 19 // 实现飞行动物接口 20 func (b *bird) Fly() { 21 fmt.Println("bird: fly") 22 } 23 24 // 为鸟添加Walk()方法,实现行走动物接口 25 func (b *bird) Walk() { 26 fmt.Println("bird: walk") 27 } 28 29 // 定义猪 30 type pig struct { 31 } 32 33 // 为猪添加Walk()方法,实现行走动物接口 34 func (p *pig) Walk() { 35 fmt.Println("pig: walk") 36 } 37 38 func main() { 39 40 // 创建动物的名字到实例的映射 41 animals := map[string]interface{}{ 42 "bird": new(bird), 43 "pig": new(pig), 44 } 45 46 // 遍历映射 47 for name, obj := range animals { 48 49 // 判断对象是否为飞行动物 50 f, isFlyer := obj.(Flyer) 51 // 判断对象是否为行走动物 52 w, isWalker := obj.(Walker) 53 54 fmt.Printf("name: %s is Flyer: %v is Walker: %v\n", name, is Flyer, is Walker) 55 56 // 如果是飞行动物则调用飞行动物接口 57 if isFlyer { 58 f.Fly() 59 } 60 61 // 如果是行走动物则调用行走动物接口 62 if isWalker { 63 w.Walk() 64 } 65 } 66 } ``` 代码说明如下: - 第6行定义了飞行动物的接口。 - 第11行定义了行走动物的接口。 - 第16和30行分别定义了鸟和猪两个对象,并分别实现了飞行动物和行走动物接口。 - 第41行是一个map,映射对象名字和对象实例,实例是鸟和猪。 - 第47行开始遍历map,obj为interface{}接口类型。 - 第50行中,使用类型断言获得f,类型为Flyer及isFlyer的断言成功的判定。 - 第52行中,使用类型断言获得w,类型为Walker及isWalker的断言成功的判定。 - 第57和62行,根据飞行动物和行走动物两者 代码输出如下: ``` name: pig is Flyer: false is Walker: true pig: walk name: bird is Flyer: true is Walker: true bird: fly bird: walk ``` # 8.空接口 — 能保存所有值的类型 空接口是接口类型的特殊形式,`空接口没有任何方法`,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。 ## 8.1 将值保存到空接口 ``` 01 var any interface{} 02 03 any = 1 04 fmt.Println(any) 05 06 any = "hello" 07 fmt.Println(any) 08 09 any = false 10 fmt.Println(any) ``` 代码输出如下: ``` 1 hello false ``` - 第1行,声明any为interface{}类型的变量。 - 第3行,为any赋值一个整型1。 - 第4行,打印any的值,提供给fmt.Println的类型依然是interface{}。 - 第6行,为any赋值一个字符串hello。此时any内部保存了一个字符串。但类型依然是interface{}。 - 第9行,赋值布尔值。 ## 8.2 从空接口获取值 保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误 代码如下: ``` 01 // 声明a变量,类型int,初始值为1 02 var a int = 1 03 04 // 声明i变量,类型为interface{},初始值为a,此时i的值变为1 05 var i interface{} = a 06 07 // 声明b变量,尝试赋值i 08 var b int = i ``` 第8行代码编译报错: ``` cannot use i (type interface {}) as type int in assignment: need type assertion ``` > 编译器告诉我们,不能将i变量视为int类型赋值给b。在代码第5行中,将a的值赋值给i时,虽然i在赋值完成后的内部值为int,但i还是一个interface{}类型的变量。 为了让第8行的操作能够完成,`编译器提示我们得使用type assertion,意思就是类型断言`。 使用类型断言修改第8行代码如下: ``` var b int = i.(int) ``` 修改后,代码可以编译通过,并且b可以获得i变量保存的a变量的值:1。 ## 8.3 空接口的值比较 空接口在保存不同的值后,可以和其他变量值一样使用“==”进行比较操作。空接口的比较有以下几种特性。 ### 8.3.1 类型不同的空接口间的比较结果不相同 保存有类型不同的值的空接口进行比较时,Go语言会优先比较值的类型。因此类型不同,比较结果也是不相同的,代码如下: ``` 01 // a保存整型 02 var a interface{} = 100 03 04 // b保存字符串 05 var b interface{} = "hi" 06 07 // 两个空接口不相等 08 fmt.Println(a == b) // 输出: false ``` ### 8.3.2 不能比较空接口中的动态值 当接口中保存有动态类型的值时,运行时将触发错误, 代码如下: ``` 01 // c保存包含10的整型切片 02 var c interface{} = []int{10} 03 04 // d保存包含20的整型切片 05 var d interface{} = []int{20} 06 07 // 这里会发生崩溃 08 fmt.Println(c == d) ``` 代码运行到第8行时发生崩溃: ``` panic: runtime error: comparing uncomparable type []int ``` 这是一个运行时错误,提示[]int是不可比较的类型。 zhuoge - 下面列出了几种类型及比较情况 类型 | 说明 ---|--- map | 宕机错误,不可比较 切片([]T)| 宕机错误,不可比较 通道(channel)| 可比较,必须由一个make生成,也就是说同一个通道才会true,否则false 数组([容量]T) | 可比较,编译期知道两个数组是否一致 结构体|可比较,可以逐个比较结构体的值 # 9. 接口使用中的注意事项 - 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例) ``` package main import "fmt" type PeopleI interface { GetName() string } type Girl struct { Name string } func (g Girl) GetName() string { return g.Name } func main() { var p PeopleI var g Girl g.Name = "小花" p = g //指向一个实现了该接口的自定义类型的变量 fmt.Println(p.GetName()) } ``` - 接口中所有的方法都没有方法体,即都是没有实现的方法。 - 在 Go中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现 了该接口。 - 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型 - 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型 - 一个自定义类型可以实现多个接口 - Go接口中不能有任何变量 - interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil - 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量 赋给空接口