August 10th, 2024

Go structs are copied on assignment (and other things about Go I'd missed)

The author reflects on learning Go, highlighting misconceptions about struct assignment, sub-slice modifications, and method receivers. They recommend the "100 Go Mistakes" resource and other learning materials for improvement.

Read original articleLink Icon
ConfusionInspirationFrustration
Go structs are copied on assignment (and other things about Go I'd missed)

The author reflects on their experiences with the Go programming language, highlighting some fundamental concepts they initially misunderstood. After encountering a bug related to struct assignment, they discovered that structs in Go are copied on assignment, which led to confusion when trying to modify a struct returned from a function. This realization prompted them to explore the "100 Go Mistakes and How To Avoid Them" resource, which helped clarify several misconceptions. They learned that appending to a sub-slice can unintentionally modify the original slice due to shared backing arrays, and they gained insights into the differences between value and pointer receivers in method declarations. The author appreciates the structured format of the "100 Common Mistakes" series, which allows for quick identification of useful information. They also mention additional resources for learning Go, including documentation and linters, and express a desire to continue improving their understanding of the language.

- The author discovered that Go structs are copied on assignment, leading to unexpected behavior in their code.

- Appending to a sub-slice can modify the original slice due to shared backing arrays.

- Understanding the difference between value and pointer receivers is crucial for method declarations in Go.

- The "100 Go Mistakes" resource helped clarify several misconceptions about the language.

- The author recommends various resources for learning Go, including documentation and linters.

AI: What people are saying
The comments reflect a range of opinions and insights regarding misconceptions in Go programming, particularly around struct assignment and value vs. reference types.
  • Many commenters emphasize the confusion surrounding value and reference semantics in Go, drawing parallels with other languages like C# and PHP.
  • Several users recommend resources for learning Go, including books and online materials, highlighting the importance of understanding the language's design.
  • There is a shared sentiment that misconceptions often stem from experiences with more dynamic or implicit languages like Java and Python.
  • Some comments critique Go's design choices, suggesting that they could lead to misunderstandings and that the language could benefit from learning from others like Rust.
  • Users express a desire for clearer semantics and better educational resources to help new programmers navigate Go's complexities.
Link Icon 25 comments
By @Animats - 3 months
The semantics of when stuff is copied, moved, or passed by reference are all over the place in language design.

C started with the idea that functions returned one int-sized value in a register. This led to classic bugs where the function returns a pointer to a local value. Compilers now usually catch this. C eventually got structure return by copy. Then C++ added return value by move, and automatic optimization for that. It's complicated.[1]

Most hard-compiled languages only let you return values of fixed length, because the caller has to allocate space. Dynamic languages where most things are boxed just return the box. Rust makes you declare boxed types explicitly. Vec and String are already boxed, which handles the common cases.

More dynamic languages tend to let you return anything, although there can be questions over whether you have your own mutable copy, a copy-on-write copy, a read-only copy, or a mutable reference to the original. That's what got the OP here, at

    thing := findThing(things, "record")
    thing.Name = "gramaphone"
They thought they had a mutable reference to the original, but they had a mutable copy.

There's a good argument for immutability by default, but many programmers dislike all the extra declarations required.

[1] https://stackoverflow.com/questions/17473753/c11-return-valu...

By @simonw - 3 months
One of the many things I find inspiring about Julia is how quick she is to admit to mistakes she has made or things that she hasn't understood.

If she didn't understand it, I can 100% guarantee that there are large numbers of people out there who also didn't understand it - many of whom were probably too embarrassed to ever admit it.

I think this is a useful trait for senior software engineers generally. If you're a senior engineer you should have earned enough of a reputation that the risk involved in admitting "I didn't know that" can be offset by everything you provably DO know already. As such, you can help everyone else out by admitting to those gaps in your knowledge and helping emphasize that nobody knows everything and it's OK to take pride in filling those knowledge gaps when you come across them.

By @MathMonkeyMan - 3 months
Donovan and Kernighan's "The Go Programming Language" is one of the best pieces of technical writing I've ever read. Buy it and read it cover to cover.

Then read the [Go Language Specification][1] cover to cover. It's dry but refreshingly not legalese.

[1]: https://go.dev/ref/spec

By @metadat - 3 months
Not understanding structs vs pointers is a pretty basic misconception in go.

Does this trip anyone else up? I found it unenlightening / unsurprising, and the linked "100 mistakes" piece also very basic and in some cases just plain wrong.

By @tialaramex - 3 months
> though apparently structs will be automatically copied on assignment if the struct implements the Copy trait

What's actually going on is that the Rust compiler is always allowed to choose whether to just copy bits during assignment, but if your type implements Copy then the value isn't gone after it has been assigned as it would be with the ordinary destructive move assignment semantic -- so any code can continue to use the value whereas otherwise it would no longer exist having been moved.

Some languages make the built-in types magic and you can't have that magic in your own types, Rust mostly resists this, a few things are magic in the stdlib and you wouldn't be allowed to do this magic in stable Rust (it's available to you in nightly) but mostly, as with Copy, your types are just as good as the built-in types.

This actually feels really natural after not long in my experience.

By @eterm - 3 months
This sometimes catches out people C#/.Net too, it's a big difference between Class and Struct, Class is reference type and Struct is value type. (see fiddle below), but in practice people very rarely reach for structs, so people don't tend to build up the muscle memory of using them, even if they intuitively understand the difference between reference types and value types from general use of other types.

(Fiddle demonstration for non-.Net peeps: https://dotnetfiddle.net/apDZP5 ).

By @knorker - 3 months
It's sad that most of the items are clear language design mistakes stemming from the creators not learning from other languages. Go is a missed opportunity. Item after item of "yeah that wouldn't happen in rust".

The list feels like it's meant to blame the programmer, but that ain't right.

By @maerF0x0 - 3 months

    func findThing(things []Thing, name string) *Thing {
      for i := range things {
        if things[i].Name == name {
          return &things[i]
        }
      }
      return nil
    }
Also you could just return i or -1, and the consuming code would be clear about what it was doing. Find the index. Update the item at the index.

    if location := findThing(things, name); location != -1 {
         things[location].Name = "updated"
    }
By @zuzuleinen - 3 months
To generalize the title into a rule is good to remember that in Go everything is passed by value(copy).
By @geoka9 - 3 months
A (shameless) plug: I've been building a collection of Go bits like this. Hopefully it can be useful to someone other than me, too:

https://github.com/geokat/easygo

By @nasretdinov - 3 months
For me, who came from PHP, the way Go works seemed the most natural. PHP is also one of very few (old) languages which makes everything pass-by-value (except for objects, which initially also were pass-by-value but it was so confusing for people coming from other languages that they changed it).

Treating everything as a value IMO is quute nice _if you except it_, because it eliminates a whole class of possible side effects from mutating the value inside the receiver, without requiring extra complexity like immutability in the language itself.

By @akira2501 - 3 months
It can also be a performance issue since range has to make a copy of whatever was in the slice. Slices of pure structs can be tantalizing for their simplicity but you should be thinking of how you want to range over them first and double check yourself everytime you write:

    _, obj := range ...
You're explicitly asking for the two argument convenience which can have a price.
By @glenjamin - 3 months
Something that strikes me about Go's approach here, and the explanations in many of the posts on this page, are that they're all focused on what is happening under the hood: What memory are we pointing at, what's being copied, is it a pointer etc etc.

Whereas if we start from a point of view of "What semantics and performance guarantees do we desire?", we might end up with a more coherent user-facing interface (even if internally that leads to something more complex).

Personally, my mental model is often influenced by Python - where a name is distinct from a variable, but this distinction doesn't seem to appear in many other languages.

By @juped - 3 months
I always get surprised in the opposite direction by languages that work like that, fun to see it from the other side.

As for the second mistake listed, this is practically the reverse confusion itself... I remember one time in an interview, I got this bit of arcana about Go slices right and the interviewer insisted it was wrong, and despite the evidence being on the screen in the program output at the time, I just backed down. Not sure why I or anyone ever submits to the indignity of job interviews, but it also soured me on Go itself a bit!

By @unwind - 3 months
Nice post!

Also, not everyone knows that even the much-maligned old C does this.

It's a huge red flag/cringe when someone breaks out memcpy() just to copy a struct value (or return it, obviously).

By @usrbinbash - 3 months

    thing := Thing{...}
    other_thing := thing
    pinter_to_same_thing := &thing
ALL types in go are being copied by value. There is no such thing as a "reference" in this language. Even a slice or map is just a small struct with some syntactic sugar provided by the language, and when you assign a slice to another variable, you are, in fact, creating a copy of that struct.
By @ryandv - 3 months
Case of where the language affects (and can clarify/obscure) your model of the system and its behaviour: C++'s copy-assignment operator [0], which makes these semantics explicit.

[0] https://en.cppreference.com/w/cpp/language/copy_assignment

By @mjevans - 3 months
jvns highlights some of the easier to forgot or overlook mistakes, but their source article https://100go.co is a great refresher and introduction as well.
By @manlobster - 3 months
Golang is sometimes considered a simple language, but it's not really beginner-proof like Java was designed to be. It's a good idea to spend time learning it thoroughly.
By @ozfive - 3 months
Looks as though the range loop isn't an issue from Go 1.22 anymore.

https://go.dev/blog/loopvar-preview

By @binary132 - 3 months
It is terrifying to read these comments. I don’t think I realized how confused so many programmers are about how programming works. Maybe the safety police are right after all.
By @OJFord - 3 months
The one about named returns, err always being nil, why is err even in scope, seems like it should be a compile error to me? (I rarely write Go).
By @frankjr - 3 months
Another thing to watch out for is that "defer" in Go is executed at the end of the function, not at the end of the current scope. This makes it not only more difficult to reason about but also much less useful.
By @HackerThemAll - 3 months
Read The Fine Manual, and read some books, so underappreciated these days...
By @acheong08 - 3 months
Misconceptions probably come from Java or Python where a bunch of things are implicitly done for you. I much prefer Golang’s explicitness. The stuff with slices are confusing though