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

接口类型

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

interface是一组函数或方法的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),不关心属性(数据),只关心行为(方法),请牢记接口(interface)是一种类型。

接口与鸭子类型:

维基百科的定义:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

翻译过来就是:如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子。

Duck Typing,鸭子类型,是动态编程语言的一种对象推断策略,它更关注对象能如何被使用,而不是对象的类型本身。Go 语言作为一门静态语言,它通过接口的方式完美支持鸭子类型。

而在静态语言如 Java, C++ 中,必须要显示地声明实现了某个接口之后,才能用在任何需要这个接口的地方。如果你在程序中调用某个数,却传入了一个根本就没有实现另一个的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。

动态语言和静态语言的差别在此就有所体现。静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。当然,静态语言要求程序员在编码阶段就要按照规定来编写程序,为每个变量规定数据类型,这在某种程度上,加大了工作量,也加长了代码量。动态语言则没有这些要求,可以让人更专注在业务上,代码也更短,写起来更快。

Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查。Go 采用了折中的做法:不要求类型显示地声明实现了某个接口,只要实现了相关的方法即可,编译器就能检测到。

总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。

Go语言的多态性:

在Java语言中,多态是通过继承和重写来体现的,而Go中的多态性就是在接口的帮助下实现的。接口可以在Go中隐式地实现。如果类型为接口中声明的所有方法提供了定义,则该类型实现了这个接口。

任何定义了接口所有方法的类型都被称为隐式地实现了该接口。

类型接口的变量可以保存实现接口的任何值。接口的这个属性用于实现Go中的多态性。

什么是接口

简言之:

  1. 接口是一组方法签名
  2. 接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口

接口的定义

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方法来做一些事情。

实现接口的条件

一个类型只要实现了接口中的全部方法,那么就实现了这个接口。换句话说,接口就是一组需要实现的方法签名

定义一个接口并实现它:

  1. type IPhone interface {
  2. call()
  3. }
  4. type NokiaPhone struct {
  5. }
  6. func (nokiaPhone NokiaPhone) call() {
  7. fmt.Println("I am Nokia, I can call you!")
  8. }
  9. type MobilePhone struct {
  10. }
  11. func (mobilePhone MobilePhone) call() {
  12. fmt.Println("I am mobile, I can call you!")
  13. }

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

接口类型变量

为什么要实现接口呢?

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

  1. func main() {
  2. var x IPhone // 声明一个Sayer类型的变量x
  3. a := NokiaPhone{} // 实例化一个NokiaPhone
  4. b := MobilePhone{} // 实例化一个MobilePhone
  5. x = a // 可以把NokiaPhone实例直接赋值给x
  6. x.call() // I am Nokia, I can call you!
  7. x = b // 可以把Phone实例直接赋值给x
  8. x.call() // I am mobile, I can call you!
  9. }

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

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

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

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?

定义一个方法,有值类型接收者和指针类型接收者两种。二者都可以调用方法,因为 Go 语言编译器自动做了转换,所以值类型接收者和指针类型接收者是等价的。但是在接口的实现中,值类型接收者和指针类型接收者不一样。

  1. type Stringer interface {
  2. String() string
  3. }
  4. type person struct {
  5. name string
  6. age uint
  7. addr address
  8. }
  9. type address struct {
  10. province string
  11. city string
  12. }
  13. func (p person) String() string{
  14. return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
  15. }
  16. func (addr address) String() string{
  17. return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
  18. }
  19. func printString(s fmt.Stringer){
  20. fmt.Println(s.String())
  21. }
  22. func main(){
  23. p := person{}
  24. printString(p) //正常输出
  25. printString(p.addr) //正常输出
  26. printString(&p) //正常输出
  27. }

把变量 p 的指针作为实参传给 printString 函数也是可以的,编译运行都正常。这就证明了以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口

值接收者(p person)实现了 Stringer 接口,那么类型 person 和它的指针类型*person就都实现了 Stringer 接口。

再把接收者改成指针类型:

  1. type Stringer interface {
  2. String() string
  3. }
  4. type person struct {
  5. name string
  6. age uint
  7. addr address
  8. }
  9. type address struct {
  10. province string
  11. city string
  12. }
  13. func (p *person) String() string{
  14. return fmt.Sprintf("the name is %s,age is %d",p.name,p.age)
  15. }
  16. func (addr address) String() string{
  17. return fmt.Sprintf("the addr is %s%s",addr.province,addr.city)
  18. }
  19. func printString(s fmt.Stringer){
  20. fmt.Println(s.String())
  21. }
  22. func main(){
  23. p := person{}
  24. printString(p) //cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)
  25. printString(&p) //正常输出
  26. }

修改成指针类型接收者后会发现,提示错误:

  1. cannot use p (type person) as type fmt.Stringer in argument to printString:person does not implement fmt.Stringer (String method has pointer receiver)

意思就是类型 person 没有实现 Stringer 接口。这就证明了以指针类型接收者实现接口的时候,只有对应的指针类型才被认为实现了该接口

总结:

当值类型作为接收者时,person 类型和*person类型都实现了该接口。

当指针类型作为接收者时,只有*person类型实现了该接口。

可以发现,实现接口的类型都有*person,这也表明指针类型比较万能,不管哪一种接收者,它都能实现该接口。

类型与接口的关系

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。分别定义Sayer接口和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. // 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. }

嵌套得到的接口的使用与普通接口一样。

空接口

空接口的定义

空接口是指没有定义任何方法签名的接口。由于任何类型都至少实现了0个方法,因此任何类型都实现了空接口。

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

  1. type I interface{}
  2. func main() {
  3. var i I
  4. i = 42 //这个时候i就是int类型
  5. fmt.Printf("%v,%T\n", i, i)
  6. i = "hello" //这个时候i就是string类型
  7. fmt.Printf("%v,%T\n", i, i)
  8. }
  1. 42,int
  2. hello,string

空接口的应用

空接口作为函数的参数

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

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

空接口作为map的值

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

  1. // 空接口作为map值
  2. var person = make(map[string]interface{})
  3. person["name"] = "张三"
  4. person["age"] = 23
  5. person["married"] = false
  6. fmt.Println(person)

空接口对切片的影响

  1. 若将一个arrayslice赋值给空接口,这个空接口无法再进行切片
  2. arrayslice赋值给空接口的行为不是复制,而是类似指针效果,只不过无法再进行切片,但元素和原来的arrayslice及其衍生的,都有关联
  1. func main() {
  2. s := []int{2, 3, 5, 7, 11, 13}
  3. var e interface{}
  4. e = s
  5. f := s[0:3]
  6. f[2] = 55
  7. fmt.Printf("%T,%v\n", s, s)
  8. fmt.Printf("%T,%v\n", e, e)
  9. fmt.Printf("%T,%v\n", f, f)
  10. }

输出

  1. []int,[2 3 55 7 11 13]
  2. []int,[2 3 55 7 11 13]
  3. []int,[2 3 55]

若改为

  1. func main() {
  2. s := []int{2, 3, 5, 7, 11, 13}
  3. var e interface{}
  4. e = s
  5. g := e[1:3]
  6. fmt.Println(g)
  7. }

报错

  1. cannot slice e (type interface {})

空接口的赋值

空接口可以存储任意值,但不代表任意类型就可以存储空接口类型的值

从实现的角度看,任何类型的值都满足空接口。因此空接口类型可以保存任何值,也可以从空接口中取出原值。

但要是把一个空接口类型的对象,再赋值给一个固定类型(比如 int, string等类型)的对象赋值,是会报错的。

  1. func main() {
  2. // 声明a变量, 类型int, 初始值为1
  3. var a int = 1
  4. // 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
  5. var i interface{} = a
  6. // 声明b变量, 尝试赋值i
  7. var b int = i
  8. }

这个报错,它就好比可以放进行礼箱的东西,肯定能放到集装箱里,但是反过来,能放到集装箱的东西就不一定能放到行礼箱了,在 Go 里就直接禁止了这种反向操作。

  1. cannot use i (type interface {}) as type int in assignment: need type assertion

类型断言

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

接口值

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

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

请看下图分解:

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

  1. // 安全类型断言
  2. <目标类型的值>,<布尔参数> := <表达式>.( 目标类型 )
  3. //非安全类型断言
  4. <目标类型的值> := <表达式>.( 目标类型 )

示例代码:

  1. func main() {
  2. var i1 interface{} = new (Student)
  3. s := i1.(Student) //不安全,如果断言失败,会直接panic
  4. fmt.Println(s)
  5. var i2 interface{} = new(Student)
  6. s, ok := i2.(Student) //安全,断言失败,也不会panic,只是ok的值为false
  7. if ok {
  8. fmt.Println(s)
  9. }
  10. }
  11. type Student struct {}

断言其实还有另一种形式,就是用在利用 switch语句判断接口的类型。每一个case会被顺序地考虑。当命中一个case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case匹配的情况:

  1. switch ins:=s.(type) {
  2. case Triangle:
  3. fmt.Println("三角形。。。",ins.a,ins.b,ins.c)
  4. case Circle:
  5. fmt.Println("圆形。。。。",ins.radius)
  6. case int:
  7. fmt.Println("整型数据。。")
  8. }

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

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

Golang通脉之接口的更多相关文章

  1. golang中的接口实现(一)

    golang中的接口实现 // 定义一个接口 type People interface { getAge() int // 定义抽象方法1 getName() string // 定义抽象方法2 } ...

  2. golang中的接口值

    package main import ( "bytes" "fmt" "io" ) // 此处的w参数默认是一个空接口,当传递进来buf参 ...

  3. 以太坊之——golang以太坊接口调用

    Go语言具有简单易学.功能强大,可跨平台编译等众多优势,所以这里选择以Go语言切入以太坊. 开始之前需要以下环境: Ubuntu(这里以ubuntu16.04为例) geth Ubuntu16.04安 ...

  4. golang方法和接口

    一.  go方法 go方法:在函数的func和函数名间增加一个特殊的接收器类型,接收器可以是结构体类型或非结构体类型.接收器可以在方法内部访问.创建一个接收器类型为Type的methodName方法. ...

  5. GOLANG的继承+接口语法练习

    继承与接口同时存在 在Golang语言中,可以这么说:接口是继承的功能补充! 武当派有一个徒弟结构体,它继承WuDangMaster结构体的字段及方法 武林之中还有一个泰山北斗,名约少林派,少林入门神 ...

  6. Golang通脉之面向对象

    面向对象的三大特征: 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式 继承:使得子类具有父类的属性和方法或者重新定义.追加属性和方法等 多态:不同对象中同种行为的不同实现方式 Go并不是一个纯 ...

  7. Golang通脉之错误处理

    在实际工程项目中,总是通过程序的错误信息快速定位问题,但是又不希望错误处理代码写的冗余而又啰嗦.Go语言没有提供像Java.C#语言中的try...catch异常处理方式,而是通过函数返回值逐层往上抛 ...

  8. Golang通脉之反射

    什么是反射 官方关于反射定义: Reflection in computing is the ability of a program to examine its own structure, pa ...

  9. Golang 函数 方法 接口的简单介绍

    函数 函数是基本的代码块,通常我们会将一个功能封装成一个函数,方便我们调用,同时避免代码臃肿复杂. 函数的基本格式 func TestFunc(a int, b string) (int, strin ...

随机推荐

  1. centos7修改服务器时区

    查看时区设置 timedatectl 列出所有时区,通过键盘上下键进行浏览 timedatectl list-timezones 修改服务器时区为Africa/Lagos # 拉各斯的时区,UTC+1 ...

  2. Pikachu靶场通关之XSS(跨站脚本)

    一.XSS(跨站脚本)概述 Cross-Site Scripting 简称为"CSS",为避免与前端叠成样式表的缩写"CSS"冲突,故又称XSS.一般XSS可以 ...

  3. ByteArrayOutputStream小测试

    import java.io.*; import org.junit.Test; public class ByteArrayOutputStreamTest { @Test public void ...

  4. (未完)Java集合框架梳理(基于JDK1.8)

    Java集合类主要由两个接口Collection和Map派生出来的,Collection派生出了三个子接口:List.Set.Queue(Java5新增的队列),因此Java集合大致也可分成List. ...

  5. 【Python学习】print语句

    一.print 可以向屏幕上输出信息,print 后面一个空格再加上''中间放入要输出的内容. 二.print可以用逗号分隔语句,但是每有一个逗号就会出来一个空格. 1 >>> pr ...

  6. 【OWASP TOP10】2021年常见web安全漏洞TOP10排行

    [2021]常见web安全漏洞TOP10排行 应用程序安全风险 攻击者可以通过应用程序中许多的不同的路径方式去危害企业业务.每种路径方法都代表了一种风险,这些风险都值得关注. 什么是 OWASP TO ...

  7. Nginx系列(8)- Nginx安装 | Docker环境下部署

    Docker环境下部署Nginx https://www.cnblogs.com/gltou/p/15186971.html

  8. php 解决返回数据 数字 变成科学计数法后转换问题

    链接 https://blog.csdn.net/liuxin_0725/article/details/81514961 问题 id int型 数字过长,json_decode的时候已经转成科学计数 ...

  9. frida的安装教程-配合夜神模拟器

    Frida安装 一.PC端安装 1. 安装frida 默认安装最新版的Frida pip install frida 因为我用的是夜神模拟器,可能不支持最新版,所以下载的之前版本. pip insta ...

  10. 鸿蒙内核源码分析(静态站点篇) | 五一哪也没去就干了这事 | 百篇博客分析OpenHarmony源码 | v52.02

    百篇博客系列篇.本篇为: v52.xx 鸿蒙内核源码分析(静态站点篇) | 五一哪也没去就干了这事 | 51.c.h.o 前因后果相关篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 ...