Golang语言社区--理解 go interface 的 5 个关键点
大家好,我是社区主编彬哥,本篇文章是给大家转载的关于Go语言中interface相关的。
1、interface 是一种类型
type I interface {
Get() int
}
首先 interface 是一种类型,从它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。
go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface。
如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。go 没有显式的关键字用来实现 interface,只需要实现 interface 包含的方法即可。
2、interface 变量存储的是实现者的值
//1
type I interface {
Get() int
Set(int)
}
//2
type S struct {
Age int
}
func(s S) Get()int {
return s.Age
}
func(s *S) Set(age int) {
s.Age = age
}
//3
func f(i I){
i.Set(10)
fmt.Println(i.Get())
}
func main() {
s := S{}
f(&s) //4
}
这段代码在 #1 定义了 interface I,在 #2 用 struct S 实现了 I 定义的两个方法,接着在 #3 定义了一个函数 f 参数类型是 I,S 实现了 I 的两个方法就说 S 是 I 的实现者,执行 f(&s) 就完了一次 interface 类型的使用。
interface 的重要用途就体现在函数 f 的参数中,如果有多种类型实现了某个 interface,这些类型的值都可以直接使用 interface 的变量存储。
s := S{}
var i I //声明 i
i = &s //赋值 s 到 i
fmt.Println(i.Get())
不难看出 interface 的变量中存储的是实现了 interface 的类型的对象值,这种能力是 duck typing。在使用 interface 时不需要显式在 struct 上声明要实现哪个 interface ,只需要实现对应 interface 中的方法即可,go 会自动进行 interface 的检查,并在运行时执行从其他类型到 interface 的自动转换,即使实现了多个 interface,go 也会在使用对应 interface 时实现自动转换,这就是 interface 的魔力所在。
3、如何判断 interface 变量存储的是哪种类型
一个 interface 被多种类型实现时,有时候我们需要区分 interface 的变量究竟存储哪种类型的值,go 可以使用 comma, ok 的形式做区分 value, ok := em.(T):em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T。
if t, ok := i.(*S); ok {
fmt.Println("s implements I", t)
}
ok 是 true 表明 i 存储的是 *S 类型的值,false 则不是,这种区分能力叫 Type assertions (类型断言)。
如果需要区分多种类型,可以使用 switch 断言,更简单直接,这种断言方式只能在 switch 语句中使用。
switch t := i.(type) {
case *S:
fmt.Println("i store *S", t)
case *R:
fmt.Println("i store *R", t)
}
4、空的 interface
interface{} 是一个空的 interface 类型,根据前文的定义:一个类型如果实现了一个 interface 的所有方法就说该类型实现了这个 interface,空的 interface 没有方法,所以可以认为所有的类型都实现了 interface{}。如果定义一个函数参数是 interface{} 类型,这个函数应该可以接受任何类型作为它的参数。
func doSomething(v interface{}){
}
如果函数的参数 v 可以接受任何类型,那么函数被调用时在函数内部 v 是不是表示的是任何类型?并不是,虽然函数的参数可以接受任何类型,并不表示 v 就是任何类型,在函数 doSomething 内部 v 仅仅是一个 interface 类型,之所以函数可以接受任何类型是在 go 执行时传递到函数的任何类型都被自动转换成 interface{}。go 是如何进行转换的,以及 v 存储的值究竟是怎么做到可以接受任何类型的,感兴趣的可以看看 Russ Cox 关于 interface 的实现 。
既然空的 interface 可以接受任何类型的参数,那么一个 interface{}类型的 slice 是不是就可以接受任何类型的 slice ?
func printAll(vals []interface{}) { //1
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 错误,why?
这个错误说明 go 没有帮助我们自动把 slice 转换成 interface{} 类型的 slice,所以出错了。go 不会对 类型是interface{} 的 slice 进行转换 。为什么 go 不帮我们自动转换,一开始我也很好奇,最后终于在 go 的 wiki 中找到了答案 https://github.com/golang/go/wiki/InterfaceSlice 大意是 interface{} 会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的N*2,但是 []T 的长度是N*sizeof(T),两种 slice 实际存储值的大小是有区别的(文中只介绍两种 slice 的不同,至于为什么不能转换猜测可能是 runtime 转换代价比较大)。
但是我们可以手动进行转换来达到我们的目的。
var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, d := range dataSlice {
interfaceSlice[i] = d
}
5、interface 的实现者的 receiver 如何选择
在我们上文的例子中调用 f 是 f(&s) 也就是 S 的指针类型,为什么不能是 f(s) 呢,如果是 s 会有什么问题?改成 f(s) 然后执行代码。
cannot use s (type S) as type I in argument to f:
S does not implement I (Set method has pointer receiver)
这个错误的意思是 S 没有实现 I,哪里出了问题?关键点是 S 中 set 方法的 receiver 是个 pointer *S 。
interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver,上面代码中的 S 的 Set receiver 是 pointer,也就是实现 I 的两个方法的 receiver 一个是 value 一个是 pointer,使用 f(s)的形势调用,传递给 f 的是个 s 的一份拷贝,在进行 s 的拷贝到 I 的转换时,s 的拷贝不满足 Set 方法的 receiver 是个 pointer,也就没有实现 I。go 中函数都是按值传递即 passed by value。
那反过来会怎样,如果 receiver 是 value,函数用 pointer 的形式调用?
type I interface {
Get() int
Set(int)
}
type SS struct {
Age int
}
func (s SS) Get() int {
return s.Age
}
func (s SS) Set(age int) {
s.Age = age
}
func f(i I) {
i.Set(10)
fmt.Println(i.Get())
}
func main(){
ss := SS{}
f(&ss) //ponter
f(ss) //value
}
I 的实现者 SS 的方法 receiver 都是 value receiver,执行代码可以看到无论是 pointer 还是 value 都可以正确执行。
导致这一现象的原因是什么?
如果是按 pointer 调用,go 会自动进行转换,因为有了指针总是能得到指针指向的值是什么,如果是 value 调用,go 将无从得知 value 的原始值是什么,因为 value 是份拷贝。go 会把指针进行隐式转换得到 value,但反过来则不行。
对于 receiver 是 value 的 method,任何在 method 内部对 value 做出的改变都不影响调用者看到的 value,这就是按值传递。
另一个说明上述现象的例子是这样的来自 https://play.golang.org/p/TvR758rfre
package main
import (
"fmt"
)
type Animal interface {
Speak() string
}
type Dog struct {
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
}
//1
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!"
}
func main() {
animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}
for _, animal := range animals {
fmt.Println(animal.Speak())
}
}
1Cat 的 speak receiver 是 pointer,interface Animal 的 slice,Cat 的值是一个 value,同样会因为 receiver 不一致而导致无法执行。
- 流水灯
- Chrome 自动化交互利器:用 tampermonkey 向页面注入自定义 Javascript
- 一次小折腾:PyCharm 调用 Cygwin Python 找不到 time、sys 等内置模块
- 网站数据统计分析之二:前端日志采集是与非
- Linux Shell 从入门到删除根目录跑路指南
- SSD Win8 系统盘 4K 无损对齐历险记
- 详解 MySQL 5.7 新的权限与安全问题
- Hive Lock 那些事儿
- Shell 黑科技之匿名函数实现任务并行化
- 文本编辑利器Notepad++ 10个强大而又鲜为人知的特性
- 记一次诡异的 ssh 互信免密码登录失败
- Linux下恶意文件大规模共性分析探讨
- 虚拟时钟
- 线上服务 CPU 100%?一键定位 so easy!
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法