从前两讲起,我们开始了接口专题的学习。关于接口的一般使用方法其实已经讲得差不多了,本讲介绍一些使用接口时的注意事项,尽量帮助大家避开一些弯路。
具体来说,本讲将涉及以下内容:
- 接口的嵌套组合
- 从空接口中取值
- 空接口的值比较
- 接口与nil
# 接口的嵌套组合
我们都知道,结构体是允许嵌套使用的。实际上,接口也可以。
举例来说,我们使用浏览器进行下载文件的时候,通常会在保存、另存为和取消之间做出选择。抛开取消不谈,选择保存时,浏览器会自动执行下载和保存两个步骤;选择另存为时,浏览器会先询问文件保存的路径,再开始下载和保存。
如果我们把选择路径、下载、保存看作是待下载文件的3个接口,并用代码来表示,它很可能会是这样的:
go
复制代码// ChooseDest 选择保存路径
type ChooseDest interface {
chooseDest(localFile string)
}
// Download 执行下载
type Download interface {
download()
}
// Save 保存文件
type Save interface {
save()
}
细心的朋友会发现,无论何种方式下载文件,其中的下载和保存都是必需且顺序不变的。所以,我们不妨再创建一个接口,使其包含下载和保存两个接口,代码如下:
go
复制代码// DownloadAndSave 下载和保存
type DownloadAndSave interface {
Download
Save
}
在使用时,我们便可直接声明DownloadAndSave类型的变量去执行下载和保存了,示例代码如下:
go
复制代码func main() {
//声明一个file类型的变量,命名为downloadFileExample
downloadFileExample := new(file)
//使用ChooseDest接口
var chooseDest ChooseDest
chooseDest = downloadFileExample
chooseDest.chooseDest("")
//使用DownloadAndSave接口
var downloadAndSave DownloadAndSave
downloadAndSave = downloadFileExample
downloadAndSave.download()
downloadAndSave.save()
}
如上代码所示,无需单独声明Download和Save接口变量,仅使用DownloadAndSave接口变量便可调用download()和save()两个方法。
# 从空接口取值
在上一讲中,曾经使用过类似下面这样的案例:
go
复制代码func main() {
dataOutput("Hello")
}
func dataOutput(data interface{}) {
fmt.Println(data)
}
为了实现“将传入的参数按原样输出”的需求,我们编写了dataOutput()函数。该函数所需的参数是空接口,能接纳所有类型的数据,然后通过调用fmt.Println()将数据输出,满足了需求。
现在,如果想从data中获取数据,并赋值给某个变量,该如何做呢?显然,可以如下实现:
go
复制代码func dataOutput(data interface{}) {
fmt.Println(data)
var stringValue string = data
fmt.Println(stringValue)
}
暂且将上述方法当作方法A。
再看如下实现:
go
复制代码func dataOutput(data interface{}) {
fmt.Println(data)
stringValue := data.(string)
fmt.Println(stringValue)
}
暂且将该方法当作方法B。
猜一猜,哪种方法可以呢?
答案是:方法B。
是不是很奇怪,为什么方法A不行呢?实际上,当我们按照方法A去写时,GoLand会自动识别出问题,提示:Cannot use 'data' (type interface{}) as the type string,意思是无法将类型为interface{}的data变量作为string类型使用。
这是因为在进行类型断言前,谁也不知道data里放的是何类型。举个形象一点的例子,虽然箱子里装了某样货物,但箱子依然还是箱子,是不能将箱子当货使用的。
所以,在从空接口中取值时,切记要使用类型断言。
# 空接口的值比较
撸起袖子,我们一起来挑战几道题。
不要用电脑编译和运行下面的代码,先猜猜它们的运行结果。
go
复制代码func main() {
var a interface{} = 10
var b interface{} = "10"
fmt.Println(a == b)
}
相信各位都能回答正确,上面这段代码运行结果为:
false
挑战继续,再来试试这个:
go
复制代码func main() {
var a interface{} = []int{1, 2, 3, 4, 5}
var b interface{} = []int{1, 2, 3, 4, 5}
fmt.Println(a == b)
}
上面这段代码运行后,程序会发生宕机。报错信息如下:
panic: runtime error: comparing uncomparable type []int
从字面上看,错误原因是程序比较了不可比较的类型——[]int。
在Go语言中,有两种数据是无法比较的,它们是:Map和Slice,强行比较会引发如上宕机错误。
数组是可以比较的,而且会比较数组中每个元素的值。因此,只需将上述代码改为:
go
复制代码func main() {
var a interface{} = [5]int{1, 2, 3, 4, 5}
var b interface{} = [5]int{1, 2, 3, 4, 5}
fmt.Println(a == b)
}
程序便会正常运行,输出结果:
true
# 接口与nil
在Go语言中,nil是一个特殊的值,它只能赋值给指针类型和接口类型。
让我们来挑战下面这段代码,还是不要用电脑编译运行,猜一猜它的输出结果:
go
复制代码func main() {
var a interface{} = nil
fmt.Println(a == nil)
}
这段代码运行后,控制台将输出:
true
应该没什么疑问吧?继续看下面的代码:
go
复制代码type Person struct {
name string
age int
gender int
}
type SayHello interface {
sayHello()
}
func (p *Person) sayHello() {
fmt.Println("Hello!")
}
func getSayHello() SayHello {
var p *Person = nil
return p
}
func main() {
var person = new(Person)
person.name = "David"
person.age = 18
person.gender = 0
var sayHello SayHello
sayHello = person
fmt.Println(reflect.TypeOf(sayHello))
fmt.Println(sayHello == nil)
fmt.Println(getSayHello())
fmt.Println(getSayHello() == nil)
}
猜一猜最终控制台将输出什么呢?
答案是:
*main.Person
false
false
是不是也很奇怪?
输出第一个false无可厚非,可输出的第二个false就很耐人寻味了。第二个false来自于main()函数中调用的getSayHello()函数,该函数返回SayHello类型的接口,函数体内返回了nil值的*Person。直接输出getSayHello()函数的结果,是nil,但与nil比较时却不是true。
这是因为:将一个带有类型的nil赋值给接口时,只有值为nil,而类型不为nil。此时,接口与nil判断将不相等。
那么,为了规避这类问题,我们不妨在getSayHello()函数值做些特殊处理。当函数体中的p变量为nil时,直接返回nil即可。发生修改部分的代码如下:
go
复制代码func getSayHello() SayHello {
var p *Person = nil
if p == nil {
return nil
} else {
return p
}
}
再次运行程序,控制台输出如下:
*main.Person
false
true
# 总结
🎉 恭喜,您完成了本次课程的学习!
📌 以下是本次课程的重点内容总结:
- 使用接口时的注意事项
- 接口的嵌套组合
- 从空接口中取值
- 空接口的值比较
- 接口与nil
本讲是接口系列的最后一篇,是整个专题的收官之作。在本讲中,介绍了使用接口时的四个注意事项。
- 和结构体类似,接口也是可以嵌套的,这种机制可以带来更加灵活的组合方式。
- 从空接口中取值时,类型断言是非常必要的。不使用类型断言将在编译前被GoLand工具检测出来。
- 当进行空接口中值的比较时,Map和Slice是无法比较的。相反,数组则可以比较,而且还是每个元素进行比较。
- 将一个带有类型的nil赋值给接口时,只有值为nil,而类型不为nil。此时,接口与nil判断将不相等。为了规避这个问题,我们应在返回接口类型前进行非nil判定。
好了,本讲就到这里。
➡️ 在下次课程中,我们会介绍Go语言中包的知识,具体内容是:
- Go程序源码的组织结构