-2

The output of the following code surprises me:

package main

import (
    "fmt"
)

type Thing struct {
  mappings  map[string]int
  orderings []string
}

func NewThing() Thing {
  t := Thing{}
  t.mappings = make(map[string]int)
  return t
}

func (t Thing) Add(s string) {
  t.mappings[s] = 1
  t.orderings = append(t.orderings, s)
}

func main() {
  t := NewThing()
  t.Add("foo")

  if len(t.mappings) == len(t.orderings) {
    fmt.Printf("Equal lengths: %v versus %v", t.mappings, t.orderings)
  } else {
    fmt.Printf("Unequal lengths: %v versus %v", t.mappings, t.orderings)
  }
}

When run on the playground (https://play.golang.org/p/Ph67tHOt2Z_I) the output is this:

Unequal lengths: map[foo:1] versus []

I believe I'm treating the slice correctly; from my understanding it is initialized to nil in NewThing(), and is appended to in Add() (ensuring that the value returned from append is only assigned to its first argument).

Am I missing something incredibly obvious?

I looked at the following resources for an explanation:

https://gobyexample.com/slices - only uses either slice literals (i.e. not a struct field) or slices with set capacities, and I will not know the final size of t.orderings. It's my understanding that append should perform the extension and allocation automatically.

https://go.dev/blog/slices-intro - again, all demonstrations use slice literals. If the fields are moved out of the struct things work as expected. It's only once in the struct that this behavior occurs.

https://yourbasic.org/golang/gotcha-append/ - while it does describe behavior where append does not work as expected, the explanation involves append reusing memory when the slice has enough capacity for a new element, causing unexpected behavior when attempts to append the same array to two different copies. In my case, there is no reassignment of slice operations such as the one in this article, which is discouraged (some_var = append(some_other_var, elem)).

And I looked at the following questions for inspiration:

Go - append to slice in struct: the solution to this question was to assign the result of append back to the field, which I have done.

Correct way to initialize empty slice: the explanation is that slices don't have to be initialized, and can be left as nil and "appended to with allocation", so I believe I'm fine not initializing Thing.orderings.

  • 3
    `append(some_other_var, elem)` returns a **new value**. And given your `t Thing` is a temporary copy - that value is lost. If you want your `t Thing` to be mutable you should make it a pointer: `t *Thing`. – zerkms Aug 22 '21 at 23:42
  • I'm not sure what you mean by temporary copy in this case. t is initialized via NewThing(), which returns a Thing. The value of t doesn't change after that, just its fields. When is a copy made? And why wouldn't that affect the map, if it's the value of t that's lost? – wherezyurheadat Aug 22 '21 at 23:46
  • I literally posted that link in my question as saying it didn't. – wherezyurheadat Aug 22 '21 at 23:49
  • 1
    `func (t Thing) Add(s string) {` <-- this signature means "every time you're invoking `Add` method it creates a temporary copy of a `Thing` and assigns it to `t`. After the function returns `t` is lost". It does not matter how you created an instance. It matters what signature of a method is. "And why wouldn't that affect the map" --- because you don't reassign the map but mutate it. – zerkms Aug 22 '21 at 23:51
  • @wherezyurheadat sorry, I missed that, but it should answer the question because it accepts a pointer rather than a copy. Use `func (t *Thing)`. I guess OP in the dupe suggestion is already doing that, so it's not a clear-cut dupe. – ggorlen Aug 22 '21 at 23:54
  • I can take the initialization function out of the equation entirely and the problem persists: https://play.golang.org/p/ugCYIWbpIT3 If I change the signature of to func (t *Thing) it works but i'm not initializing a pointer and don't want to. And again, the map is unaffected by any of this. – wherezyurheadat Aug 23 '21 at 00:01
  • 3
    @wherezyurheadat because, as was already stated, it does matter how you initialise things, it's the `Add` method's declaration matters. – zerkms Aug 23 '21 at 00:01
  • 4
    @wherezyurheadat "and don't want to" --- then you won't be able to mutate the struct's fields. "And again, the map is unaffected by any of this" --- because you don't reassign the map. If you reassign the map - you will see the same "problem". – zerkms Aug 23 '21 at 00:02
  • 4
    Map changes because you mutate the map, you don't reassign it. – zerkms Aug 23 '21 at 00:03
  • 1
    Another way to look at this: maps are *inherently* pointers; slices (slice headers) *contain* pointers but are *not themselves* pointers. This means that you'll always mutate the map in place, so it's OK to copy the map pointer, but you'll sometimes mutate the slice in place and sometimes replace the slice header, so it's not OK to copy the slice header. – torek Aug 23 '21 at 00:53
  • The magic words to understand this was "pointer receivers". Before I asked the question I was under the impression that a function that took a pointer could not take an object. If someone had said that right away... – wherezyurheadat Aug 23 '21 at 16:03

1 Answers1

-1

Incase you don't want to use a pointer ,you can declare a global variable for Thing struct and assign it with the value of t from add function.Here is the code for the same logic :

package main

import (
    "fmt"
)

var thing Thing

type Thing struct {
    mappings  map[string]int
    orderings []string
}

func NewThing() Thing {
    t := Thing{}
    t.mappings = make(map[string]int)
    return t
}

func (t Thing) Add(s string) {
    t.mappings[s] = 1
    t.orderings = append(t.orderings, s)

    thing = t

}

func main() {
    t := NewThing()
    t.Add("foo")

    if len(thing.mappings) == len(thing.orderings) {
        fmt.Printf("Equal lengths: %v versus %v", thing.mappings, thing.orderings)
    } else {
        fmt.Printf("Unequal lengths: %v versus %v", thing.mappings, thing.orderings)
    }
}

Output:

Equal lengths: map[foo:1] versus [foo]
Gopher
  • 721
  • 3
  • 8