专栏首页我的小碗汤go语言nil和interface详解

go语言nil和interface详解

golang的nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。nil是预先说明的标识符,也即通常意义上的关键字。在golang中,nil只能赋值给指针、channel、func、interface、map或slice类型的变量。如果未遵循这个规则,则会引发panic。对此官方有明确的说明:https://godoc.golangtc.com/pkg/builtin/#Type

golang中的interface类似于java的interface、PHP的interface或C++的纯虚基类。接口就是一个协议,规定了一组成员。这个没什么好说的,本文不打算对宏观上的接口概念和基于接口的范式编程做剖析。golang语言的接口有其独到之处:只要类型T的公开方法完全满足接口I的要求,就可以把类型T的对象用在需要接口I的地方。这种做法的学名叫做Structural Typing,

https://en.wikipedia.org/wiki/Structural_type_system

有人也把它看作是一种静态的Duck Typing。所谓类型T的公开方法完全满足接口I的要求,也即是类型T实现了接口I所规定的一组成员。

在底层,interface作为两个成员来实现,一个类型和一个值。对此官方也有文档说明,这里有一篇柴大的翻译:Go中error类型的nil值和nil:

https://my.oschina.net/chai2010/blog/117923

接下来通过编写测试代码和gdb来看看interface倒底是什么。会用到反射,如果您不太了解golang的反射是什么,这里有刑星翻译自官方博客的一篇文章:反射的规则:

https://mikespook.com/2011/09/%E5%8F%8D%E5%B0%84%E7%9A%84%E8%A7%84%E5%88%99/

原文在:laws-of-reflection:

http://blog.golang.org/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
&{5268064 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")
  }
}

END

◆ ◆

原文链接:

https://blog.csdn.net/abv123456789/article/details/24174521

我是小碗汤,我们一起学习。

本文分享自微信公众号 - 我的小碗汤(mysmallsoup),作者:孙飞

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-05-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Go语言 nil 的作用

    最近在油管上面看了一个视频:Understanding nil,挺有意思,这篇文章就对视频做一个归纳总结,代码示例都是来自于视频。

    我的小碗汤
  • 用go语言爬取珍爱网 | 第一回

    在返回体里可以找到<meta charset="gbk" /> 即编码为gbk,而go默认编码为utf-8,所以就会出现乱码。接下来用第三方库将其编码格式转为u...

    我的小碗汤
  • 使用pprof优化golang性能

    Donald E.Knuth说过一句非常著名的话,过早的优化是万恶之源。原文如下:

    我的小碗汤
  • 仅有两名前端开发,联机小游戏一周内上线,如何做到?

    它在上线四小时内用户数激增60倍,获得新华社力荐,开发过程中仅投入2个前端开发+1个美术+1个策划,这款小游戏里,单机玩法、邀请好友对战、在线匹配对战、排行榜、...

    腾讯云开发TCB
  • python模块--subprocess

    subprocess模块                                                                    ...

    py3study
  • [每周看]代码优化技巧·代码编写好习惯·代码规范

    明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:

    陈黎栋
  • 网络工程师技能图谱,看看你会多少技能

    技术推动了时代变革,互联网则加速了这场变革。在蓬勃发展的互联网浪潮下,网络作为基础设施的关键纽带保障着网路流量的顺利流通,维持着赛博世界的繁荣。而在这繁荣之下,...

    SDNLAB
  • 有哪些工具可以让嵌入式开发事半功倍?详细盘点工程师必备工具

    嵌入式开发就是指在嵌入式操作系统下进行开发,一般常用的系统有μcos,vxworks,linux,android等。当然,对于嵌入式或单片机工程师的利器就是C语...

    Linux阅码场
  • Jaeger和OpenTelemetry

    最近,OpenTelemetry宣布成为CNCF新的沙箱项目,由OpenTracing和OpenCensus[1]、[2]、[3]、[4]合并而成。已经有几个人...

    CNCF
  • Genymotion模拟器常见问题整理与相应解决方法

    为什么说是常见问题整合呢,因为小编我就是Genymotion模板器最悲剧的使用者,该见过的问题,我基本都见过了,在此总结出这血的教训,望大家不要重蹈覆辙。

    砸漏

扫码关注云+社区

领取腾讯云代金券