通过反射在Go中快速检测空值的方法

37

我有一个存储在 interface{} 中的 int/string/bool/等值,并想确定它是否未初始化,也就是它的值为以下之一:

  • 0
  • ""
  • false
  • 或者 nil

我该如何检查这个值?


为了获取更加实时和简单的答案,也可以参考以下内容:如何在Golang中判断任意类型的变量是否为零? - blackgreen
4个回答

77

据我所理解,你想要类似于以下的东西:

func IsZeroOfUnderlyingType(x interface{}) bool {
    return x == reflect.Zero(reflect.TypeOf(x)).Interface()
}
当谈论接口和nil时,人们经常会混淆两个非常不同且无关的事情:
1. nil接口值,即没有底层值的接口值。这是接口类型的零值。 2. 非nil接口值(即它具有底层值),但其底层值是其底层类型的零值。例如,底层值是nil地图、nil指针或0数字等。
我理解你在询问第二种情况。
更新: 由于上面的代码使用==,因此对于不可比较的类型不起作用。我认为改用reflect.DeepEqual()将使其适用于所有类型。
func IsZeroOfUnderlyingType(x interface{}) bool {
    return reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface())
}

6
PS: 应该修改为 x == nil || x == reflect.Zero(reflect.TypeOf(x)).Interface(),否则在输入为 nil 时会引发异常。 - user187676
1
@ErikAigner:嗯,这取决于你具体想要什么。如果你想要能够检测nil接口本身,那么是的,你应该添加它。但在问题中,你说接口内存储了int/string/bool等类型。 - newacct
1
使用 map[string]string 失败。除了强制转换和检查 len 之外,有没有其他方法可以支持映射? - chakrit

23

Go 1.13(2019年第3季度)应该简化了该检测过程:

新的Value.IsZero()方法报告一个值是否为其类型的零值。

它是在src/reflect/value.go中实现的,来自c40bffd提交CL 171337,解决了问题7501

请参见示例(仅当支持Go 1.13时)

var p *string
v := reflect.ValueOf(p)

fmt.Printf("1.13 v.IsZero()='%v' vs. IsZero(v)='%v'\n", v.IsZero(), IsZero(v))

// Ouput:
// 1.13 v.IsZero()='true' vs. IsZero(v)='true'

然而:

v = v.Elem()
fmt.Printf("1.13 v.Elem().IsZero()='%v' vs. IsZero(v.Elem())='%v'\n", v.IsZero(), IsZero(v))

// Panic:
//
// panic: reflect: call of reflect.Value.IsZero on zero Value

问题46320中所解释的:

您描述的问题是由Go中接口类型的性质以及reflect包建立在接口类型上这一事实引起的。

接口类型的值始终描述另一个值。
当您将接口类型的值传递给reflect.ValueOf时,您会得到一个反射对象来描述那个其他值,而不是接口类型的值。

要使用reflect包处理interface值,通常需要使用指针。

这与IsZero()无关,这只是reflect包的工作原理。

package main

import (
  "fmt"
  "reflect" 
)

func main() {
  var c interface{}

  fmt.Println(reflect.ValueOf(&c).Elem().Kind())
  fmt.Println(reflect.ValueOf(&c).Elem().IsZero())
} 

基本上,在调用IsZero()之前,需要先调用IsValid()


游乐场示例无法运行。 - Samir Kape
1
@SamirKape,它是可以工作的,但正如问题46320中所报告的那样,panic消息确实是违反直觉和令人困惑的。 - VonC

4

零值*在类型interface{}中只是nil,而不是0""false

package main

import "fmt"

func main() {
        var v interface{}
        fmt.Println(v == nil, v == 0, v == "", v == false)
}

(还有 http://play.golang.org/p/z1KbX1fOgB)


输出

true false false false

*: [Q]当通过声明或调用make或new来分配内存以存储一个值时,如果没有提供显式初始化,则该内存被赋予默认初始化。这样一个值的每个元素都被设置为其类型的零值:布尔类型为false,整数类型为0,浮点数类型为0.0,字符串类型为"",指针、函数、接口、切片、通道和映射类型为nil。[/Q]


在我的看法中,根据您在原始帖子中的陈述,您的问题不需要使用反射。如果我理解正确的话,interface{} 的零值与 nil 相等。 - zzzz
哦,我没意识到你的意思。我想这更好地阐明了我的问题 http://play.golang.org/p/qFmrij9RYQ - user187676
我有些困惑。这里并不是比较 interface{} 的零值,而是比较一个 interface{} 类型的参数,该参数携带了其中一个传递的值。除了未指定类型的 nil 之外,所有这些值都不是零值。你所说的“空/未初始化”到底是什么意思?它似乎并不等同于零值,那它又是什么呢? - zzzz
不,那还有其他问题。http://play.golang.org/p/Rr7kjltONj。我能想到的唯一方法是http://play.golang.org/p/nKsjDU1OzI。然而,在playground中由于其使用unsafe,这并不起作用。 - Stephen Weinberg
混淆的根源在于这样的值并不是“空/未初始化”的,因为在问题被提出时(最近的编辑之前)就已经问过了。即使在那次编辑之后,“未初始化”这个词的新用法也没有太多意义。每个Go实体只有零值和非零值。你可能是指某个特定目标的语义上的“未初始化”,但没有人知道具体是什么。使用规范定义的术语来提出精确的问题,可以得到更好的答案;-) - zzzz
显示剩余3条评论

0

@newacct的答案无法检测原始零值,调用reflect.Value.Interface()会导致错误。它可以使用reflect.Value.IsValid()进行检查。

// IsValid reports whether v represents a value.
// It returns false if v is the zero Value.
// If IsValid returns false, all other methods except String panic.
// Most functions and methods never return an invalid value.
// If one does, its documentation states the conditions explicitly.
func (v Value) IsValid() bool 

更新方法:

func IsZero(v reflect.Value) bool {
    return !v.IsValid() || reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}

func TestIsZero(t *testing.T) {
    var p *string
    v := reflect.ValueOf(p)

    assert.Equal(t, true, v.IsValid())
    assert.True(t, IsZero(v))

    assert.Equal(t, uintptr(0), v.Pointer())

    v = v.Elem()
    assert.Equal(t, false, v.IsValid())
    assert.True(t, IsZero(v))
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接