Who owns this value? Three rejections from a real compiler, each beside the Go program that runs happily and shares the memory instead.
The finding. Rust answers the ownership question by refusing to let a second name for the value exist. Go answers it by handing you a second name that reaches the same memory. Neither is being careless: one enforces a single owner at compile time, the other documents that a slice is a header over an array somebody else may also hold. The difference shows up in what you have to run to see it.
Nothing on this page touches a chain.
Captured output, not a compiler or a program running here
No compiler runs when you open this page, nothing is executed and nothing is fetched. A committed harness ran the Rust build, ran the Go build, ran the Go binary it had just built, and wrote all of it to JSON — which is compiled into this binary. Every caret, every label, every suggested fix and every line of program output below is read out of that.
There is no memory diagram on this page, and that is deliberate. You will not find a stack frame, a heap box or an arrow from a name to a value — not because such a picture would be hard to draw, but because the capture does not contain one. Drawing it would mean this page teaching you something it did not measure.
The harness is pigfox/rust-move-lab, and the fixtures below live at commit fdcb74e.
What the Go compiler said about all of this
Nothing. Not a warning, not a note, not a vet finding — ownership is not a thing Go's compiler has an opinion about, so there is no second set of diagnostics to put beside the first. That is why this page shows what the Go programs do rather than what a compiler said about them.
Build command
go build -o <tmp> . — exit 0
Bytes the compiler printed
0
Recorded as
compilerSaidNothing = true
That last line is a control, not a claim. The harness computes it from both of the build's output streams and refuses the whole capture if it is ever false, so a page carrying this section cannot have been built from a capture where the compiler spoke.
One rule, three syntaxes
Every error below carries the same code, from the same pass. Binding to a second name, passing to a function, and going round a loop a second time are the same ownership transfer written three ways — which is why the label beside each code names the syntax rather than the compiler stage, and why each diagnostic is found by which program it came from rather than by its code.
Step 1 Bind it to a second name
E0382Assignmentsrc/main.rs
One value, two names. Rust does not copy the String and does not alias it: it moves the single owner from the first name to the second, and the first stops being usable from that line onward. Read the message closely — the compiler calls it a BORROW of a moved value, because the print macro takes its argument by reference, so the failure is not "you used it" but "you tried to look at it".
error[E0382]: borrow of moved value: `a`
22 | println!("{a} {b}");
| ^ value borrowed here after move
The error points at 2 further places, and it needs all of them: what is wrong is a relationship between two lines, which neither line shows on its own.
20 | let a = String::from("hello");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
21 | let b = a;
| - value moved here
What rustc offers here
Each fix carries the compiler's own grade for it. MachineApplicable means cargo fix would apply it unattended; MaybeIncorrect means it compiles and may not be what you meant. They are shown differently because printing them the same way would flatten the compiler's hedging into a recommendation it did not make.
consider cloning the value if the performance cost is acceptable
21 - let b = a;21 + let b = a.clone();
Applicability: MachineApplicable
The same shape in Go, running
The same two lines in Go leave both names alive, and pointing at the same array. A slice value is a three-word header — pointer, length, capacity — so assigning it copies the header and not the memory behind it. Writing through the second name changes what the first one sees, and the program checks that by comparing the addresses of the two slices' first elements rather than asserting it.
What the program printed in scene assign:
assign/declared: a=[1 2 3] b=[1 2 3] shared=true
assign/after b[0]=99: a=[99 2 3] b=[99 2 3]
The lines marked as measured were not narrated by this page or by the program's author. The program compared the addresses of the two slices' first elements and printed the answer, which is the only way a claim about sharing can be evidence rather than an assertion.
Step 2 Pass it to a function
E0382Function callsrc/bin/into_call.rs
The same rule with no assignment operator anywhere in it. Handing a value to a function that takes it by value is a move, exactly as binding it to a new name was — which is the point of the pair. Rust does not have a rule about `=`; it has a rule about ownership, and every syntax that hands a value somewhere else is that one rule.
error[E0382]: borrow of moved value: `a`
21 | println!("{n} {a}");
| ^ value borrowed here after move
The error points at 2 further places, and it needs all of them: what is wrong is a relationship between two lines, which neither line shows on its own.
19 | let a = String::from("hello");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
20 | let n = consume(a);
| - value moved here
What rustc offers here
Each fix carries the compiler's own grade for it. MachineApplicable means cargo fix would apply it unattended; MaybeIncorrect means it compiles and may not be what you meant. They are shown differently because printing them the same way would flatten the compiler's hedging into a recommendation it did not make.
consider cloning the value if the performance cost is acceptable
20 - let n = consume(a);20 + let n = consume(a.clone());
Applicability: MachineApplicable
The same shape in Go, running
Go passes the slice header by value too, and the caller's array still comes back changed, because what was copied was a pointer to it. "Passed by value" and "the callee cannot affect me" are different statements, and Go is one of the languages where only the first is true. The caller's name is also still usable afterwards, which is precisely what the Rust version is refused for.
What the program printed in scene call:
call/before: a=[1 2 3]
call/after bump(a): a=[1001 2 3] stillUsable=true
Step 3 Do it twice
E0382Loopsrc/bin/in_loop.rs
Every line here would be accepted on its own, and a program that made this move once and stopped compiles. What the compiler rejects is the SECOND time round, and it says so in a note worth reading twice: the line is in conflict with ITSELF, one iteration ago. Nothing in the source text is wrong. The loop is what makes it wrong, and no amount of staring at the line reveals that.
error[E0382]: use of moved value: `a`
27 | total += consume(a);
| ^ value moved here, in previous iteration of loop
The error points at 2 further places, and it needs all of them: what is wrong is a relationship between two lines, which neither line shows on its own.
24 | let a = String::from("hello");
| - move occurs because `a` has type `String`, which does not implement the `Copy` trait
26 | for _ in 0..2 {
| ------------- inside of this loop
What rustc offers here
Each fix carries the compiler's own grade for it. MachineApplicable means cargo fix would apply it unattended; MaybeIncorrect means it compiles and may not be what you meant. They are shown differently because printing them the same way would flatten the compiler's hedging into a recommendation it did not make.
consider moving the expression out of the loop so it is only moved once
26 - for _ in 0..2 {26 + let mut value = consume(a);
for _ in 0..2 {
Applicability: MaybeIncorrect
consider moving the expression out of the loop so it is only moved once
27 - total += consume(a);27 + total += value;
Applicability: MaybeIncorrect
consider cloning the value if the performance cost is acceptable
27 - total += consume(a);27 + total += consume(a.clone());
Applicability: MachineApplicable
The same shape in Go, running
This is where the comparison earns its keep. Go runs both iterations, and the second silently overwrites what the first wrote — into a slot neither the slice nor the returned value ever shows you, because it is past the length and inside the capacity. Watch the backing array between the two iterations. Whether that write is visible at all depends on the spare capacity, which is a number nobody in this program chose deliberately.
The lines marked as measured were not narrated by this page or by the program's author. The program compared the addresses of the two slices' first elements and printed the answer, which is the only way a claim about sharing can be evidence rather than an assertion.
Everything the program printed
The scenes above are a partition of the 8 lines below, not a selection from them. Here is the whole run, verbatim and in order, so that can be checked rather than taken on trust. Order matters here and is not incidental: in the last scene the second iteration overwrote what the first one wrote, which is a statement about sequence.
The Rust build was supposed to fail, and it did. cargo exited 101 and reported 3 errors across three separate binaries. Three, and not one file with three functions in it: each program is the smallest thing that shows its own move, so every caret points into a file containing nothing else.
Two Go versions appear in that capture, and only one built anything. The capture machine's default toolchain is go version go1.26.3 linux/amd64; the harness module asks for a newer one, and with GOTOOLCHAIN=auto Go switches to it silently for any command run inside that module. Both are pinned so the switch is visible rather than hidden.
The two commit identifiers above differ, and they always will. A capture reads the working tree, writes the fixture, and only then is the fixture committed — so the sources that produced these bytes are one commit earlier than the commit carrying them. Nothing is out of step.
What was dropped on the way here. The Rust fixture is 35261 bytes in this repository and 78656 in the harness. Two fields were removed: the text of rustc --explain for the error code, repeated once per diagnostic, which this page does not render and you can run yourself; and the verbatim cargo output, which duplicates the parsed diagnostics beside it. The program's own capture is committed whole, which is why its output is printed above in full.
One of these fixtures is byte-reproducible and the other is not, and the harness treats them differently for that reason. cargo compiles the three binaries in parallel, so the order they finish in varies between runs of the same commit and the capture timestamp moves every time; the harness compares two Rust captures through a projection that sorts those streams. The program's output has no such excuse — same binary, same bytes, every time — so it is compared exactly, and its order is never sorted away.