从前两讲起,我们开始了接口专题的学习。关于接口的一般使用方法其实已经讲得差不多了,本讲介绍一些使用接口时的注意事项,尽量帮助大家避开一些弯路。

具体来说,本讲将涉及以下内容:

  • 接口的嵌套组合
  • 从空接口中取值
  • 空接口的值比较
  • 接口与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

本讲是接口系列的最后一篇,是整个专题的收官之作。在本讲中,介绍了使用接口时的四个注意事项。

  1. 和结构体类似,接口也是可以嵌套的,这种机制可以带来更加灵活的组合方式。
  2. 从空接口中取值时,类型断言是非常必要的。不使用类型断言将在编译前被GoLand工具检测出来。
  3. 当进行空接口中值的比较时,Map和Slice是无法比较的。相反,数组则可以比较,而且还是每个元素进行比较。
  4. 将一个带有类型的nil赋值给接口时,只有值为nil,而类型不为nil。此时,接口与nil判断将不相等。为了规避这个问题,我们应在返回接口类型前进行非nil判定。

好了,本讲就到这里。

➡️ 在下次课程中,我们会介绍Go语言中包的知识,具体内容是:

  • Go程序源码的组织结构