如何修改切片中结构的字段?

How to modify field of a struct in a slice?

我有一个名为 test.json 的 JSON 文件,其中包含:

[
    {
        "name" : "john",
        "interests" : ["hockey", "jockey"]
    },
    {
        "name" : "lima",
        "interests" : ["eating", "poker"]
    }
]

现在我已经编写了一个 golang 脚本,它将 JSON 文件读取到结构切片,然后在条件检查时,通过遍历切片来修改结构字段。

这是我到目前为止尝试过的方法:

package main

import (
    "log"
    "strings"
    "io/ioutil"
    "encoding/json"
)

type subDB struct {
    Name       string   `json:"name"`
    Interests  []string `json:"interests"`
}

var dbUpdate []subDB

func getJSON() {
    // open the file
    filename := "test.json"
    val, err := ioutil.ReadFile(filename)
    if err != nil {
        log.Fatal(err)
    }
    err = json.Unmarshal(val, &dbUpdate)
}

func (v *subDB) Change(newresponse []string) {
    v.Interests = newresponse
}

func updater(name string, newinterest string) {
    // iterating over the slice of structs
    for _, item := range dbUpdate {
        // checking if name supplied matches to the current struct
        if strings.Contains(item.Name, name) {
            flag := false  // declare a flag variable
            // item.Interests is a slice, so we iterate over it
            for _, intr := range item.Interests {
                // check if newinterest is within any one of slice value
                if strings.Contains(intr, newinterest) {
                    flag = true
                    break  // if we find one, we terminate the loop
                }
            }
            // if flag is false, then we change the Interests field
            // of the current struct
            if !flag {
                // Interests holds a slice of strings
                item.Change([]string{newinterest}) // passing a slice of string
            }
        }
    }
}

func main() {
    getJSON()
    updater("lima", "jogging")
    log.Printf("%+v\n", dbUpdate)
}

我得到的输出是:

[{Name:john Interests:[hockey jockey]} {Name:lima Interests:[eating poker]}]

但是我应该得到如下输出:

[{Name:john Interests:[hockey jockey]} {Name:lima Interests:[jogging]}]

我的理解是Change()既然传了一个指针,应该直接修改字段。谁能指出我做错了什么?

问题

让我们引用一下语言规范says on the for ... range loops:

A "for" statement with a "range" clause iterates through all entries of an array, slice, string or map, or values received on a channel. For each entry it assigns iteration values to corresponding iteration variables if present and then executes the block.

所以,在

for _, item := range dbUpdate { ... }

整个语句形成一个作用域,其中声明了一个名为 item 变量 并为其赋值 的值dbUpdate 的每个元素依次从第一个元素到最后一个元素——当语句执行其迭代时。

Go 中的所有赋值,无论何时何地,都会将被赋值的表达式的值复制到接收该值的变量中。

所以,当你有

type subDB struct {
    Name       string   `json:"name"`
    Interests  []string `json:"interests"`
}

var dbUpdate []subDB

您有一个切片,其支持数组包含一组元素,每个元素的类型为 subDB
因此,当 for ... range 遍历您的切片时,每次迭代都会完成当前切片元素中包含的 subDB 值字段的浅表副本:这些字段的值被复制到变量 item.

我们可以re-write发生这样的事情:

for i := 0; i < len(dbUpdate); i++ {
  var item subDB

  item = dbUpdate[i]

  ...
}

如您所见,如果您在循环体中改变 item,您对其所做的更改不会以任何方式影响当前正在迭代的集合元素。

解决方案

从广义上讲,解决方案是充分了解 Go 在其实现的大部分内容中都非常简单的事实,因此 range 并不神奇:迭代变量只是一个变量, 对它的赋值只是一个赋值。

具体问题的解决方法有很多种。

通过索引引用集合元素

for i := range dbUpdate {
  dbUpdate[i].FieldName = value
}

由此得出的一个推论是,有时,当元素很复杂或者您想将其突变委托给某个函数时,您可以使用指向它的指针:

for i := range dbUpdate {
  p := &dbUpdate[i]

  mutateSubDB(p)
}

...

func mutateSubDB(p *subDB) {
  p.SomeField = someValue
}

在切片中保留指针

如果你的切片被声明为

var dbUpdates []*subDB

…你会保留指向(通常heap-allocated)SubDB值的指针,

for _, ptr := range dbUpdate { ... }

语句自然会将指向 SubDB(匿名)变量的指针复制到 ptr,因为切片包含指针,因此赋值会复制一个指针。

由于所有包含相同地址的指针都指向相同的值,通过保存在迭代变量中的指针改变目标变量将改变切片元素指向的相同事物。

select 的哪种方法通常应该取决于 除了 考虑如何迭代元素之外的其他考虑因素 — 仅仅是因为一旦你理解了为什么你的代码没有工作,你没有这个问题了。

像往常一样:如果您的值真的很大,请考虑保留指向它们的指针。 如果您的值需要同时从多个位置引用,请保留指向它们的指针。在其他情况下,直接保留值——这极大地改善了 CPU 数据 cache locality(简单地说,当你要访问下一个元素时,它的内容很可能已经从内存中获取了,当 CPU 必须追逐一个指针以通过它访问某个任意内存位置时不会发生这种情况。