golang: 详解interface和nil null和nil
赞15
摘要 golang的nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。nil是预先说明的标识符,也即通常意义上的关键字。在golang中,nil只能赋值给指针、channel、func、interface、map或slice类型的变量。如果未遵循这个规则,则会引发panic。对此官方文档有明确的说明:http://pkg.golang.org/pkg/builtin/#Type golang interface nil gdb error
golang的nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。nil是预先说明的标识符,也即通常意义上的关键字。在golang中,nil只能赋值给指针、channel、func、interface、map或slice类型的变量。如果未遵循这个规则,则会引发panic。对此官方有明确的说明:http://pkg.golang.org/pkg/builtin/#Type
golang中的interface类似于java的interface、PHP的interface或C++的纯虚基类。接口就是一个协议,规定了一组成员。这个没什么好说的,本文不打算对宏观上的接口概念和基于接口的范式编程做剖析。golang语言的接口有其独到之处:只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方。这种做法的学名叫做Structural Typing,有人也把它看作是一种静态的Duck Typing。所谓类型T的公开方法完全满足接口I的要求,也即是类型T实现了接口I所规定的一组成员。
在底层,interface作为两个成员来实现,一个类型和一个值。对此官方也有文档说明:http://golang.org/doc/go_faq.html#nil_error,如果您不习惯看英文,这里有一篇柴大的翻译:Go中error类型的nil值和nil。
接下来通过编写测试代码和gdb来看看interface倒底是什么。会用到反射,如果您不太了解golang的反射是什么,这里有刑星翻译自官方博客的一篇文章:反射的规则,原文在:laws-of-reflection。
$GOPATH/src
----interface_test
--------main.go
main.go的代码如下:
01packagemain
02
03import(
04"fmt"
05"reflect"
06)
07
08funcmain(){
09varvalinterface{}=int64(58)
10fmt.Println(reflect.TypeOf(val))
11val=50
12fmt.Println(reflect.TypeOf(val))
13}
我们已经知道接口类型的变量底层是作为两个成员来实现,一个是type,一个是data。type用于存储变量的动态类型,data用于存储变量的具体数据。在上面的例子中,第一条打印语句输出的是:int64。这是因为已经显示的将类型为int64的数据58赋值给了interface类型的变量val,所以val的底层结构应该是:(int64, 58)。我们暂且用这种二元组的方式来描述,二元组的第一个成员为type,第二个成员为data。第二条打印语句输出的是:int。这是因为字面量的整数在golang中默认的类型是int,所以这个时候val的底层结构就变成了:(int, 50)。借助于gdb很容易观察到这点:
1$cd$GOPATH/src/interface_test
2$gobuild-gcflags"-N-l"
3$gdbinterface_test
接下来说说interface类型的值和nil的比较问题。这是个比较经典的问题,也算是golang的一个坑。
---来自柴大的翻译
接着来看代码:
01packagemain
02
03import(
04"fmt"
05)
06
07funcmain(){
08varvalinterface{}=nil
09ifval==nil{
10fmt.Println("valisnil")
11}else{
12fmt.Println("valisnotnil")
13}
14}
变量val是interface类型,它的底层结构必然是(type, data)。由于nil是untyped(无类型),而又将nil赋值给了变量val,所以val实际上存储的是(nil, nil)。因此很容易就知道val和nil的相等比较是为true的。
1$cd$GOPATH/src/interface_test
2$gobuild
3$./interface_test
4valisnil
对于将任何其它有意义的值类型赋值给val,都导致val持有一个有效的类型和数据。也就是说变量val的底层结构肯定不为(nil, nil),因此它和nil的相等比较总是为false。
上面的讨论都是在围绕值类型来进行的。在继续讨论之前,让我们来看一种特例:(*interface{})(nil)。将nil转成interface类型的指针,其实得到的结果仅仅是空接口类型指针并且它指向无效的地址。注意是空接口类型指针而不是空指针,这两者的区别蛮大的,学过C的童鞋都知道空指针是什么概念。
关于(*interface{})(nil)还有一些要注意的地方。这里仅仅是拿(*interface{})(nil)来举例,对于(*int)(nil)、(*byte)(nil)等等来说是一样的。上面的代码定义了接口指针类型变量val,它指向无效的地址(0x0),因此val持有无效的数据。但它是有类型的(*interface{})。所以val的底层结构应该是:(*interface{}, nil)。有时候您会看到(*interface{})(nil)的应用,比如var ptrIface = (*interface{})(nil),如果您接下来将ptrIface指向其它类型的指针,将通不过编译。或者您这样赋值:*ptrIface = 123,那样的话编译是通过了,但在运行时还是会panic的,这是因为ptrIface指向的是无效的内存地址。其实声明类似ptrIface这样的变量,是因为使用者只是关心指针的类型,而忽略它存储的值是什么。还是以例子来说明:
01packagemain
02
03import(
04"fmt"
05)
06
07funcmain(){
08varvalinterface{}=(*interface{})(nil)
09//val=(*int)(nil)
10ifval==nil{
11fmt.Println("valisnil")
12}else{
13fmt.Println("valisnotnil")
14}
15}
很显然,无论该指针的值是什么:(*interface{}, nil),这样的接口值总是非nil的,即使在该指针的内部为nil。
1$cd$GOPATH/src/interface_test
2$gobuild
3$./interface_test
4valisnotnil
interface类型的变量和nil的相等比较出现最多的地方应该是error接口类型的值与nil的比较。有时候您想自定义一个返回错误的函数来做这个事,可能会写出以下代码:
01packagemain
02
03import(
04"fmt"
05)
06
07typedatastruct{}
08
09func(this*data)Error()string{return""}
10
11functest()error{
12varp*data=nil
13returnp
14}
15
16funcmain(){
17vareerror=test()
18ife==nil{
19fmt.Println("eisnil")
20}else{
21fmt.Println("eisnotnil")
22}
23}
但是很可惜,以上代码是有问题的。
1$cd$GOPATH/src/interface_test
2$gobuild
3$./interface_test
4eisnotnil
我们可以来分析一下。error是一个接口类型,test方法中返回的指针p虽然数据是nil,但是由于它被返回成包装的error类型,也即它是有类型的。所以它的底层结构应该是(*data, nil),很明显它是非nil的。
可以打印观察下底层结构数据:
01packagemain
02
03import(
04"fmt"
05"unsafe"
06)
07
08typedatastruct{}
09
10func(this*data)Error()string{return""}
11
12functest()error{
13varp*data=nil
14returnp
15}
16
17funcmain(){
18vareerror=test()
19
20d:=(*struct{
21itabuintptr
22datauintptr
23})(unsafe.Pointer(&e))
24
25fmt.Println(d)
26}
1$cd$GOPATH/src/interface_test
2$gobuild
3$./interface_test
4&{30789079120}
正确的做法应该是:
01packagemain
02
03import(
04"fmt"
05)
06
07typedatastruct{}
08
09func(this*data)Error()string{return""}
10
11funcbad()bool{
12returntrue
13}
14
15functest()error{
16varp*data=nil
17ifbad(){
18returnp
19}
20returnnil
21}
22
23funcmain(){
24vareerror=test()
25ife==nil{
26fmt.Println("eisnil")
27}else{
28fmt.Println("eisnotnil")
29}
30}
分享到: 15赞
声明:OSCHINA 博客文章版权属于作者,受法律保护。未经作者同意不得转载。
? 上一篇
下一篇 ?
开源中国-程序员在线工具:API文档大全(120+) JS在线编辑演示 二维码 更多>>
评论21
1楼:叶子飞 发表于 2014-01-19 20:44 回复此评论
2楼:xiaoye 发表于 2014-01-19 22:55 回复此评论
我可以理解成返回值被error接口包装了吗?
3楼:陈一回 发表于 2014-01-20 04:52 回复此评论
引用来自“xiaoye”的评论
我可以理解成返回值被error接口包装了吗?
你是说后面的例子么?是这样的
4楼:王振威(Android) 发表于 2014-01-21 09:09 回复此评论
看起来好晕
5楼:CLLam 发表于 2014-01-21 12:50 回复此评论
的确比较复杂。
6楼:无闻 发表于 2014-01-21 13:12 回复此评论
已转载:http://blog.go-china.org/21-interface-nil
7楼:陈一回 发表于 2014-01-21 13:24 回复此评论
引用来自“无闻”的评论
已转载:http://blog.go-china.org/21-interface-nil
额,欢迎转载。我要是更新了要如何同步?
8楼:Mr-Cheung 发表于 2014-01-21 13:39 回复此评论
看着好高大上的样子!
9楼:无闻 发表于 2014-01-21 14:55 回复此评论
引用来自“陈一回”的评论
引用来自“无闻”的评论
已转载:http://blog.go-china.org/21-interface-nil
额,欢迎转载。我要是更新了要如何同步?
提交一个 PR :https://github.com/Unknwon/gcblog/blob/master/content/21-interface-nil.md
更多阅读
DNF全新系统风暴竞技场详解及奖励介绍 风暴部落 竞技场
DNF游戏最近推出的活动确实挺多的,先是说要推出守护者祭坛,现在又推出了一个风暴竞技场系统。大混战模式,最后生存下来的人会受到万人敬仰哦,其中的奖励自然也是非常强力的。DNF全新系统风暴竞技场详解及奖励介绍——步骤/方法DNF
三国杀ol叶诗文武将详解 三国杀ol武将大全
三国杀ol叶诗文武将详解——简介《三国杀》是一款热门的桌面游戏,融合了西方类似游戏的特点,并结合中国三国时期背景,以身份为线索,以卡牌为形式,集合历史、文学、美术等元素于一身。游戏中共有4种身份:主公、反贼、忠臣、内奸。主公和忠
12593和17951的区别图文详解 git使用教程图文详解
12593和17951的区别(图文详解)——简介在使用手机通信时经常会听到12593和17951,可能大家在使用的时候都知道在电话号码前加上17951或12593在通话时可以享受通话优惠,但是可能很大一部分人都不知道什么时候该使用17951,什么时候该使用125
csol中加特林详解 csol格林炮和加特林
上面为仓库属性图csol中加特林详解——步骤/方法csol中加特林详解 1、散弹类武器数据表伤害,商城里显示的是百分之48,实际上是6个弹丸,每个弹丸17的伤害,近身。装甲修正值在0.44左右,距离修正因为霰弹的特殊性未加以评测,但是通过大量实战
海伦英语48个音标海伦英语20个元音音标发音详解海伦英语28个辅音 音标26个元音和辅音
海伦英语48个音标海伦英语20个元音音标发音详解海伦英语28个辅音音标详解海论-傻瓜国际音标教学A海论-傻瓜国际音标教学B