简体   繁体   中英

How to modify field of a struct in a slice?

I have a JSON file named test.json which contains:

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

Now I have written a golang script which reads the JSON file to an slice of structs, and then upon a condition check, modifies a struct fields by iterating over the slice.

Here is what I've tried so far:

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)
}

The output I'm getting is:

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

However I should be getting an output like:

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

My understanding was that since Change() has a pointer passed, it should directly modify the field. Can anyone point me out what I'm doing wrong?

The problem

Let's cite what the language specification 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.

So, in

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

the whole statement forms a scope in which a variable named item is declared and it gets assigned a value of each element of dbUpdate , in turn, form the first to the last — as the statement performs its iterations.

All assignments in Go, always and everywhere do copy the value of the expression being assigned, into a variable receiving that value.

So, when you have

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

var dbUpdate []subDB

you have a slice whose backing array contains a set of elements, each of which has type subDB .
Consequently, when for ... range iterates over your slice, on each iteration a shallow copy of the fields of a subDB value contained in the current slice element is done: the values of those fields are copied into the variable item .

We could re-write what happes as this:

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

  item = dbUpdate[i]

  ...
}

As you can see, if you mutate item in the loop's body, the changes you do to it do not in any way affect the collection's element currently being iterated over.

The solutions

Broadly speaking, the solution is to become fully acquainted with the fact that Go is very simple in most of the stuff it implements, and so range is no magic to: the iteration variable is just a variable, and assignment to it is just an assignment.

As to solving the particular problem, there are multiple ways.

Refer to a collection element by its index

Do

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

A corollary to this is that sometimes, when the element is complex or you'd like to delegate its mutation to some function, you may take a pointer to it:

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

  mutateSubDB(p)
}

...

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

Keep pointers in the slice

If your slice were declated like

var dbUpdates []*subDB

…and you'd keep pointers to (usually heap-allocated) SubDB values, the

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

statement would naturally copy a pointer to a SubDB (anonymous) variable into ptr as the slice contains pointers and so the assignment copies a pointer.

Since all pointers containing the same address are pointing to the same value, mutating the target variable through the pointer kept in the iteration variable would mutate the same thing which is pointed to by the slice's element.

Which approach to select should usually depend on considerations other than thinking about how one would iterate over the elements — simply because once you understand why your code did not work, you do not have this problem anymore.

As usually: if your values are really big, consider keeping pointers to them. If you values need to be referenced from multiple places at the same time, keep pointers to them. In other cases keep the values directly — this greatly improves CPU data cache locality (simply put, by the time you're about to access the next element its contents will most likely have been already fetched from the memory, which does not occur when the CPU has to chase a pointer to access some arbitrary memory location through it).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM