【问题标题】:Golang assign value to deep nested structureGolang 为深层嵌套结构赋值
【发布时间】:2017-05-11 08:05:37
【问题描述】:

我正在学习 Go 并且到目前为止非常喜欢它。来自 JS 背景,我仍在探索某些模式和最佳实践。

在 Go 中使用对象路径获取和分配值给深度嵌套对象的最佳方法是什么?例如,在 JS 中可以这样做...

var children = [{children:[{children:[{a:1}]}]}]
var child = "0.children.0.children.0".split('.').reduce((c, p) => c[p], children)
child.a = 2
console.log(children[0].children[0].children[0].a)

【问题讨论】:

  • 使用嵌套结构,您只需要做A.B.C ...。使用地图、数组或切片,您只需执行a[0]["x"] ...。没有其他惯用的快捷方式。
  • Python 的列表推导提供了一种不同的方式来执行这样的函数操作。 This answer addressing another question involving list comprehensions in Go 值得一读,因为它解释了这是可能的,但不值得。

标签: javascript go dynamic struct slice


【解决方案1】:

如果你需要一个通用的解决方案,你可以使用包reflect,但如果可能的话最好避免它(例如,如果你在编译时知道类型和“路径”,只需使用字段selectorsindex expressions)。

这是一个演示。设置由string 元素指定的“深度”值的辅助函数可能如下所示:

func set(d interface{}, value interface{}, path ...string) {
    v := reflect.ValueOf(d)
    for _, s := range path {
        v = index(v, s)
    }
    v.Set(reflect.ValueOf(value))
}

上面使用的index() 函数可能如下所示:

func index(v reflect.Value, idx string) reflect.Value {
    if i, err := strconv.Atoi(idx); err == nil {
        return v.Index(i)
    }
    return v.FieldByName(idx)
}

我们可以这样测试它:

type Foo struct {
    Children []Foo
    A        int
}

func main() {
    x := []Foo{
        {
            Children: []Foo{
                {
                    Children: []Foo{
                        {
                            A: 1,
                        },
                    },
                },
            },
        },
    }
    fmt.Printf("%+v\n", x)
    path := "0.Children.0.Children.0.A"
    set(x, 2, strings.Split(path, ".")...)
    fmt.Printf("%+v\n", x)
}

输出(在Go Playground上试试):

[{Children:[{Children:[{Children:[] A:1}] A:0}] A:0}]
[{Children:[{Children:[{Children:[] A:2}] A:0}] A:0}]

从输出中可以看出,string 路径"0.Children.0.Children.0.A" 表示的“深”字段A 从最初的1 更改为2

注意结构体的字段(Foo.AFoo.Children在这种情况下)必须导出(必须以大写字母开头),否则其他包将无法访问这些字段,并且它们的值无法更改使用包reflect


无需反射,事先知道类型和“路径”,可以这样做(继续前面的示例):

f := &x[0].Children[0].Children[0]
fmt.Printf("%+v\n", f)
f.A = 3
fmt.Printf("%+v\n", f)

输出(在Go Playground上试试):

&{Children:[] A:2}
&{Children:[] A:3}

这个的一般解决方案(没有反射):

func getFoo(x []Foo, path ...string) (f *Foo) {
    for _, s := range path {
        if i, err := strconv.Atoi(s); err != nil {
            panic(err)
        } else {
            f = &x[i]
            x = f.Children
        }
    }
    return
}

使用它(再次,继续前面的示例):

path = "0.0.0"
f2 := getFoo(x, strings.Split(path, ".")...)
fmt.Printf("%+v\n", f2)
f2.A = 4
fmt.Printf("%+v\n", f2)

输出(在Go Playground 上试试):

&{Children:[] A:3}
&{Children:[] A:4}

但是请注意,如果我们只处理int 索引,那么将path 声明为...string(即[]string)就没有意义了,int 切片将产生更有意义。

【讨论】:

  • 谢谢。很好的例子!
  • 如果没有反思,我该怎么做。例如,使用只有整数“0.0.0”的路径,然后返回可以变异的struct Foo?
  • 是的,但这需要您知道嵌套的确切长度并且不灵活。如果结构一致但嵌套长度不同,我如何访问嵌套对象来更新树? JS 中类似的东西是使用不可变的数据结构。
  • 太棒了。感谢您的帮助!