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 articleThe 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.
Related
Interface Upgrades in Go (2014)
The article delves into Go's interface upgrades, showcasing their role in encapsulation and decoupling. It emphasizes optimizing performance through wider interface casting, with examples from io and net/http libraries. It warns about complexities and advises cautious usage.
Common Interface Mistakes in Go
The article delves into interface mistakes in Go programming, stressing understanding of behavior-driven, concise interfaces. It warns against excessive, non-specific interfaces and offers guidance from industry experts for improvement.
First impressions of Go 1.23's range-over-func feature
The author shares positive experiences with Go 1.23's range-over-func feature, initially skeptical but finding it easy to use. Successful adaptation in their project Kivik disproved initial concerns, highlighting benefits for codebase improvement.
Full Introduction to Golang with Test-Driven Development
The article presents a tutorial series on learning Go through Test-Driven Development, covering the creation of a "Hello, World!" program, module management, and testing using Go's framework.
Don't write Rust like it's Java
The author discusses transitioning from Java to Rust, highlighting Rust's type safety, differences in traits and interfaces, complexities of ownership, and the importance of embracing Rust's unique features for effective programming.
- 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.
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...
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.
Then read the [Go Language Specification][1] cover to cover. It's dry but refreshingly not legalese.
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.
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.
(Fiddle demonstration for non-.Net peeps: https://dotnetfiddle.net/apDZP5 ).
The list feels like it's meant to blame the programmer, but that ain't right.
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"
}
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.
_, obj := range ...
You're explicitly asking for the two argument convenience which can have a price.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.
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!
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).
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.[0] https://en.cppreference.com/w/cpp/language/copy_assignment
Related
Interface Upgrades in Go (2014)
The article delves into Go's interface upgrades, showcasing their role in encapsulation and decoupling. It emphasizes optimizing performance through wider interface casting, with examples from io and net/http libraries. It warns about complexities and advises cautious usage.
Common Interface Mistakes in Go
The article delves into interface mistakes in Go programming, stressing understanding of behavior-driven, concise interfaces. It warns against excessive, non-specific interfaces and offers guidance from industry experts for improvement.
First impressions of Go 1.23's range-over-func feature
The author shares positive experiences with Go 1.23's range-over-func feature, initially skeptical but finding it easy to use. Successful adaptation in their project Kivik disproved initial concerns, highlighting benefits for codebase improvement.
Full Introduction to Golang with Test-Driven Development
The article presents a tutorial series on learning Go through Test-Driven Development, covering the creation of a "Hello, World!" program, module management, and testing using Go's framework.
Don't write Rust like it's Java
The author discusses transitioning from Java to Rust, highlighting Rust's type safety, differences in traits and interfaces, complexities of ownership, and the importance of embracing Rust's unique features for effective programming.