14 ms·
Go structs are copied on assignment (and other things about Go I'd missed)
- MathMonkeyMan 2y agoDonovan 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 https://go.dev/ref/spec
- heyoni 2y agoThe language spec was so good I was able to make tangible contributions to an open source project just by using that and I don’t consider myself a go programmer at all. I want to buy that book but it’s technical and I feel like there might be a second edition around the corner? /edit I bought it after reading this thread: https://groups.google.com/g/golang-nuts/c/U99js3UYz-U https://groups.google.com/g/golang-nuts/c/U99js3UYz-U
- trentnix 2y agoLearning Go by Jon Bodner, particularly the newest edition, is also excellent. An aside: are there any other Go books, particularly ones the explore more specific topics, that are recommended? I've read a few that I didn't find very impressive.
- pss314 2y agoI liked Cloud Native Go by Matthew A. Titmus and Concurrency in Go by Katherine Cox-Buday.
- metadat 2y agoNot 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.
- simonw 2y ago"Very basic" is the entire point of this exercise. Just because things are basic doesn't mean people won't misunderstand them, and won't benefit from clarification. Which of those 100 mistakes were "plain wrong"? That would be useful feedback for the author.
- adastra22 2y agoAs she points out though, a lot of dynamic languages don’t behave this way. A string after all points to a heap allocation; so it’s not unreasonable to think of a string as a pointer.
- jerf 2y agoI've several times answered questions from people coming from dynamic languages that ask lots of questions about Go pointers. And the answer is, actually, since Go lacks pointer arithmetic, Go pointers work the way you're used to things working. It's the Go non-pointers that are the new bizarre thing you're not used to! So there is definitely a common language heritage that will find the behavior of value copies in Go surprising. I came into Go with compiled language experience but I'd been exclusively in dynamic scripting languages exclusively for over a decade. I had to remind myself about this as well.
- layer8 2y agoIt’s how structs work in C though, and Go is spiritually very close to C, including explicit pointer types, address-taking and dereferencing.
- adastra22 2y agoNot necessarily. A string in C is usually a char. If you have a struct with a char and you copy it, you copy the pointer to the backing memory. This is analogous to Go which also has a String be a pointer to the heap, but the behavior is different. Go’s String is a char* that behaves like a char[] when copied.
- 2y ago
- zuzuleinen 2y agoTo generalize the title into a rule is good to remember that in Go everything is passed by value(copy).
- jerf 2y agoThat is the case for almost every modern language. C++ is one of the few languages that has "references" and at least last I looked that's a language accommodation over what are pointers being passed by value in the assembly, at least until compiler optimizations take over (and that's not limited to references either). If you're in 2024 and you're in some programming class making a big deal about pass-by-value versus pass-by-reference, ask for your money back and find a course based in this century. Almost literally any topic is a better use of valuable class time than that. From what I've seen of the few unfortunate souls suffering through such a curriculum in recent times is that it literally anti-educates them.
- azundo 2y agoIs python no longer a modern language? Objects are certainly not copied when passed to a function.
- jerf 2y agoPython copies references by value. $ python3 Python 3.12.3 (main, Jul 31 2024, 17:43:48) [GCC 13.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> def x(): ... v = 1 ... y(v) ... print(v) ... >>> def y(val): ... val += 1 ... >>> x() 1 A pass-by-reference language would print 2. Everything in a modern language is passed by copy. Exactly what is copied varies and can easily be pointers/references. But there were languages once upon a time that didn't work that way. It's a dead distinction now, though, unless you go dig one of them up. If you want a specific one to look at, look at Forth. Note how when you call a function ("invoke a word", closest equivalent concept), the function/word doesn't get a copy of anything. It directly gets the actual value. There is no new copy, no new memory location, it gets the actual same memory as the caller was using, and not as a "pointer"... directly. Nothing works like that any more.
- _lvbh 2y agoMisconceptions 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
- deleted 2y ago[deleted]
- jsfysdfwmyasdf 2y ago[flagged]
- fredrikholm 2y ago> Maybe you meant Zig or C This seems hubris for someone who depends on types, looping constructs, and compilers. Maybe you meant writing machine code or 6502, then I'd likely agree, but C is quite literally (& excellently) designed for quiche eaters. /s
- metadat 2y agoAgreed on this one, the "fix" involving the capacity flag, e.g. "2:3:3" is unintuitive compared to Python where there is no such concept. Still, as far as sharp edges go these are nothing compared to Java. See the discussion from: Common I/O Tasks in Modern Java https://news.ycombinator.com/item?id=41142737 https://news.ycombinator.com/item?id=41142737 House of horrors, especially the URL equality triggering DNA requests.
- throwaway8e83 2y ago> URL equality triggering DNS requests I agree it can be confusing, as the URL also can act as a client that performs actual connections. The documentation actually mentions that URI is a better choice when you want only a representation In general I don't think the other examples are that bad. The reason why there are so many different ways of performing I/O, is because Java is evolving and adding better solutions, but can't really remove older stuff like "URL" because it is widely used. I see similar issues with other languages as they evolve, and I think Java has managed it well. The IDEs are also often good at making suggestions on how to replace outdated code.
- simonw 2y agoOne 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.
- Attummm 2y agoPersonally, I think that the idea that programmers should know everything is kinda bizarre. Programming is about finding the answer. If you already knew everything, you could just sit down and type any program from your knowledge. We all know that you can't know everything otherwise, why even write documentation or use git? Unfortunately, it's difficult to move past this idea, and it's so pervasive that stating "I don't know" can negatively affect your standing for some.
- simonw 2y agoRight! The best thing about our chosen career is that it's completely impossible to know everything - there's always a new corner of software engineering to dig into, be it how the Linux kernel works, or programming with Haskell, understanding Transformer LLM architectures, or how the Svelte compiler works or whatever.
- surfingdino 2y ago... and be able to code in React
- silvestrov 2y agoMany people have the same expectation of their doctor: he must know everything or he is completely useless and I should find a new doctor. It is also insanely difficult for a doctor/surgeon to admit a mistake without being punished severly.
- akira2501 2y agoIt 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.
- unwind 2y agoNice 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).
- mjevans 2y agojvns highlights some of the easier to forgot or overlook mistakes, but their source article https://100go.co https://100go.co is a great refresher and introduction as well.
- eterm 2y agoThis 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 https://dotnetfiddle.net/apDZP5 ).
- rickstanley 2y agoNon-C# developer question: what use-case/situation would a `struct` make sense to use instead of a `class`? Just out of curiosity. [Edit] Well, there's a nice, special article for this very question: https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/choosing-between-class-and-struct https://learn.microsoft.com/en-us/dotnet/standard/design-gui...
- makotech221 2y agousually performance reasons; don't want to allocate on the heap, contributing to GC pressure, or you want pass by copy semantics
- neonsunset 2y agoPlease note that the article is quite old and does not encompass the wide variety of scenarios C# is effective at. Structs are used for but not limited to: all kinds of "transient" data containers, pass by value semantics, precise control over layout of data in memory, low-level programming and interoperating with C/C++, zero-cost abstractions via struct generics (ala Rust or templates in C++), lightweight wrappers over existing types, etc. Most C# codebases use them without even noticing in `foreach` loops - enumerators are quite often structs, and so are Span<T> and Memory<T> (which are .NET slice types for arrays, strings and pretty much every other type of contiguous memory, including unmanaged). Tuple syntax uses structs too - `(int x, int y)` is `ValueTuple<int, int>` behind the scenes. .NET has gotten quite good at optimizing structs, so the more general response is "you use them for the same things you use structs in C, C++". Or the same reason you would pick plain T struct in Rust over Box/Arc<T>. Intro to structs: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
- OJFord 2y agoThe 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).
- jyap 2y agoIn the function signature, the return variable err is type error. Since it is named it is also defined and initialized as nil. In the code, an error is found and it does not assign a value to err and just returns it as the error value. So it returns nil as the error when it wants to return an error with a proper value. The code should be something like: if ctx.Err() != nil { return 0, 0, ctx.Err() }
- tialaramex 2y ago> 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.
- HackerThemAll 2y agoRead The Fine Manual, and read some books, so underappreciated these days...
- ozfive 2y agoLooks as though the range loop isn't an issue from Go 1.22 anymore. https://go.dev/blog/loopvar-preview https://go.dev/blog/loopvar-preview
- tialaramex 2y agoThat's a different language design mistake, which several languages have had to fix including C# back in C# 5. The question there is, does our for-each style loop make a new variable each iteration, with the appropriate value, or does it have a single variable and it's just re-assigned for each iteration. People who haven't designed a language before might think the second option sounds optimal and won't make a practical difference, but it's actually very annoying and that's why Go changed to the former. This time though it's not about the variable staying the same, the problem is that we got a copy of the data we cared about, not a mutable reference to that data.
- maerF0x0 2y agofunc 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" }
- Joker_vD 2y agoWell, if you don't mind that it doesn't work correctly with slices: [0], then sure, you may return indices. [0] https://go.dev/play/p/Q2ntuaugbGQ https://go.dev/play/p/Q2ntuaugbGQ
- benmmurphy 2y agoIt does work with slices as long as you use the slice to update as well
- maerF0x0 2y agoThat didn't work because you didn't pass the same slice in. You subsliced your things slice, which outputs a new slice.
- Animats 2y agoThe 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-value-optimization-or-move https://stackoverflow.com/questions/17473753/c11-return-valu...
- mariozski 2y agoActually Go is always pass by value. Even when you pass the pointer you’ll get a copy of it.
- the_gipsy 2y agoWhat matters is that a function can modify the value pointed at, without necessarily returning it.
- binary132 2y agoThat is no different than C or C++. A pointer is just another type of value.
- everybodyknows 2y ago> Actually Go is always pass by value. False, this depends upon the underlying type. The Go 'map' type is always pass (and assign) by reference. https://go.dev/play/p/ovfuNBNtiza https://go.dev/play/p/ovfuNBNtiza
- geoka9 2y agoA (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 https://github.com/geokat/easygo
- frankjr 2y agoAnother 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.
- usrbinbash 2y agothing := 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.
- uniq7 2y agoMy pet peeve with slices and maps is that they hide a reference to the actual structure, and you are never sure of what you are modifying, or the performance impact when moving around big structures. Example with slices: https://go.dev/play/p/8arcUrGU4SU https://go.dev/play/p/8arcUrGU4SU Example with maps: https://go.dev/play/p/eq8i6z8a4jN https://go.dev/play/p/eq8i6z8a4jN
- usrbinbash 2y agoNo, there is no "hiding" of a "reference". There is copying a struct: s2 := s1 This copies the struct in `s1` into a new name `s2`. This struct contains, among other things, a pointer to the backing array. Therefore, when you assign to the slice s2[0] = "bye" You assign to the same backing array. Slices are not arrays. Copying a slice copies a struct containing a pointer to an array. A similar situation holds true for maps. The same logic that is universal throughout the language, aka. "Go only ever copies things by value" holds true for all of these types. https://go.dev/ref/spec#Slice_types https://go.dev/ref/spec#Slice_types "A slice, once initialized, is always associated with an underlying array that holds its elements. A slice therefore shares storage with its array and with other slices of the same array; by contrast, distinct arrays always represent distinct storage."
- Joker_vD 2y agoSince a slice/map, internally, contains a pointer to the data, it looks like slices/maps have reference semantics: after you do "m2 := m1", all changes done through m1 are visible through m2, even though the type of m1 and m2 has no visible asterisk anywhere in it.
- knorker 2y agoIt'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.
- antonvs 2y agoThis drives me crazy. Go essentially doubles down on all the programming language mistakes we spent decades discovering and finding solutions for. When people ask all those questions about why software is still so terrible in all sorts of ways, a large part of the answer is languages like C, C++, JavaScript, Python, and now Go.
- nasretdinov 2y agoFor 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.
- glenjamin 2y agoSomething 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.
- ryandv 2y agoCase 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 https://en.cppreference.com/w/cpp/language/copy_assignment
- binary132 2y agoIt 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.
- juped 2y agoI 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!
- manlobster 2y agoGolang 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.