The short answer is no, this won't work reliably, for several reasons:
1. Race between move and goroutine creation
go func(c2 *ftp.ServerConn) {
c2.List()
}(move(&c))
The sequence is:
1. move(&c) executes in G1
2. Return value sits in G1's registers/stack
3. New goroutine G2 is scheduled
4. Value is copied to G2's stack
Between steps 1-4, the pointer lives in G1's execution context. The GC could run during this window.
2. Compiler doesn't understand ownership
c, err := getFtpConnection()
// Compiler may keep 'c' in a register here
go func(c2 *ftp.ServerConn) {
c2.List()
}(move(&c))
// Even with c = nil, compiler might have copies
Go's compiler can:
- Keep copies in registers
- Introduce temporaries you don't see
- Reorder operations
Setting c = nil in source doesn't guarantee all machine-level references are cleared.
3. GC scans conservatively
Go's GC scans entire stack frames, including:
- All slots that could hold pointers
- Registers at safepoints
- Temporary locations
There's no mechanism to tell the GC "I've semantically transferred this pointer."
4. The move generic doesn't compile
func move[PT *any](pp *PT) (res PT) // Won't work
You'd need:
func move[T any, PT *T](pp *PT) PT
But even then, it doesn't achieve the semantic goal.
5. What would actually be needed
For true ownership transfer, you'd need runtime support:
// Hypothetical - doesn't exist
func runtime.TransferOwnership[T any](from *T) T {
// 1. Atomically mark object as "in transfer"
// 2. Clear stack slot with write barrier
// 3. Return value with new ownership metadata
}
This would require changes to:
- GC's stack scanner (recognize transferred objects)
- Write barriers (track ownership changes)
- Escape analysis (understand cross-goroutine transfers)
The fundamental issue
Go's memory model is shared memory with GC, not ownership-based. The GC assumes any reachable pointer might be used. There's no concept of "this goroutine owns this object."
Rust can do this because ownership is compile-time — the borrow checker ensures single ownership. Go would need either:
1. Compile-time ownership tracking (major language change)
2. Runtime ownership metadata (significant GC overhead)
What you can do instead
// Use channels for explicit handoff
ch := make(chan *ftp.ServerConn, 1)
c, err := getFtpConnection()
if err != nil {
return nil, err
}
ch <- c
c = nil // Now truly unreachable from G1
go func() {
c2 := <-ch
c2.List()
}()
This achieves a cleaner ownership transfer, though the GC still manages the lifetime — it just now knows c in G1 is nil before G2 runs.