在嵌套结构中测试空值的方法

huangapple go评论88阅读模式
英文:

Test for nil values in nested stucts

问题

我在Go语言中有一个深层嵌套的结构体。这些结构体是由JSON解码器构建的。

然而,这个结构体中的许多字段都是"omitifempty"的,所以我最终得到的结构体可能在各个位置上都有空值。

示例(实际情况更深层嵌套,且更大:400行的结构体):

package main

import "fmt"

type Foo struct {
    Foo string
    Bar *Bar
}

type Bar struct {
    Bar string
    Baz *Baz
}

type Baz struct {
    Baz string
}

func main() {
    f1 := Foo{Foo: "f1"}
    f2 := Foo{Foo: "f2", Bar: &Bar{Bar: "br2"}}
    f3 := Foo{Foo: "f3", Bar: &Bar{Bar: "br3", Baz: &Baz{Baz: "bz3"}}}

    fmt.Println(f3.Bar.Baz.Baz) //-> bz3
    fmt.Println(f2.Bar.Baz.Baz) //-> panic: runtime error: invalid memory address or nil pointer dereference
    fmt.Println(f1.Bar.Baz.Baz) //-> panic: runtime error: invalid memory address or nil pointer dereference    
    //so far so good, but

    //is there a more generic way to do this kind of testing?
    if f2.Bar != nil && f2.Bar.Baz != nil {
        fmt.Println(f2.Bar.Baz.Baz)
    } else {
        fmt.Println("something nil")
    }
}

问题是是否有一种更通用的方法来测试引用树中的某个节点是否为nil?我需要获取很多不同的项,编写所有这些if语句将会很麻烦。而且速度也是一个问题。

英文:

I have a deeply nested struct in go. These are constructed by a json unmarshaller.

Quite some fields in this struct are however 'omitifempty' so I end op with a struct that can have nills in various places.

Example (the real thing is even deeper nested, and big: 400 lines of structs):

package main

import "fmt"

type Foo struct {
	Foo string
	Bar *Bar
}

type Bar struct {
	Bar string
	Baz *Baz
}

type Baz struct {
	Baz string
}

func main() {
	f1 := Foo{Foo: "f1"}
	f2 := Foo{Foo: "f2", Bar: &Bar{Bar: "br2"}}
	f3 := Foo{Foo: "f3", Bar: &Bar{Bar: "br3", Baz: &Baz{Baz: "bz3"}}}

	fmt.Println(f3.Bar.Baz.Baz) //-> bz3
	fmt.Println(f2.Bar.Baz.Baz) //-> panic: runtime error: invalid memory address or nil pointer dereference
	fmt.Println(f1.Bar.Baz.Baz) //-> panic: runtime error: invalid memory address or nil pointer dereference	
	//so far so good, but

	//is there a more generic way to do this kind of testing?
	if f2.Bar != nil && f2.Bar.Baz != nil {
		fmt.Println(f2.Bar.Baz.Baz)
	} else {
		fmt.Println("something nil")
	}
}

The question is if there is a more generic way to test if some node in the reference tree is nil? I need to get a lot of different items and it will be a pain to write all these if statements.
Oh and speed is of concern.

答案1

得分: 17

一种优雅的处理方法(在我看来)是为用作指针的结构体添加getter方法。这种“技巧”也被protobuf生成的Go代码使用,它允许自然地链式调用方法,而不必担心由于nil指针而导致运行时恐慌。

在你的示例中,BarBaz结构体被用作指针,所以给它们添加getter方法。重点是添加带有指针接收器的方法,首先必须检查接收器是否为nil。如果是,返回结果类型的零值。如果不是,则继续返回结构体的字段:

func (b *Bar) GetBaz() *Baz {
    if b == nil {
        return nil
    }
    return b.Baz
}

func (b *Baz) GetBaz() string {
    if b == nil {
        return ""
    }
    return b.Baz
}

使用指针接收器的好处是可以使用nil接收器调用它们。直到尝试引用它们的字段时才会导致运行时恐慌,而我们不会这样做,这就是为什么我们首先检查接收器是否为nil(最终,接收器就像普通参数一样,将nil作为指针参数传递从不会出错)。

有了上述的getter方法,使用变得简化了,而且在这些示例中不会发生运行时恐慌:

fmt.Println(f3.Bar.GetBaz().GetBaz()) // 自然地没有恐慌
fmt.Println(f2.Bar.GetBaz().GetBaz()) // 没有恐慌
fmt.Println(f1.Bar.GetBaz().GetBaz()) // 没有恐慌

if baz := f2.Bar.GetBaz(); baz != nil {
    fmt.Println(baz.GetBaz())
} else {
    fmt.Println("something nil")
}

Go Playground上尝试一下。

英文:

One elegant way (in my opinion) of handling it is to add getters to structs that are used as pointers. This "technique" is also used by the generated Go code of protobuf, and it allows natural chaining of method calls without having to worry about runtime panic due to nil pointers.

In your example the Bar and Baz structs are used as pointers, so arm them with getters. The focus is on adding methods with pointer receiver, and first it must be checked if the receiver is nil. If so, return the zero value of the result type. If not, proceed to return the field of the struct:

func (b *Bar) GetBaz() *Baz {
	if b == nil {
		return nil
	}
	return b.Baz
}

func (b *Baz) GetBaz() string {
	if b == nil {
		return ""
	}
	return b.Baz
}

The good thing about methods with pointer receivers is that you may call them with nil receivers. It does not cause a runtime panic until you try to refer to their fields, which we don't, that's why we first check if the receiver is nil (ultimately, receivers act as normal parameters–and it's never an error to pass nil as a pointer argument).

Having the above getters, use is simplified to this, and no runtime panic occurs in any of these examples:

fmt.Println(f3.Bar.GetBaz().GetBaz()) // naturally no panic
fmt.Println(f2.Bar.GetBaz().GetBaz()) // No panic
fmt.Println(f1.Bar.GetBaz().GetBaz()) // No panic

if baz := f2.Bar.GetBaz(); baz != nil {
	fmt.Println(baz.GetBaz())
} else {
	fmt.Println("something nil")
}

Try it on the Go Playground.

答案2

得分: 4

如果你想避免使用反射(reflection)来测试任何结构体的字段(类似于“通过反射获取指向值的指针”或者这个gist中的方法:它会比较慢),最可靠的方法是在Foo上实现方法来返回正确的值。

func (foo *Foo) BarBaz() string {
    if f2.Bar != nil && f2.Bar.Baz != nil {
        return f2.Bar.Baz.Baz
    } else {
        fmt.Println("something nil")
        return "" // 例如
    } 
}

如果有很多这样的函数需要编写,也许可以使用go 1.4 go generate命令来生成大部分函数。

英文:

If you want to avoid 'reflect' (reflection, as a "generic" way to test fields of any struct, a bit as in "Get pointer to value using reflection" or in this gist: it is slower), the surest way would be to implement methods on Foo in order to return the right value

func (foo *Foo) BarBaz() string {
    if f2.Bar != nil && f2.Bar.Baz != nil {
        return f2.Bar.Baz.Baz
    } else {
        fmt.Println("something nil")
        return "" // for example
    } 
}

If there are a lot of such functions to write, maybe go 1.4 go generate command can help generate most of them.

答案3

得分: 0

我已经使用了recover

package navigate

func recoverFrom(err any, panicPred func(e error) bool) {
	if err != nil {
		e, ok := err.(error)
		if !ok || panicPred(e) {
			panic(err)
		}
	}
}

func RecoverFromNilPointerDereference() {
	recoverFrom(
		recover(),
		func(e error) bool {
			// 不幸的是,没有可以匹配的内存访问对象,所以我们被迫通过字符串匹配
			// 如果这个内部字符串发生了变化,这个函数将需要更改以匹配新值
			const internalMemoryAccessErrorMsg = "runtime error: invalid memory address or nil pointer dereference"

			return e.Error() != internalMemoryAccessErrorMsg
		},
	)
}

func Safely[T any](navigate func() T) T {
	defer RecoverFromNilPointerDereference()

	return navigate()
}

用法:

navigate.Safely(func() *string {
	return a.b.c
})
英文:

I've used recover:

package navigate

func recoverFrom(err any, panicPred func(e error) bool) {
	if err != nil {
		e, ok := err.(error)
		if !ok || panicPred(e) {
			panic(err)
		}
	}
}

func RecoverFromNilPointerDereference() {
	recoverFrom(
		recover(),
		func(e error) bool {
			// unfortunately, there's no memory access object that can be matched, so we're forced to match by string
			// if this internal string changes, this function will need to be changed to match the new value
			const internalMemoryAccessErrorMsg = "runtime error: invalid memory address or nil pointer dereference"

			return e.Error() != internalMemoryAccessErrorMsg
		},
	)
}

func Safely[T any](navigate func() T) T {
	defer RecoverFromNilPointerDereference()

	return navigate()
}

Usage:

navigate.Safely(func() *string {
	return a.b.c
})

huangapple
  • 本文由 发表于 2015年1月29日 20:40:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/28215031.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定