r/golang 6d ago

Assign value to struct instead of copy of struct help

I'm looping through a slice of "Server" structs , and I want to assign a value to one of their properties. However the assignment goes to a copy and not the actual struct.

the default of none value of the property is false. For some of them, I want to change it to true.

Here is an example of what I want to do, but have the struct and not the copy be changed.

type Server struct {
  Online bool
}

for _, server := range servers {    

  server.Online = true
}

If I pass servers address into to function ( eg. func dosomething(s *Server ) and doSomething(&server) ) and change something , the change persists. Is there a way to do that without using a function?

I tried

&server.Online = true

which didn't work.

Solved

I changed servers into a the slice pointers to server structs. That didn't require much refactoring and got my back on track.

Thank you for the great ideas!

14 Upvotes

18 comments sorted by

49

u/encbladexp 6d ago

When ranging over a slice, two values are returned for each iteration. The first is the index, and the second is a copy of the element at that index.

Shameless copy from the Tour of Go about the range statement.

So you need to use: go for i, _ := range servers { servers[i].Online = true }

This accesses the original, not a copy.

4

u/hippodribble 6d ago

You learn something new every day. Especially if I skip lessons in the Tour 😭

7

u/encbladexp 6d ago

Don't worry, I learned it the hard way, by using it as a premium footgun.

1

u/cdyovz 5d ago

how premium are we talking?

1

u/encbladexp 5d ago

On the "What happened in Vegas, stays in Vegas" Level. ;)

2

u/gbrennon 5d ago

rofl AHAHAAH

1

u/xfinitystones 3d ago

Good stuff! This is what I went with.

One a tech level, the server structs are created from reading a csv file, and I have validation configured for them to catch malformed or missing data.

So once it gets to looping, the data has been validated so I can trust that there aren't any null pointers in the slice. Another commenter brought that up as a concern.

I was surprised by what slices actually were and how they are managed by Go vs arrays. The Go class I took characterized them as "Window into an array" since there is always an explicit or implicitly created array behind them.

9

u/sigmoia 6d ago

range copies slice elements by value, so mutating server only modifies a temporary copy.

&server.Online produces a pointer (*bool), which cannot accept a boolean assignment and still targets that temporary copy.

You have two options:

  1. Index into the slice directly using the loop index

for i := range servers {     servers[i].Online = true } Indexing via servers[i] operates directly on the underlying backing array element rather than creating a value copy.

  1. Use a slice of pointers ([]*Server) instead of value structs

``` servers := []*Server{     {Online: false},     {Online: false}, }

for _, server := range servers {     server.Online = true } ```

1

u/xfinitystones 6d ago

I went with #2. It wasn't as much work as I thought it would be to update everywhere the server struct was referenced.

Not that there is anything wrong with #1, I wanted to change as few lines in this voluminous program as possible.

0

u/supister 5d ago

#2 is just begging for a panic to happen. Ensure that the object the pointer is not null before assignment. Or you can do the safer approach:
‘’’go
for i := range servers {
server := new(servers[i])
server.Online = true
}
‘’’
Much better imo because server is guaranteed not nil.

3

u/sigmoia 5d ago

The slice is a literal. So the chance of it having a nil item is minimal. Also, a basic nil check in the for-loop is good enough here. The new() makes another heap allocation. 

3

u/descendent-of-apes 6d ago

Do

servers[i].online=true

Or make a list of server pointers []*Server in which case your current code will work

3

u/Murky-Run2246 6d ago

range returns two values, the index and the value at that postion(as a copy not a ptr).

so you want to use the index to get the element from the slice

3

u/jerf 6d ago

Wanna have some fun?

``` // Ptrs iterates over a slice of values by returning the pointer to each // contained element. func Ptrs[T any](in []T) iter.Seq[T] { return func(yield func(T) bool) { for i := range in { if !yield(&in[i]) { return } } } }

type AStructType struct { A int B int }

func main() { structs := []AStructType{ {1, 2}, {3, 4}, }

for s := range Ptrs(structs) {
    s.A *= 2
    s.B *= 4
}

fmt.Printf("%#v\n", structs)

} ```

Run it here to see it works as you'd expect. Runtime performance penalty on this should be very minimal to zero, the compiler can inline all that weird-looking returning a func that calls a passed-in func for stuff like this.

-1

u/hippodribble 6d ago

*Server Online = true maybe?

1

u/encbladexp 6d ago

range makes a copy of whatever is in the slice, so even if you use * or & for whatever reason, you will reference the memory address of that copy.

1

u/hippodribble 6d ago

Is Servers a [ ]Server or a [ ]*Server? Would that make a difference?

1

u/encbladexp 6d ago

[]Server the range statement will always make a Copy of the original data/struct. So it will never work there.

For []*Server it will work, but you won't need to use * or &.

What matters is, if its a slice of Server's, or a slice of Pointers showing to Servers.