从本讲开始,我会介绍Go语言中的接口部分,我将用三讲的篇幅为大家讲解。本讲是第一讲的内容,将为大家介绍的知识点是:

  • 接口的定义和使用

在正式开始讲解之前,我们还需要明确一个必备的概念——接口的目的,或者说是接口的作用。

# 接口的作用

概括地说,接口的作用实际上就是规定了对象的“行动法则”。

一方面,举例来说:一个“学生”对象,他能做的事情有很多:学习、锻炼、吃饭、睡觉、打电子游戏等等。当这个对象来到学校之后,打电子游戏、睡觉这类行为就会受到禁止或者是限制了。换句话说,在某些场景中,我们会规范某个对象的行为,使其受控。那么,想要实现这种规范(或者说是控制),就要用到接口了。

另一方面,得益于接口的编程设计模式。使用者(调用接口的一方)无需关注某个动作的具体实现;实现者(具体执行某个行为的一方)也无需关心使用方式。

用比较生动的方式来阐述,使用者的内心戏就是:我现在想让你做这件事情,我不管你用什么方法,反正得给我个结果。实现者的内心戏就是:我会做也能做好这件事情,我不管是谁让我做,反正我会把结果给你。

映射到现实生活中,假设有“老师”和“学生”两种对象,都会执行“上课”的行为。对于老师而言,上课的具体实现方式便是讲课;对于学生而言,上课的具体实现方式便是听课。如此一来,一旦使用老师对象去执行上课的行为,讲课便会发生。反过来,一旦使用学生对象去执行上课的行为,听课便会发生。

同时,由于“上课”这个行为规范了老师只能“讲课”,所以尽管老师有逛街、看电影等等行为,但都不会在上课时执行;相应地,“上课”行为也规范了学生只能“听课”,所以尽管学生有睡觉、玩游戏等等行为,也都不会在上课时去做。

大家看,整个上课的流程由于有了规范,所有涉及到的对象的行为便会“受控”。由于对象都按照规范去做,那么行为一旦开始,每个对象便会执行各自的具体行为。这便是使用接口的意义。

在实际开发之中,接口的使用非常普遍。比如:我们在使用淘宝App进行商品搜索时,其结果页往往会是一个搜索结果列表,其中通常包含了商品的概况以及图片。

就拿图片来说吧,一般加载和显示图片会采用“本地缓存”的机制,一方面节省用户流量,另一方面加快了图片的显示速度。作为列表,无需关心缓存或是网络是如何具体查找和返回图片数据的,只要先看看本地有没有数据,没有再去网上下载就完工了。而具体的查找和返回数据则交给负责缓存管理和负责网络下载的执行者去做,它们遵循通用的行为准则。

既然说到图片加载,我们不妨就以此为例讲解Go语言中接口的使用。一般来说,接口分为两个步骤来准备,首先是定义(行为规范),然后是相关对象的实现(具体的操作),准备好后便可在后续的代码中使用接口了。

# 接口的定义

在Go语言中,定义接口的格式如下:

go
复制代码type interface_name interface{
    function_name( [params] ) [return_values]
    ...
}

其中,type关键字表示要自定义类型;interface_name是自定义的接口名;interface表示接口类型;由大括号包裹的部分定义了要被实现方法,一个接口中可以同时存在一个或多个方法。function_name是方法名;params是方法所需的参数;return_values是方法的返回值。params和return_values可以省略,也可以存在一个或多个。

对于本例而言,接口的目的在于规范图片加载的流程。为了讲解方便,我在此将图片加载的过程简化为查找并下载图片一个步骤。

在查找并下载图片时,需要图片下载地址作为依据,并将返回图片的实际数据。因此,我们定义一个名为imageLoader的接口,接口中包含FetchImage()方法,该方法需要string类型的变量作为参数,表示下载地址,返回string类型(在实际开发中通常是byte[]),表示图片数据。

💡 提示: 注意到接口的命名(imageLoader)特点了吗?在为接口命名时,一般会在单词后面加上er后缀。接口中的方法名(FetchImage())首字母大小写决定了该方法的可访问范围。

具体代码如下:

go
复制代码// ImageDownloader 图片加载接口
type ImageDownloader interface {
	// FetchImage 获取图片,需要传入图片地址,方法返回图片数据
	FetchImage(url string) string
}

如此,接口的定义便完成了。

# 接口的实现

接下来,就到了接口的实现环节。

接口的实现,实际上就是指实现具体的行为。比如:本例中的从缓存中加载或从网络上下载图片的具体方法。

我们先来实现从本地缓存中获取图片数据的部分。通过前面对结构体的学习,我们已经掌握了如何使用结构体来表示一个对象,以及执行对象的方法。现在,我们一同定义负责本地缓存管理的结构体。具体代码如下:

go
复制代码type fileCache struct {
}

为了讲解方便,我在这里省略了这个结构体的内部构造。

接下来,便是实现具体的接口方法了。在Go语言中,实现接口的格式如下:

go
复制代码func (struct_variable struct_name) function_name([params]) [return_values] {
   // 方法实现 
}

其中,struct_name_variable和struct_name一起,表示作用的对象。对于本例而言,则是*fileCache类型的变量。紧接着的function_name是方法名,params指的是方法所需的参数,return_values指的是方法的返回值。其中,params和return_values是可选的,也允许有多个值。

套用到本例,我们编写作用于*fileCache的接口实现,代码如下:

go
复制代码//FetchImage接口实现
func (f *fileCache) FetchImage(url string) string {
	return "从本地缓存中获取图片:" + url
}

对比FetchImage()方法的接口声明:

go
复制代码// FetchImage 获取图片,需要传入图片地址,方法返回图片数据
FetchImage(url string) string

发现了吗?在实现方法时,需要满足两个条件:

  • 第一是接口中定义的的方法与实现接口的类型方法格式一致。这要求不仅方法名称相同,参数和返回值也要相同;
  • 第二就是接口中定义的所有方法全部都要实现

如法炮制,继续定义负责从网络下载图片的结构体以及作用于该结构体的接口实现:

go
复制代码//定义从网络下载图片的结构体
type netFetch struct {
}
//FetchImage接口实现
func (n *netFetch) FetchImage(url string) string {
	return "从网络下载图片:" + url
}

到此,接口的定义和实现就都已完成。下一步就是回到main()函数中使用它们了。

# 接口的调用

来到main()函数,定义一个ImageDownloader类型的变量,然后通过new(fileCache)函数为其赋值,随后便可通过这个变量调用从缓存中加载图片的方法。类似地,通过new(netFetch)为其赋值,便可通过这个变量调用从网络上下载图片的方法。

💡 提示: 为何ImageDownloader类型的变量可以通过new(fileCache)或new(netFetch)进行赋值呢,你知道原因吗?

从具体的业务需求分析,我们应首先检查本地缓存是否存在相应的图片数据,当找不到时再从网络中获取。因此,整个接口调用部分的示例代码如下:

go
复制代码func main() {
	//从本地缓存中获取数据
	var imageLoader ImageDownloader
	imageLoader = new(fileCache)
	data := imageLoader.FetchImage("https://www.example.com/a.png")
	fmt.Println(data)
	if data == "" {
		// 当本地缓存中没有数据时,从网络下载
		var imageLoader2 ImageDownloader
		imageLoader2 = new(netFetch)
		data2 := imageLoader2.FetchImage("https://www.example.com/a.png")
		fmt.Println(data2)
	}
}

如上所示,代码的逻辑将首先检查本地缓存,当返回值为空字符串("")时,即表示本地无缓存。此时,应考虑去网络上下载图片。

将本讲示例代码汇总并运行,控制台可得如下输出:

从本地缓存中获取图片:www.example.com/a.png (opens new window)

修改针对*fileCache的FetchImage()方法,使其返回值为空字符串(""),再次运行程序,控制台上的输出将变为:

从网络下载图片:www.example.com/a.png (opens new window)

怎么样,在Go语言中使用接口是不是很简单呢?

# 总结

🎉 恭喜,您完成了本次课程的学习!

📌 以下是本次课程的重点内容总结:

  • 接口的定义和使用

本讲是接口系列专题的第一篇。在本讲中,我们首先明确了一个概念——使用接口的目的是什么。概括地说有两点:一是规范某个对象的行为,使其受控;二是接口的使用者和实现者各司其职,互不干涉。

然后,我们分为三个步骤介绍了接口的一般使用流程,包括接口的定义接口的实现以及接口方法的调用。并以模拟图片加载为例,模拟了从本地缓存中加载和从网络上下载图片两种方式显示图片的流程。

最后,还需要补充一个知识点——一种类型可以实现多个接口,多种类型可以实现相同的接口。看上去是不是像绕口令?别着急,不妨再写个示例来演示该特性。有关这部分的内容,感兴趣的朋友可以阅读这一篇参考文章(实际上,只要掌握了本讲的内容,这一特性是非常容易理解的):Go语言类型与接口的关系 (biancheng.net) (opens new window)

好了,本讲就到这里。

➡️ 在下次课程中,我们会介绍Go语言中接口的更多使用技巧,具体内容是:

  • 空接口与泛型