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

Inf*_*ake 0 iteration struct for-loop variable-assignment go

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

[
    {
        "name" : "john",
        "interests" : ["hockey", "jockey"]
    },
    {
        "name" : "lima",
        "interests" : ["eating", "poker"]
    }
]
Run Code Online (Sandbox Code Playgroud)

现在我已经编写了一个 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)
}
Run Code Online (Sandbox Code Playgroud)

我得到的输出是:

[{Name:john Interests:[hockey jockey]} {Name:lima Interests:[eating poker]}]
Run Code Online (Sandbox Code Playgroud)

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

[{Name:john Interests:[hockey jockey]} {Name:lima Interests:[jogging]}]
Run Code Online (Sandbox Code Playgroud)

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

kos*_*tix 7

问题

让我们引用语言规范for ... range循环中所说的内容

带有“range”子句的“for”语句遍历数组、切片、字符串或映射的所有条目,或通道上接收到的值。对于每个条目,它会将迭代值分配给相应的迭代变量(如果存在),然后执行该块。

所以,在

for _, item := range dbUpdate { ... }
Run Code Online (Sandbox Code Playgroud)

整个语句形成一个作用域,在该作用域中声明了一个名为变量的变量,item为 的每个元素分配了一个值dbUpdate,依次从第一个到最后一个——随着语句执行它的迭代。

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

所以,当你有

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

var dbUpdate []subDB
Run Code Online (Sandbox Code Playgroud)

你有一个切片,它的后备数组包含一组元素,每个元素都有 type subDB
因此,当for ... range迭代您的切片时,在每次迭代subDB中,都会完成当前切片元素中包含的值的字段的浅拷贝:这些字段的值被复制到变量中item

我们可以把发生的事情改写成这样:

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

  item = dbUpdate[i]

  ...
}
Run Code Online (Sandbox Code Playgroud)

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

解决方案

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

对于具体问题的解决,有多种方式。

通过索引引用集合元素

for i := range dbUpdate {
  dbUpdate[i].FieldName = value
}
Run Code Online (Sandbox Code Playgroud)

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

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

  mutateSubDB(p)
}

...

func mutateSubDB(p *subDB) {
  p.SomeField = someValue
}
Run Code Online (Sandbox Code Playgroud)

将指针保留在切片中

如果你的切片被声明为

var dbUpdates []*subDB
Run Code Online (Sandbox Code Playgroud)

…并且你会保留指向(通常是堆分配的)SubDB值的指针,

for _, ptr := range dbUpdate { ... }
Run Code Online (Sandbox Code Playgroud)

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

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

选择哪种方法通常取决于考虑因素,而不是考虑如何迭代元素——因为一旦你理解了为什么你的代码不起作用,你就不再有这个问题了。

通常:如果您的值非常大,请考虑保留指向它们的指针。如果需要同时从多个位置引用值,请保留指向它们的指针。在其他情况下,直接保留这些值——这极大地提高了 CPU 数据缓存的局部性(简单地说,当您将要访问下一个元素时,它的内容很可能已经从内存中获取,当CPU 必须追逐一个指针以通过它访问某个任意内存位置)。