Live data from Hacker News

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

jvns.ca

131–140 of 176 posts

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

#131
post #3

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.

That was my reaction too, but it does point the way to a more subtle chronic ache in coding Go: the efficiencysimplicity tradeoff between passing some large struct to a function by reference, or by copying.

The former, "by reference" is guaranteed to impose no increase in calling overhead, irrespective of the compiler's ability to optimize the object code, however fat the struct eventually grows.

The latter, "by copying" guarantees the calling function will upon return find all fields of the struct just as before -- a great aid to understanding during code review.

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

#132
post #126

Earlier quoted context omitted.

Right. Passing pointers is much cheaper than passing values of large structures. And then references are an abstraction over pointers that allow further compile-time optimization in languages that support it. Pass-by-value, pass-by-pointer, and pass-by-reference are three distinct operational concepts that should be taught to programmers.

I think the right mental model is pass-by-value for the first two. There is nothing different in the calling convention between sending a parameter of type int* vs a parameter of type int. They are both pass-by-value. The value of a pointer happens to be a reference to an object, while the value of an int is an int. In both cases, the semantics is that the value of the expression passed to the function is copied to a…

> I think the right mental model is pass-by-value for the first two. There is nothing different in the calling convention between sending a parameter of type int* vs a parameter of type int.

You're talking about parameters of type int; I'm talking about structs that are strictly larger than pointers. Structs which may be nested; for which deep copies are necessary to avoid memory leaks / corruption. And here, the distinction between these "mental models" exhibits a massive gap in real performance.

Here's a deliberately pathological case in C++; I've seen this error countless times from programmers in languages that make a distinction between references/pointers and values:

    bool vector_compare(vector vec, size_t i, size_t j) {
        return vec[i]  vec) {
        if (vec.size()) {
            size_t arg = 0;
            for(size_t i = 1; i 
The vector_compare function makes a copy of the full vector before doing its thing; this ends up turning my linear-looking runtime into accidentally-quadratic. From the perspective of this solitary example, it would make sense to collapse reference/pointer into the same category and leave "value" on its own.

But actually these are three distinct concepts, with nuance and overlap, that should be taught to anybody with more than a passing interest in languages and compilers. I'm not here to weigh in on what constitutes a modern language, but the notion that we should just throw this crucial distinction away because some half-rate programmers don't understand it is patently offensive.

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

#133
post #53

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…

Actually Go is always pass by value. Even when you pass the pointer you’ll get a copy of it.

> 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

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

#134
post #47

Earlier quoted context omitted.

So what should we call "foo(x)" and "foo(ref x)" in C# to distinguish them if not pass-by-value and pass-by-reference?

C# can call it that specifically if it likes, because the general computer science term is dead, but under the hood you're passing a reference by value. Look to the generated assembler in a non-inlined function. You'll find a copy of a pointer. You did not in true pass-by-refernce langauges. The fact that is a sensible thing to say in a modern language is another sign the terminology is dead.

You're talking about pointers but calling them references. I'm sorry, but no, the terminology is not "dead" you're just contributing to confusion.

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

#135

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" }

Well, 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

That didn't work because you didn't pass the same slice in. You subsliced your things slice, which outputs a new slice.

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

#136
post #129

Earlier quoted context omitted.

Let's work through the example 1. There's a user-defined type named Thing with a String inside it. 2. We've got a function named findThing, which takes an array of Things and a string name, performs a linear search of the array and returns a mutable reference to the Thing with that name or if it's not found it doesn't. In Go, this function accidentally gives back a mutable reference to a copy of the Thing. 3. We make…

Ah, I was working on a mistaken assumption about the Go example code (I thought the function returned Thing, whereas actually it returns *Thing). That will teach me to RTFA...

Right, I think Julia would spot that if we get a Thing back, it's clearly not the Thing inside our array, because those are necessarily different Things.

If the function does return Thing, in Rust we can implement the mistake, returning a copy, but there's no plausible way to implement the intended functionality, we can get the matching thing out of an array we're allowed to mutate -- by swapping it, but now some different Thing is in the array of Things instead, that's what swapping means. If we're given the actual array, not a (mutable) reference as a parameter, we can destroy it and keep just the matching Thing to return, but now our caller hasn't got a Things array, it was destructively moved to give us a parameter.

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

#137

Earlier quoted context omitted.

Actually Go is always pass by value. Even when you pass the pointer you’ll get a copy of it.

> 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

No, it's definitely passed by value:

https://go.dev/play/p/mIehOwUWz95

A map is implicitly a pointer to the underlying map structure. When you pass a map, you're passing a copy of a pointer.

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

#138
post #132

Earlier quoted context omitted.

I think the right mental model is pass-by-value for the first two. There is nothing different in the calling convention between sending a parameter of type int* vs a parameter of type int. They are both pass-by-value. The value of a pointer happens to be a reference to an object, while the value of an int is an int. In both cases, the semantics is that the value of the expression passed to the function is copied to a…

> I think the right mental model is pass-by-value for the first two. There is nothing different in the calling convention between sending a parameter of type int* vs a parameter of type int. You're talking about parameters of type int; I'm talking about structs that are strictly larger than pointers. Structs which may be nested; for which deep copies are necessary to avoid memory leaks / corruption. And here, the dis…

My point is the same for int as for vector. There is 0 difference in the C++ calling convention between passing a vector and a vector: they both copy an object of the parameter type. Of course, copying a 1000 element vector is much slower than copying a single pointer, but the difference is strictly the size of the type. The copying occurs the same way regardless. This is also the reason foo(char) is less overhead than a foo(char).

Everything (except reference types) is pass-by-value, but of course values can have wildly different sizes.

Also, the problem of accidentally copying large structs is not limited to arguments, the same considerations are important for assignments. Another reason why "pass-by-pointer" shouldn't be presented as some special thing, it's just passing a pointer copy.

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

#139

Earlier quoted context omitted.

Actually Go is always pass by value. Even when you pass the pointer you’ll get a copy of it.

> 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

Technically no, but I get why it feels like this. :-)

I think the confusion stems from the fact that Go hides the distinction of "dot" access vs "pointer dot" access. (eg, the difference between `foo.Bar` and `foo->Bar` in C++). Go knows if an object is a pointer and needs to be de-referenced, and hides the de-reference operation from you syntactically. So you can use a pointer without knowing it's a pointer.

This is the confusion: You never have a "map object that is passed by reference", you instead have a "pointer to a map object that is always passed by value".

This is less confusing for structs the programmer defines themselves, since they can check the type of "foo" see if "foo.Boo" is accessing Bar directly or de-referencing "foo" and then accessing "Bar".

But it's more confusing when applied to builtin type for which you never see the implementation. How would you know if `map[string]bool{}` returns a value or a pointer? You don't!

The map type is passed by value but its implementation is that it is a thin wrapper around a pointer to a struct. Ergo in practice it feels like passing by reference. But Go is technically always pass by value. Map does not get any special implementation in the language.

You can do this yourself. `type MyStruct struct{ data *MyType }`. Now if you pass MyStruct by value, any operations on it are effectively by reference since all operations have to dereference `data` to get at the actual content.

(Same goes for strings, except they are immutable so no one notices.)

This might feel like semantics, but it's important to remember that Go doesn't treat any of those built-ins with special "pass by reference" rules. Instead, it's behaving consistently like it would for any types you defined yourself and one of the learning curves of the language is to think in Go's terms of pointers vs non-pointers and learn which native types are pointers. If you think of it that way, learning Go's built-ins is no different than learning the API methods for a custom struct Go's built-ins just happen to be, well, built-in and thus get syntactic sugar that your custom structs do not.

"Pass by reference" is usually used to refer to the case where you use a value at level N of the call stack, but the code at level N+1 of the callstack decides to make it a pointer. In Go this never happens, instead you technically always had a pointer the whole time!

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

#140
post #132

Earlier quoted context omitted.

> I think the right mental model is pass-by-value for the first two. There is nothing different in the calling convention between sending a parameter of type int* vs a parameter of type int. You're talking about parameters of type int; I'm talking about structs that are strictly larger than pointers. Structs which may be nested; for which deep copies are necessary to avoid memory leaks / corruption. And here, the dis…

My point is the same for int as for vector . There is 0 difference in the C++ calling convention between passing a vector and a vector : they both copy an object of the parameter type. Of course, copying a 1000 element vector is much slower than copying a single pointer, but the difference is strictly the size of the type. The copying occurs the same way regardless. This is also the reason foo(char) is less overhead…

Your point rather misses the mark.

Your vector is a red herring. The distinction I'm making is between passing a (vector)* and a vector, because those two objects have radically different sizes, and the distinction can and does create severe performance issues. And yet, pointers are still different from references: with a reference, you don't even need your object to have a memory address.

Post reply on HN