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

262 views
Skip to first unread message

Axel Wagner

unread,
Jan 7, 2026, 2:15:20 AMJan 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 AMJan 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.

Qingwei Li

unread,
Feb 24, 2026, 8:41:15 AM (2 days ago) Feb 24
to golang-nuts
Sorry for the late reply and thank you all for your replies.
I think I should loosen the requirements for my question without introducing the concept of ownership. 

Does setting c to nil in "move" function helps GC sweep the connection object pointed by c and c2 soon if G1 keeps long but does not use connection object, G2 finishes soon and the c pointer does not exist in temporary locations, slots that can hold pointers, or registers? The program in original post in pasted here:

```go
func move[T any, PT *T](pp *PT) (res PT) {

res = *pp
*pp = nil
return
}
// 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
```

I may do an experiment with goref tool, which I recently found, and show the result here. Thanks. 

Ian Lance Taylor

unread,
Feb 24, 2026, 5:26:01 PM (2 days ago) Feb 24
to Qingwei Li, golang-nuts
On Tue, Feb 24, 2026 at 5:41 AM 'Qingwei Li' via golang-nuts
<golan...@googlegroups.com> wrote:
>
> Sorry for the late reply and thank you all for your replies.
> I think I should loosen the requirements for my question without introducing the concept of ownership.
>
> Does setting c to nil in "move" function helps GC sweep the connection object pointed by c and c2 soon if G1 keeps long but does not use connection object, G2 finishes soon and the c pointer does not exist in temporary locations, slots that can hold pointers, or registers? The program in original post in pasted here:

While things can vary, in general, no, setting c to nil won't make any
difference. The compiler keeps track of when variables are live, and
the GC ignores variables that are not live.

Ian

Qingwei Li

unread,
Feb 25, 2026, 10:39:35 PM (11 hours ago) Feb 25
to golang-nuts
Thanks for your explanation.
Reply all
Reply to author
Forward
0 new messages