什么是interface


在面向对象编程中,可以这么说:“接口定义了对象的行为”, 那么具体的实现行为就取决于对象了。

在Go中,接口是一组方法签名(声明的是一组方法的集合)。当一个类型为接口中的所有方法提供定义时,它被称为实现该接口。它与oop非常相似。接口指定类型应具有的方法,类型决定如何实现这些方法。

让我们来看看这个例子: Animal 类型是一个接口,我们将定义一个 Animal 作为任何可以说话的东西。这是 Go 类型系统的核心概念:我们根据类型可以执行的操作而不是其所能容纳的数据类型来设计抽象。

type Animal interface {
Speak() string
}

  

非常简单:我们定义 Animal 为任何具有 Speak 方法的类型。Speak 方法没有参数,返回一个字符串。所有定义了该方法的类型我们称它实现了 Animal 接口。Go 中没有 implements 关键字,判断一个类型是否实现了一个接口是完全是自动地。让我们创建几个实现这个接口的类型:
type Dog struct {
} func (d Dog) Speak() string {
return "Woof!"
} type Cat struct {
} func (c Cat) Speak() string {
return "Meow!"
} type Llama struct {
} func (l Llama) Speak() string {
return "?????"
} type JavaProgrammer struct {
} func (j JavaProgrammer) Speak() string {
return "Design patterns!"
}

  

我们现在有四种不同类型的动物:DogCatLlama 和 JavaProgrammer。在我们的 main 函数中,我们创建了一个 []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} ,看看每只动物都说了些什么:

func main() {
animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}
for _, animal := range animals {
fmt.Println(animal.Speak())
}
}

  

interface{} 类型


interface{} 类型,空接口,是导致很多混淆的根源。interface{} 类型是没有方法的接口。由于没有 implements 关键字,所以所有类型都至少实现了 0 个方法,所以 所有类型都实现了空接口。这意味着,如果您编写一个函数以 interface{} 值作为参数,那么您可以为该函数提供任何值。例如:
func DoSomething(v interface{}) {
// ...
}

  

这里是让人困惑的地方:在 DoSomething 函数内部,v 的类型是什么?新手们会认为 v 是任意类型的,但这是错误的。v 不是任意类型,它是 interface{} 类型。对的,没错!当将值传递给DoSomething 函数时,Go 运行时将执行类型转换(如果需要),并将值转换为 interface{} 类型的值。所有值在运行时只有一个类型,而 v 的一个静态类型是 interface{}
这可能让您感到疑惑:好吧,如果发生了转换,到底是什么东西传入了函数作为 interface{} 的值呢?(具体到上例来说就是 []Animal 中存的是啥?)
 
一个接口值由两个字(32 位机器一个字是 32 bits,64 位机器一个字是 64 bits)组成;一个字用于指向该值底层类型的方法表,另一个字用于指向实际数据。我不想没完没了地谈论这个。
 

在我们上面的例子中,当我们初始化变量 animals 时,我们不需要像这样 Animal(Dog{}) 来显示的转型,因为这是自动地。这些元素都是 Animal 类型,但是他们的底层类型却不相同。

为什么这很重要呢?理解接口是如何在内存中表示的,可以使得一些潜在的令人困惑的事情变得非常清楚。比如,像 “我可以将 []T 转换为 []interface{}
吗?” 这种问题就容易回答了。下面是一些烂代码的例子,它们代表了对 interface{} 类型的常见误解:

package main

import (
"fmt"
) func PrintAll(vals []interface{}) {
for _, val := range vals {
fmt.Println(val)
}
} func main() {
names := []string{"stanley", "david", "oscar"}
PrintAll(names)
}

  

运行这段代码你会得到如下错误:cannot use names (type []string) as type []interface {} in argument to PrintAll。如果想使其正常工作,我们必须将 []string 转为 []interface{}

package main

import (
"fmt"
) func PrintAll(vals []interface{}) {
for _, val := range vals {
fmt.Println(val)
}
} func main() {
names := []string{"stanley", "david", "oscar"}
vals := make([]interface{}, len(names))
for i, v := range names {
vals[i] = v
}
PrintAll(vals)
}

  

很丑陋,但是生活就是这样,没有完美的事情。(事实上,这种情况不会经常发生,因为 []interface{} 并没有像你想象的那样有用)

指针和接口

 

接口的另一个微妙之处是接口定义没有规定一个实现者是否应该使用一个指针接收器或一个值接收器来实现接口。当给定一个接口值时,不能保证底层类型是否为指针。在前面的示例中,我们将方法定义在值接收者之上。让我们稍微改变一下,将 Cat 的 Speak() 方法改为指针接收器:
 
func (c *Cat) Speak() string {
return "Meow!"
}

  

运行上述代码,会得到如下错误:

cannot use Cat literal (type Cat) as type Animal in array or slice literal:
Cat does not implement Animal (Speak method has pointer receiver)

  

该错误的意思是:你尝试将 Cat 转为 Animal ,但是只有 *Cat 类型实现了该接口。你可以通过传入一个指针 (new(Cat) 或者 &Cat{})来修复这个错误。

animals := []Animal{Dog{}, new(Cat), Llama{}, JavaProgrammer{}}

 

让我们做一些相反的事情:我们传入一个 *Dog 指针,但是不改变 Dog 的 Speak() 方法:

animals := []Animal{new(Dog), new(Cat), Llama{}, JavaProgrammer{}}

  

这种方式可以正常工作,因为一个指针类型可以通过其相关的值类型来访问值类型的方法,但是反过来不行。即,一个 *Dog 类型的值可以使用定义在 Dog 类型上的 Speak() 方法,而 Cat 类型的值不能访问定义在 *Cat 类型上的方法。

这可能听起来很神秘,但当你记住以下内容时就清楚了:Go 中的所有东西都是按值传递的。每次调用函数时,传入的数据都会被复制。对于具有值接收者的方法,在调用该方法时将复制该值。例如下面的方法:

func (t T)MyMethod(s string) {
// ...
} 

是 func(T, string) 类型的方法。方法接收器像其他参数一样通过值传递给函数。

因为所有的参数都是通过值传递的,这就可以解释为什么 *Cat 的方法不能被 Cat 类型的值调用了。任何一个 Cat 类型的值可能会有很多 *Cat 类型的指针指向它,如果我们尝试通过 Cat 类型的值来调用 *Cat 的方法,根本就不知道对应的是哪个指针。相反,如果 Dog 类型上有一个方法,通过 *Dog 来调用这个方法可以确切的找到该指针对应的 Gog 类型的值,从而调用上面的方法。运行时,Go 会自动帮我们做这些,所以我们不需要像 C语言中那样使用类似如下的语句 d->Speak()

结语


我希望读完此文后你可以更加得心应手地使用 Go 中的接口,记住下面这些结论:

  • 通过考虑数据类型之间的相同功能来创建抽象,而不是相同字段
  • interface{} 的值不是任意类型,而是 interface{} 类型
  • 接口包含两个字的大小,类似于 (type, value)
  • 函数可以接受 interface{} 作为参数,但最好不要返回 interface{}
  • 指针类型可以调用其所指向的值的方法,反过来不可以
  • 函数中的参数甚至接受者都是通过值传递
  • 一个接口的值就是就是接口而已,跟指针没什么关系
  • 如果你想在方法中修改指针所指向的值,使用 * 操作符

Golang 之 interface接口全面理解的更多相关文章

  1. C#.NET常见问题(FAQ)-interface接口如何理解

    个人把interface理解为一种比较特殊的判断技巧,不是常规的变量类型比如判断字符串,判断数组,而是判断类的实例是否拥有某些属性或者方法(比如有十个女的穿一样的衣服,头上盖住,让新郎去猜哪一个是他的 ...

  2. golang基础--Interface接口

    接口是一个或多个方法签名名的集合,定义方式如下 type Interface_Name interface { method_a() string method_b() int .... } 只要某个 ...

  3. Golang 入门系列(四)如何理解interface接口

    前面讲了很多Go 语言的基础知识,包括go环境的安装,go语言的语法等,感兴趣的朋友,可以先看看之前的文章.https://www.cnblogs.com/zhangweizhong/category ...

  4. golang面向对象和interface接口

    一. golang面向对象介绍 1.golang也支持面向对象编程,但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言.2.golang没有类(class),golang语言的结合体(struc ...

  5. Golang基础(8):go interface接口

    一:接口概要 接口是一种重要的类型,他是一组确定的方法集合. 一个接口变量可以存储任何实现了接口方法的具体值.一个重要的例子就是io.Reader和io.Writer type Reader inte ...

  6. Golang的Interface是个什么鬼

    问题概述 Golang的interface,和别的语言是不同的.它不需要显式的implements,只要某个struct实现了interface里的所有函数,编译器会自动认为它实现了这个interfa ...

  7. golang的interface剖析

      背景: golang的interface是一种satisfied式的.A类只要实现了IA interface定义的方法,A就satisfied了接口IA.更抽象一层,如果某些设计上需要一些更抽象的 ...

  8. Golang的反射reflect深入理解和示例

    编程语言中反射的概念 在计算机科学领域,反射是指一类应用,它们能够自描述和自控制.也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examin ...

  9. golang 关于 interface 的学习整理

    Golang-interface(四 反射) go语言学习-reflect反射理解和简单使用 为什么在Go语言中要慎用interface{} golang将interface{}转换为struct g ...

随机推荐

  1. OceanBase 2.0让百万支付不是梦?

    小蚂蚁说: 你们都很关心的 “OB双11大促实战分享” 专题来啦!本系列将为你系统性的介绍OceanBase支撑蚂蚁双11背后的技术原理和实战分享.从平台到架构,再到实现,一起来探索蚂蚁双11这场神秘 ...

  2. Java Deque 队列 栈

    垃圾JDK啊 Deque这个接口,既承担着FIFO的任务,即队列,也承担着LIFO的任务,即栈 目前jdk里面实现了这个接口的类有两个,一个是ArrayDeque,另一个是LinkedList 但是由 ...

  3. apache kafka技术分享系列(目录索引)

    https://blog.csdn.net/lizhitao/article/details/39499283 https://blog.csdn.net/lizhitao

  4. Django 修改视图文件(views.py)并加载Django模块 + 利用render_to_response()简化加载模块 +locals()

    修改视图代码,让它使用 Django 模板加载功能而不是对模板路径硬编码.返回 current_datetime 视图,进行如下修改: from django.template.loader impo ...

  5. RNA velocity | RNA速率

    单细胞转录组确实是利器,但我们大多只利用了表达的信息,而从reads到表达之间的信息完全被我们忽略了. 最近nature发了一篇单细胞方法类文章,讲得就是如何利用RNA velocity来做细胞发育路 ...

  6. centos7安装doxygen

    编译 编译过程参考官网:https://www.stack.nl/~dimitri/doxygen/download.html 编译过程: git clone https://github.com/d ...

  7. vue组件利用formdata图片预览以及上传

    转载修改 在项目中直接新建一个单文件页,复制一下代码即可       upload组件: <template> <div class="vue-uploader" ...

  8. linux下read命令详解

    要与Linux交互,脚本获取键盘输入的结果是必不可少的,read可以读取键盘输入的字符. read [-rs] [-a ARRAY] [-d delim] [-n nchars] [-N nchars ...

  9. Python—全局变量、局部变量、匿名函数

    局部变量和全局变量 college1 = 'JMU' #全局变量 def change_name(name): college1 = 'LiGong' #局部变量,此函数是其作用域 # global ...

  10. linux下mysql多实例安装

    1.MySQL多实例介绍 1.1.什么是MySQL多实例 MySQL多实例就是在一台机器上开启多个不同的服务端口(如:3306,3307),运行多个MySQL服务进程,通过不同的socket监听不同的 ...