Go语言基础之接口

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

接口

接口类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

为什么要使用接口

  1. type Cat struct{}
  2. func (c Cat) Say() string { return "喵喵喵" }
  3. type Dog struct{}
  4. func (d Dog) Say() string { return "汪汪汪" }
  5. func main() {
  6. c := Cat{}
  7. fmt.Println("猫:", c.Say())
  8. d := Dog{}
  9. fmt.Println("狗:", d.Say())
  10. }

上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?

像类似的例子在我们编程过程中会经常遇到:

比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?

比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?

比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。

接口的定义

Go语言提倡面向接口编程。

每个接口由数个方法组成,接口的定义格式如下:

  1. type 接口类型名 interface{
  2. 方法名1( 参数列表1 ) 返回值列表1
  3. 方法名2( 参数列表2 ) 返回值列表2

  4. }

其中:

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

  1. type writer interface{
  2. Write([]byte) error
  3. }

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表

我们来定义一个Sayer接口:

  1. // Sayer 接口
  2. type Sayer interface {
  3. say()
  4. }

定义dogcat两个结构体:

  1. type dog struct {}
  2. type cat struct {}

因为Sayer接口里只有一个say方法,所以我们只需要给dogcat 分别实现say方法就可以实现Sayer接口了。

  1. // dog实现了Sayer接口
  2. func (d dog) say() {
  3. fmt.Println("汪汪汪")
  4. }
  5. // cat实现了Sayer接口
  6. func (c cat) say() {
  7. fmt.Println("喵喵喵")
  8. }

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

接口类型变量

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dogcat类型的变量。

  1. func main() {
  2. var x Sayer // 声明一个Sayer类型的变量x
  3. a := cat{} // 实例化一个cat
  4. b := dog{} // 实例化一个dog
  5. x = a // 可以把cat实例直接赋值给x
  6. x.say() // 喵喵喵
  7. x = b // 可以把dog实例直接赋值给x
  8. x.say() // 汪汪汪
  9. }

Tips: 观察下面的代码,体味此处_的妙用

  1. // 摘自gin框架routergroup.go
  2. type IRouter interface{ ... }
  3. type RouterGroup struct { ... }
  4. var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter

值接收者和指针接收者实现接口的区别

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个Mover接口和一个dog结构体。

  1. type Mover interface {
  2. move()
  3. }
  4. type dog struct {}

值接收者实现接口

  1. func (d dog) move() {
  2. fmt.Println("狗会动")
  3. }

此时实现接口的是dog类型:

  1. func main() {
  2. var x Mover
  3. var wangcai = dog{} // 旺财是dog类型
  4. x = wangcai // x可以接收dog类型
  5. var fugui = &dog{} // 富贵是*dog类型
  6. x = fugui // x可以接收*dog类型
  7. x.move()
  8. }

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

指针接收者实现接口

同样的代码我们再来测试一下使用指针接收者有什么区别:

  1. func (d *dog) move() {
  2. fmt.Println("狗会动")
  3. }
  4. func main() {
  5. var x Mover
  6. var wangcai = dog{} // 旺财是dog类型
  7. x = wangcai // x不可以接收dog类型
  8. var fugui = &dog{} // 富贵是*dog类型
  9. x = fugui // x可以接收*dog类型
  10. }

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

面试题

注意:这是一道你需要回答“能”或者“不能”的题!

首先请观察下面的这段代码,然后请回答这段代码能不能通过编译?

  1. type People interface {
  2. Speak(string) string
  3. }
  4. type Student struct{}
  5. func (stu *Student) Speak(think string) (talk string) {
  6. if think == "sb" {
  7. talk = "你是个大帅比"
  8. } else {
  9. talk = "您好"
  10. }
  11. return
  12. }
  13. func main() {
  14. var peo People = Student{}
  15. think := "bitch"
  16. fmt.Println(peo.Speak(think))
  17. }

类型与接口的关系

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。

  1. // Sayer 接口
  2. type Sayer interface {
  3. say()
  4. }
  5. // Mover 接口
  6. type Mover interface {
  7. move()
  8. }

dog既可以实现Sayer接口,也可以实现Mover接口。

  1. type dog struct {
  2. name string
  3. }
  4. // 实现Sayer接口
  5. func (d dog) say() {
  6. fmt.Printf("%s会叫汪汪汪\n", d.name)
  7. }
  8. // 实现Mover接口
  9. func (d dog) move() {
  10. fmt.Printf("%s会动\n", d.name)
  11. }
  12. func main() {
  13. var x Sayer
  14. var y Mover
  15. var a = dog{name: "旺财"}
  16. x = a
  17. y = a
  18. x.say()
  19. y.move()
  20. }

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。

  1. // Mover 接口
  2. type Mover interface {
  3. move()
  4. }

例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:

  1. type dog struct {
  2. name string
  3. }
  4. type car struct {
  5. brand string
  6. }
  7. // dog类型实现Mover接口
  8. func (d dog) move() {
  9. fmt.Printf("%s会跑\n", d.name)
  10. }
  11. // car类型实现Mover接口
  12. func (c car) move() {
  13. fmt.Printf("%s速度70迈\n", c.brand)
  14. }

这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。

  1. func main() {
  2. var x Mover
  3. var a = dog{name: "旺财"}
  4. var b = car{brand: "保时捷"}
  5. x = a
  6. x.move()
  7. x = b
  8. x.move()
  9. }

上面的代码执行结果如下:

  1. 旺财会跑
  2. 保时捷速度70

并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

  1. // WashingMachine 洗衣机
  2. type WashingMachine interface {
  3. wash()
  4. dry()
  5. }
  6. // 甩干器
  7. type dryer struct{}
  8. // 实现WashingMachine接口的dry()方法
  9. func (d dryer) dry() {
  10. fmt.Println("甩一甩")
  11. }
  12. // 海尔洗衣机
  13. type haier struct {
  14. dryer //嵌入甩干器
  15. }
  16. // 实现WashingMachine接口的wash()方法
  17. func (h haier) wash() {
  18. fmt.Println("洗刷刷")
  19. }

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

  1. // Sayer 接口
  2. type Sayer interface {
  3. say()
  4. }
  5. // Mover 接口
  6. type Mover interface {
  7. move()
  8. }
  9. // 接口嵌套
  10. type animal interface {
  11. Sayer
  12. Mover
  13. }

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

  1. type cat struct {
  2. name string
  3. }
  4. func (c cat) say() {
  5. fmt.Println("喵喵喵")
  6. }
  7. func (c cat) move() {
  8. fmt.Println("猫会动")
  9. }
  10. func main() {
  11. var x animal
  12. x = cat{name: "花花"}
  13. x.move()
  14. x.say()
  15. }

空接口

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

  1. func main() {
  2. // 定义一个空接口x
  3. var x interface{}
  4. s := "Hello 沙河"
  5. x = s
  6. fmt.Printf("type:%T value:%v\n", x, x)
  7. i := 100
  8. x = i
  9. fmt.Printf("type:%T value:%v\n", x, x)
  10. b := true
  11. x = b
  12. fmt.Printf("type:%T value:%v\n", x, x)
  13. }

空接口的应用

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

  1. // 空接口作为函数参数
  2. func show(a interface{}) {
  3. fmt.Printf("type:%T value:%v\n", a, a)
  4. }

空接口作为map的值

使用空接口实现可以保存任意值的字典。

  1. // 空接口作为map值
  2. var studentInfo = make(map[string]interface{})
  3. studentInfo["name"] = "沙河娜扎"
  4. studentInfo["age"] = 18
  5. studentInfo["married"] = false
  6. fmt.Println(studentInfo)

类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

接口值

一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值

我们来看一个具体的例子:

  1. var w io.Writer
  2. w = os.Stdout
  3. w = new(bytes.Buffer)
  4. w = nil

请看下图分解:

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

  1. x.(T)

其中:

  • x:表示类型为interface{}的变量
  • T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

  1. func main() {
  2. var x interface{}
  3. x = "Hello 沙河"
  4. v, ok := x.(string)
  5. if ok {
  6. fmt.Println(v)
  7. } else {
  8. fmt.Println("类型断言失败")
  9. }
  10. }

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

  1. func justifyType(x interface{}) {
  2. switch v := x.(type) {
  3. case string:
  4. fmt.Printf("x is a string,value is %v\n", v)
  5. case int:
  6. fmt.Printf("x is a int is %v\n", v)
  7. case bool:
  8. fmt.Printf("x is a bool is %v\n", v)
  9. default:
  10. fmt.Println("unsupport type!")
  11. }
  12. }

因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

GO学习-(14) Go语言基础之接口的更多相关文章

  1. Java学习笔记:语言基础

    Java学习笔记:语言基础 2014-1-31   最近开始学习Java,目的倒不在于想深入的掌握Java开发,而是想了解Java的基本语法,可以阅读Java源代码,从而拓展一些知识面.同时为学习An ...

  2. Go语言基础之接口

    Go语言基础之接口 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节. 接口 接口介绍 在Go语言中接口(interface)是一种类型,一种抽象的类 ...

  3. ios开发学习笔记001-C语言基础知识

    先来学习一下C语言基础知识,总结如下: 在xcode下编写代码. 1.编写代码 2.编译:cc –c 文件名.c 编译成功会生成一个 .o的目标文件 3.链接:把目标文件.o和系统自带的库合并在一起, ...

  4. GO学习-(17) Go语言基础之反射

    Go语言基础之反射 本文介绍了Go语言反射的意义和基本使用. 变量的内在机制 Go语言中的变量是分为两部分的: 类型信息:预先定义好的元信息. 值信息:程序运行过程中可动态变化的. 反射介绍 反射是指 ...

  5. GO学习-(19) Go语言基础之网络编程

    Go语言基础之网络编程 现在我们几乎每天都在使用互联网,我们前面已经学习了如何编写Go语言程序,但是如何才能让我们的程序通过网络互相通信呢?本章我们就一起来学习下Go语言中的网络编程. 关于网络编程其 ...

  6. Arduino学习笔记② Arduino语言基础

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  7. ndk学习之C语言基础复习----虚拟内存布局与malloc申请

    在这一次中来学习一下C语言的内存布局,了解它之后就可以解释为啥在用malloc()申请的内存之后需要用memset()来对内存进行一下初始化了,首先来了解一下物理内存与虚拟内存: 物理内存:通过物理内 ...

  8. GO学习-(20) Go语言基础之单元测试

    Go语言基础之单元测试 不写测试的开发不是好程序员.我个人非常崇尚TDD(Test Driven Development)的,然而可惜的是国内的程序员都不太关注测试这一部分. 这篇文章主要介绍下在Go ...

  9. GO学习-(15) Go语言基础之包

    Go语言基础之包 在工程化的Go语言开发项目中,Go语言的源码复用是建立在包(package)基础之上的.本文介绍了Go语言中如何定义包.如何导出包的内容及如何导入其他包. Go语言的包(packag ...

随机推荐

  1. JMeter 结果处理常见问题

    1. 前言 2. 结果处理常见问题 1)在察看结果树中只看失败情况 2)如何把日志放入文件查看 3)cvs 文件中文读取乱码 4)失败请求数据的采集 5)结果树响应数据中文乱码解决办法 1. 前言 工 ...

  2. 计算机系统原理:cache容量计算

    Cache容量计算例题: 假定主存地址位数为32位,按字节编址,主存和cache之间采用4-路组相联映射方式,主存块大小为4个字,每字32位,采用直写(Write Throght)方式和LRU替换策略 ...

  3. 【日志追踪】(微服务应用和单体应用)-logback中的MDC机制

    一.MDC介绍 MDC(Mapped Diagnostic Contexts)映射诊断上下文,该特征是logback提供的一种方便在多线程条件下的记录日志的功能, 某些应用程序采用多线程的方式来处理多 ...

  4. hdu1824 基础2sat

    题意: Let's go home Time Limit: 10000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) ...

  5. 从苏宁电器到卡巴斯基第10篇:我在苏宁电器当营业员 II

    之所以是主推,其实是有原因的 据我所知,尽管诺基亚卖的很好,但是他们的厂促的待遇却很一般,估计也就一千多两千的样子,撑死两千多.但是呢,记得当时我们的卖场里面还有联想手机,别看卖得相当次,但是他们的厂 ...

  6. Node-Web模块

    创建服务端------------------------------------------------------ var http = require('http'); var fs = req ...

  7. 使用Layui、Axios、Springboot(Java) 实现EasyExcel的导入导出(浏览器下载)

    实现EasyExcel的导入导出(浏览器下载) 实现三个按钮的功能,但是却花费了一天的时间包括总结. 使用到的技术:springboot layui axios EasyExcel mybatis-p ...

  8. 02 CTF WEB 知识梳理

    1. 工具集 基础工具 Burpsuit, Python, FireFox(Hackbar, FoxyProxy, User-Agent Swither .etc) Burpsuit 代理工具,攻击w ...

  9. 三分钟了解B2B CRM系统的特点

    最近很多朋友想了解什么是B2B CRM系统,说到这里小Z先来给大家说说什么是B2B--B2B原本写作B to B,是Business-to-Business的缩写.正常来说就是企业与企业之间的生意往来 ...

  10. foreign key 多对一 多对对 一对一

    使用foreign key 要清除先有哪张表再有哪张表,后表对应前表 例如现有部门再有员工,所以员工对应部门 现有作者后有书,所以书对应作者 现有潜在顾客后有顾客,所以顾客对应潜在顾客 多对多建立3张 ...