Re: [go-nuts] Does hand-written move function without runtime support can transfer the ownership of an object entirely?

107 views
Skip to first unread message

Axel Wagner

unread,
Jan 7, 2026, 2:15:20 AM (5 days ago) Jan 7
to Qingwei Li, golang-nuts
I'm not sure your `move` function actually does anything in this case. AIUI the compiler and runtime are already clever enough to recognize that c is no longer used after the `go` statement (hence the necessity of runtime.KeepAlive) and that's all your `move` is trying to do, no?
And in the general case, you have no guarantee that a pointer passed to `move` is the *only* pointer to the relevant object.
So ISTM the cases where the Go implementation *can't* tell that c is no longer used, adding the `move` doesn't really help.

Do you have concrete evidence that this helps in some cases? Because otherwise it seems like a premature optimization that mostly makes the code harder to read.

On Wed, 7 Jan 2026 at 04:25, Qingwei Li <qingwe...@gmail.com> wrote:
Take the following program as an example.

```go
c, err := getFtpConnection()
if err != nil {
return nil, err
}
go func(c2 *ftp.ServerConn) {
c2.List()
}(c)
// c will not be used later
```

Let's add the `move` function.

```go
// pointer is
func move[PT *any](pp *PT) (res PT) {
res = *pp
*pp = nil
return
}
```

```go
// Goroutine G1
c, err := getFtpConnection()
if err != nil {
return nil, err
}
go func(c2 *ftp.ServerConn) { // Goroutine G2
c2.List()
}(move(&c))
// c will not be used later
```

Would this `move` without runtime support make GC unable to reach the object pointed by `c` scanning from the stack of goroutine G1 so that the ownership of object is entirely moved to goroutine G2? With this ownership transfering, freegc for `c2` in the end of goroutine G2 is feasible in cross-goroutine reference case.


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/5c0b8aaf-8470-4de2-91f6-5d74e884dff3n%40googlegroups.com.

Andrey Andrade

unread,
Jan 7, 2026, 7:41:56 AM (4 days ago) Jan 7
to golang-nuts
 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.
Reply all
Reply to author
Forward
0 new messages