文档章节

golang: 详解interface和nil

陈亦
 陈亦
发布于 2014/01/19 20:34
字数 1742
阅读 20932
收藏 88

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的。

$ 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")
	}
}


© 著作权归作者所有

共有 人打赏支持
陈亦
粉丝 237
博文 23
码字总数 53194
作品 0
浦东
高级程序员
私信 提问
加载中

评论(29)

海强
海强
最后一段代码
if bad() {
    return p
  }
应该改为
if !bad() {
    return p
  }
吧?
新牛哥
为什么我试了一下 博主的最后的正确方法,返回的还是 e is not nil
jemygraw
jemygraw
好文。
BrainWu
BrainWu
var err error = (*MyError)(nil)
fmt.Println(reflect.TypeOf(err))
fmt.Println(reflect.ValueOf(err))
我发现value并不是nil输出结果为
*main.MyError
<*main.MyError Value>
BrainWu
BrainWu
楼主 为什么 interface (*type, nil) 但是依然可以调用type类型的方法呢?比如如下例子
var err error = (*MyError)(nil) //err 是一个接口类型变量 存储的值为 (*MyError, nil)
fmt.Println(err.Error())//既然value为nil 为何还可以调用 Error呢 这里不太明白希望可以解答
并且为什么 typeof 和 valueof都有值呢
chapin
chapin
楼主V5
Kyli
Kyli
还不错,这个概念挺简单的,一个语言本身没什么牛不牛的,进步,让人喜欢就好
go-skyblue
go-skyblue

引用来自“陈一回”的评论

引用来自“go-skyblue”的评论

http://play.golang.org/p/ICQlEZsUHr 这个例子,b竟然是非nil。博主是否可以解释下。

因为bytes.Buffer不是接口,而是普通类型。即使v是*bytes.Buffer,它也不会像interface一样存储为(type, data),所以v是nil。而b是io.Writer接口,在Set方法内将v赋值给b,则相当于(*bytes.Buffer, nil),虽然数据是nil,但因为存储的类型为指针类型,无论该指针指向的值是什么,它总是非nil的。如果要使b为nil,则需满足b存储:(nil, nil)。

多谢博主。
陈亦
陈亦

引用来自“go-skyblue”的评论

http://play.golang.org/p/ICQlEZsUHr 这个例子,b竟然是非nil。博主是否可以解释下。

因为bytes.Buffer不是接口,而是普通类型。即使v是*bytes.Buffer,它也不会像interface一样存储为(type, data),所以v是nil。而b是io.Writer接口,在Set方法内将v赋值给b,则相当于(*bytes.Buffer, nil),虽然数据是nil,但因为存储的类型为指针类型,无论该指针指向的值是什么,它总是非nil的。如果要使b为nil,则需满足b存储:(nil, nil)。
go-skyblue
go-skyblue
http://play.golang.org/p/ICQlEZsUHr 这个例子,b竟然是非nil。博主是否可以解释下。
golang: 类型转换和类型断言

类型转换在程序设计中都是不可避免的问题。当然有一些语言将这个过程给模糊了,大多数时候开发者并不需要去关注这方面的问题。但是golang中的类型匹配是很严格的,不同的类型之间通常需要手动...

陈亦
2014/01/20
0
8
Golang资料集

该资源的github地址:Qix 《Platform-native GUI library for Go》 介绍:跨平台的golang GUI库,支持Windows(xp以上),Unix,Mac OS X(Mac OS X 10.7以上) 《Gopm 快速入门》 介绍:Gopm(Go 包管...

ty4z2008
2016/03/11
0
0
不得不知道的golang知识点之nil

golang中的,很多人都误以为与Java、PHP等编程语言中的null一样。但是实际上Golang的niu复杂得多了,如果不信,那我们继续往下阅读。 为预声明的标示符,定义在, nil的零值 按照Go语言规范,...

梦朝思夕
2017/10/30
0
0
go 开发中需要注意的与python的不同点

从python转golang开发已经3个月了,因为写过c++,所以对golang接受的还算快,这段经历也不是很痛苦。伯乐在线上看了一些大神关于python转golang过程中的不适应和吐槽,决定写下篇博客。接下来...

熔遁丶螺旋手里剑
07/29
0
0
Go圣经-学习笔记之函数和错误处理

上一篇 Go圣经-学习笔记之复合类型(三) 下一篇 Go圣经-学习笔记之函数值(二) 函数声明 下面给出函数的四种声明方法: 存在必有其存在的价值,那后两者存在的应用场景在哪里呢?我表示持续懵逼...

cdh0805010
2017/10/24
0
0

没有更多内容

加载失败,请刷新页面

加载更多

stylus

stylus基础教程,stylus实例教程,stylus语法总结

miaojiangmin
7分钟前
0
0
PHP生成CSV之内部换行

当我们使用PHP将采集到的文件内容保存到csv文件时,往往需要将采集内容进行二次过滤处理才能得到需要的内容。比如网页中的换行符,空格符等等。 对于空格等处理起来都比较简单,这里我们单独...

豆花饭烧土豆
今天
2
0
使用 mjml 生成 thymeleaf 邮件框架模板

发邮件算是系统开发的一个基本需求了,不过搞邮件模板实在是件恶心事,估计搞过的同仁都有体会。 得支持多种客户端 支持响应式 疼彻心扉的 outlook 多数客户端只支持 inline 形式的 css 布局...

郁也风
今天
8
0
让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字

让哲学照亮我们的人生——读《医务工作者需要学点哲学》有感2600字: 作者:孙冬梅;以前读韩国前总统朴槿惠的著作《绝望锻炼了我》时,里面有一句话令我印象深刻,她说“在我最困难的时期,...

原创小博客
今天
5
0
JAVA-四元数类

public class Quaternion { private final double x0, x1, x2, x3; // 四元数构造函数 public Quaternion(double x0, double x1, double x2, double x3) { this.x0 = ......

Pulsar-V
今天
20
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部