【荐】详解 golang 中的 interface 和 nil
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 的代码如下:
package main import (
"fmt"
"reflect"
) func main() {
var val interface{} = int64(58)
fmt.Println(reflect.TypeOf(val))
val = 50
fmt.Println(reflect.TypeOf(val))
}
我们已经知道接口类型的变量底层是作为两个成员来实现,一个是 type,一个是 data。type 用于存储变量的 动态类型,data 用于存储变量的 具体数据。在上面的例子中,第一条打印语句输出的是:int64。这是因为已经显示的将类型为 int64 的数据 58 赋值给了 interface 类型的变量 val,所以 val 的底层结构应该是:(int64, 58)。我们暂且用这种二元组的方式来描述,二元组的第一个成员为 type,第二个成员为 data。第二条打印语句输出的是:int。这是因为字面量的整数在 golang 中默认的类型是 int,所以这个时候 val 的底层结构就变成了:(int, 50)。借助于 gdb 很容易观察到这点:
$ cd $GOPATH/src/interface_test
$ go build -gcflags "-N -l"
$ gdb interface_test

接下来说说 interface 类型的值 和 nil 的比较问题。这是个比较经典的问题,也算是 golang 的一个坑。
接着来看代码:
package main import (
"fmt"
) func main() {
var val interface{} = nil
if val == nil {
fmt.Println("val is nil")
} else {
fmt.Println("val is not nil")
}
}
变量 val 是 interface 类型,它的底层结构必然是(type, data)。由于 nil 是 untyped (无类型),而又将 nil 赋值给了变量 val,所以 val 实际上存储的是(nil, nil)。因此很容易就知道 val 和 nil的相等比较是为 true 的。
注意:变量 val 的两个存储值同为 nil 时,跟 nil 相等比较才为 true
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
val is nil
对于将任何其它有意义的值类型赋值给 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 这样的变量,是因为使用者只是关心指针的类型,而忽略它存储的值是什么。还是以例子来说明:
package main import (
"fmt"
) func main() {
var val interface{} = (*interface{})(nil)
// val = (*int)(nil)
if val == nil {
fmt.Println("val is nil")
} else {
fmt.Println("val is not nil")
}
}
很显然,无论该指针的值是什么:(*interface{}, nil),这样的接口值总是 非nil 的,即使在该指针的内部为 nil。
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
val is not nil
interface 类型的变量 和 nil 的相等比较出现最多的地方应该是 error 接口类型的值与 nil 的比较。有时候您想自定义一个返回错误的函数来做这个事,可能会写出以下代码:
package main import (
"fmt"
) type data struct{} func (this *data) Error() string { return "" } func test() error {
var p *data = nil
return p
} func main() {
var e error = test()
if e == nil {
fmt.Println("e is nil")
} else {
fmt.Println("e is not nil")
}
}
但是很可惜,以上代码是有问题的。
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
e is not nil
我们可以来分析一下。error 是一个接口类型,test 方法中返回的指针 p 虽然数据是 nil,但是由于它被返回成包装的 error 类型,也即它是有类型的。所以它的底层结构应该是 (*data, nil),很明显它是非 nil 的。
可以打印观察下底层结构数据:
package main import (
"fmt"
"unsafe"
) type data struct{} func (this *data) Error() string { return "" } func test() error {
var p *data = nil
return p
} func main() {
var e error = test() d := (*struct {
itab uintptr
data uintptr
})(unsafe.Pointer(&e)) fmt.Println(d)
}
$ cd $GOPATH/src/interface_test
$ go build
$ ./interface_test
&{3078907912 0}
正确的做法应该是:
package main import (
"fmt"
) type data struct{} func (this *data) Error() string { return "" } func bad() bool {
return true
} func test() error {
var p *data = nil
if bad() {
return p
}
return nil
} func main() {
var e error = test()
if e == nil {
fmt.Println("e is nil")
} else {
fmt.Println("e is not nil")
}
}
参考:
https://my.oschina.net/goal/blog/194233
[荐] interface{} 与 nil,违背了 == 的传递性
【荐】详解 golang 中的 interface 和 nil的更多相关文章
- 详解Objective-C中委托和协议
Objective-C委托和协议本没有任何关系,协议如前所述,就是起到C++中纯虚类的作用,对于“委托”则和协议没有关系,只是我们经常利用协议还实现委托的机制,其实不用协议也完全可以实现委托. AD: ...
- jQuery:详解jQuery中的事件(二)
上一篇讲到jQuery中的事件,深入学习了加载DOM和事件绑定的相关知识,这篇主要深入讨论jQuery事件中的合成事件.事件冒泡和事件移除等内容. 接上篇jQuery:详解jQuery中的事件(一) ...
- 图文详解Unity3D中Material的Tiling和Offset是怎么回事
图文详解Unity3D中Material的Tiling和Offset是怎么回事 Tiling和Offset概述 Tiling表示UV坐标的缩放倍数,Offset表示UV坐标的起始位置. 这样说当然是隔 ...
- 【转】详解C#中的反射
原帖链接点这里:详解C#中的反射 反射(Reflection) 2008年01月02日 星期三 11:21 两个现实中的例子: 1.B超:大家体检的时候大概都做过B超吧,B超可以透过肚皮探测到你内 ...
- 详解Webwork中Action 调用的方法
详解Webwork中Action 调用的方法 从三方面介绍webwork action调用相关知识: 1.Webwork 获取和包装 web 参数 2.这部分框架类关系 3.DefaultAction ...
- 【转】详解JavaScript中的this
ref:http://blog.jobbole.com/39305/ 来源:foocoder 详解JavaScript中的this JavaScript中的this总是让人迷惑,应该是js众所周知的坑 ...
- 深入详解SQL中的Null
深入详解SQL中的Null NULL 在计算机和编程世界中表示的是未知,不确定.虽然中文翻译为 “空”, 但此空(null)非彼空(empty). Null表示的是一种未知状态,未来状态,比如小明兜里 ...
- java 乱码详解_jsp中pageEncoding、charset=UTF -8"、request.setCharacterEncoding("UTF-8")
http://blog.csdn.net/qinysong/article/details/1179480 java 乱码详解__jsp中pageEncoding.charset=UTF -8&quo ...
- 举例详解Python中的split()函数的使用方法
这篇文章主要介绍了举例详解Python中的split()函数的使用方法,split()函数的使用是Python学习当中的基础知识,通常用于将字符串切片并转换为列表,需要的朋友可以参考下 函数:sp ...
随机推荐
- 彻底理解this指向-----实例分析
this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁,一定要搞清楚这个. 情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是wind ...
- 多行文字溢出[...]的实现(text-overflow: ellipsis)
声明:此文章为转载(点击查看原文),如有侵权24小时内删除.联系QQ:1522025433. 对于单行文字, 很简单.(详见css3产考手册 进入) css: .oneLine { width: 20 ...
- graphql详解
随着系统业务量的增大不同的应用和系统共同使用着许多的服务api,而随着业务的变化和发展,不同的应用对相同资源的不同使用方法最终会导致需要维护的服务api数量呈现爆炸式的增长,比如我试着跑了下我们自己业 ...
- Django整合Keras报错:ValueError: Tensor Tensor("Placeholder:0", shape=(3, 3, 1, 32), dtype=float32) is not an element of this graph.解决方法
本人在写Django RESful API时,碰到一个难题,老出现,整合Keras,报如下错误:很纠结,探索找资料近一个星期,皇天不负有心人,解决了 Internal Server Error: /p ...
- 使用super调用父类的构造方法
package com.bjpowernode.t02inheritance.c09; /* * 使用super调用父类的构造方法 */public class TestSuper02 { publi ...
- jQuery插件实践之轮播练习(二)
所有文章搬运自我的个人主页:sheilasun.me 上一篇中学习了jQuery插件的写法,这篇该着手实现啦.首先明确一下轮播要具备哪些功能: 可以点击"向后"按钮向后翻页 可以点 ...
- C#连接oracle连接字符串
/// <summary> /// Oracle 的数据库连接字符串. /// </summary> private const String connString = @&q ...
- 【LOJ】#2187. 「SHOI2014」三叉神经树
题解 可以发现每次修改的是这个点往上一条连续的链,如果我要把1改成0,需要满足这一段往上的一部分都有两个1 如果我要把0改成1,需要满足这一段往上的部分有两个0 对于每个点记录1的个数,发现我们只会把 ...
- BZOJ1853 [Scoi2010]幸运数字 容斥原理
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ1853 题意概括 求一个区间范围内,近似幸运数字的个数. 定义: 幸运数字:仅由6或者8组成的数字. ...
- 085 HBase的二级索引,以及phoenix的安装(需再做一次)
一:问题由来 1.举例 有A列与B列,分别是年龄与姓名. 如果想通过年龄查询姓名. 正常的检索是通过rowkey进行检索. 根据年龄查询rowkey,然后根据rowkey进行查找姓名. 这样的效率不高 ...
