golang 避坑指南interface 之坑多多 | 文末深圳 Meetup
Posted Go语言中文网
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang 避坑指南interface 之坑多多 | 文末深圳 Meetup相关的知识,希望对你有一定的参考价值。
interface{}和void*
/Object 是一样的吗?
先来看一段关于 interface 的官方说明
Under the covers, interfaces are implemented as two elements, a type and a value. The value, called the interface’s dynamic value, is an arbitrary concrete value and the type is that of the value. For the int value 3, an interface value contains, schematically, (int, 3).
事实上,interface 的实现分为两种 eface,iface,它们结构如下:
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
iface 为有方法声明的 interface,eface 为空的 interface 即 interface{}。我们可以看到 eface 结构体中只存两个指针:一个_type 类型指针用于存数据的实际类型,一个通用指针(unsafe.Pointer)存实际数据;iface 则比较复杂,这里不做详细展开了,你只需要记住它也是两个指针,和 eface 一样其中一个用来存数据,另一个 itab 指针用来存数据类型以及方法集。因此 interface 类型的变量所占空间一定为 16。
明白了原理,我们看一段简单代码:
example1
type Student struct {
Name string
}
var b interface{} = Student{
Name: "aaa",
}
var c = b.(Student)
c.Name = "bbb"
fmt.Println(b.(Student).Name)
你觉得输出是什么?
如果你的答案是 bbb,恭喜你,你掉坑了!不信你运行试试:
//example1 output
aaa
这个坑道理很简单,根据我们开头讲的 interface 原理,很多从 java 或 c/c++转过来的程序员都把 interface{}看成了 Object 或`void*``。
的确,很多场景(如传参、返回值)它们的确很类似;但请注意,在底层实现上它们是完全不同的。java 的 Object 以及 c 语言的void*
可以通过通过强转为某个类型获取指向原数据的一个目标类型的引用或指针,因此如果在这个引用或指针上进行修改操作,原数据也会被修改;但是 golang 的 interface 和具体类型之间的转换、赋值是将实际数据复制了一份进行操作的。例如上例中的
var c = b.(Student)
实际的过程是首先将 b 指向的数据复制一份,然后转换为 Student 类型赋值给 c。
记住了吗?好,我们看个类似的例子:
example2
type Student struct {
Name string
}
a := Student{Name:"aaa"}
var b interface{} = a
a.Name = "bbb"
fmt.Println(b.(Student).Name)
这次,输出结果又会是什么?
如果你给出的答案是 aaa,恭喜你脱坑了!
遇到 interface 类的返回值你要注意了
看个简短的例子:
example3
func GetReader(id int64) io.Reader {
var r *MyReader = nil
if id > 0 && id < 10000{
r = openReader(id)
}
return r
}
func main() {
r := GetReader(-2)
if r == nil {
fmt.Println("bad reader")
} else {
fmt.Println("valid reader")
}
}
其中 MyReader 为某个实现了 io.Reader 的结构体类型,openReader 根据传入参数返回一个 MyReader 结构体指针。
你觉得这段程序会输出什么呢?会输出"bad reader"吗?
答案是刚好相反:
//example3 output
valid reader
为了解释这个结果,我们再看两段简单的代码:
example4
var b interface{} = nil
fmt.Println(b == nil)
example5
var a *Student = nil
var b interface{} = a
fmt.Println(b == nil)
输出分别为:
//example4 output
true
//example5 output
false
相信仔细对比过后,你应该已经有答案了:当一个指针赋值给 interface 类型时,无论此指针是否为 nil,赋值过的 interface 都不为 nil。
ok,结论已经有了,那为什么是这样呢?还记得本文开头介绍的 interface 底层实现吗?无论是 iface 还是 eface,都有两个指针,指向数据的是通用指针,还有一个指针用于指定数据类型或方法集;当我们将一个 nil 指针赋值给 interface 时,实际是对 interface 的这两个指针分别赋值,虽言数据指针 data 为 nil,但是类型指针_type 或 tab 并不是 nil,他将指向你的空指针的类型,因此赋值的结果 interface 肯定不是 nil 啦!
什么?interface 还能嵌入 struct?
众所周知,一个新定义的 type 要想实现某个 interface,一定需要将该 interface 的所有方法都实现一遍。对吗?
老规矩,先上例子:
example6
type Talkable interface {
TalkEnglish(string)
TalkChinese(string)
}
type Student1 struct {
Talkable
Name string
Age int
}
func main(){
a := Student1{Name: "aaa", Age: 12}
var b Talkable = a
fmt.Println(b)
}
以上的代码时 100%能编译运行的。输出为:
// example6 output
{<nil> aaa 12}
这是一种取巧的方法,将 interface 嵌入结构体,可以使该类型快速实现该 interface。所以,本小节开头的话并不成立。但是如果我们调一下方法呢?
example7
...
func main(){
a := Student1{Name: "aaa", Age: 12}
a.TalkEnglish("nice to meet you\n")
}
可以预见到的,报错了:
//example7 output
panic: runtime error: invalid memory address or nil pointer dereference
并没有实现 interface 的方法,当然会报错。我们可以只实现 interface 的一部分方法,比如我只需要用到 Talkable 的 TalkEnglish 方法:
func (s *Student1) TalkEnglish(s1 string) {
fmt.Printf("I'm %s,%d years old,%s", s.Name, s.Age, s1)
}
或者只需要讲中文:
func (s *Student1) TalkChinese(s1 string) {
fmt.Printf("我是 %s, 今年%d岁,%s", s.Name, s.Age, s1)
}
总而言之,嵌入 interface 的好处就是可以帮在整体类型兼容某个接口的前提下,允许你你针对你的应用场景只实现 interface 中的一部分方法。但是在使用时要注意没有实现的方法在调用时会 panic。
总结
interface 时 golang 编程中使用得非常频繁的特性,我们需要明白它的底层结构,以及一些编译和运行时的特殊之处,能帮我们避免一些不必要的麻烦:
-
interface 很类似 void*
,但在值类型的变量和 interface 类型变量相互赋值时,会发生数据的复制。 -
将某个类型的指针赋值给 interface,interface 的值永远不可能是 nil; -
interface 可以嵌入结构体,帮类型快速实现接口,但是注意如果调用未实现的方法则会 panic;
12 月 15 日 Go语言中文网深圳Meetup,免费报名
报名方式点击底部 阅读原文
以上是关于golang 避坑指南interface 之坑多多 | 文末深圳 Meetup的主要内容,如果未能解决你的问题,请参考以下文章