Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime: add AddCleanup and deprecate SetFinalizer #67535

Open
mknyszek opened this issue May 20, 2024 · 43 comments
Open

runtime: add AddCleanup and deprecate SetFinalizer #67535

mknyszek opened this issue May 20, 2024 · 43 comments
Assignees
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. Proposal Proposal-Accepted
Milestone

Comments

@mknyszek
Copy link
Contributor

mknyszek commented May 20, 2024

Background

Go provides one function for object finalization in the form of runtime.SetFinalizer. Finalizers are notoriously hard to use, and the documentation of runtime.SetFinalizer describes all the caveats with a lot of detail. For instance:

  • SetFinalizer must always refer to the first word of an allocation. This means programmers must be aware of what an 'allocation' is whereas that distinction isn't generally exposed in the language.
  • There cannot be more than one finalizer on any object.
  • Objects with finalizers that are involved in any reference cycle will silently fail to be freed and the finalizer will never run.
  • Objects with finalizers require at least two GC cycles to be freed.

The last two of these caveats boil down to the fact that runtime.SetFinalizer allows object resurrection.

Proposal

I propose adding the following API to the runtime package as a replacement for SetFinalizer. I also propose officially deprecating runtime.SetFinalizer.

// AddCleanup attaches a cleanup function to ptr. Some time after ptr is no longer
// reachable, the runtime will (probably) call cleanup(arg) in a separate goroutine.
// 
// If ptr is reachable from cleanup or arg, ptr will never be collected
// and the cleanup will never run. AddCleanup panics if arg == ptr. 
//
// The cleanup(arg) call is not always guaranteed to run; in particular it is not
// guaranteed to run before program exit.
//
// A single goroutine runs all cleanup calls for a program, sequentially. If a cleanup
// function must run for a long time, it should create a new goroutine.
func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup

// Cleanup is a handle to a cleanup call for a specific object.
type Cleanup struct { ... }

// Stop cancels the cleanup call. Stop will have no effect if the cleanup call
// has already been queued for execution (because ptr became unreachable). 
// To guarantee that Stop removes the cleanup function, the caller must ensure 
// that the pointer that was passed to AddCleanup is reachable across the call to Stop.
func (c Cleanup) Stop() { ... }

AddCleanup resolves many of the problems with SetFinalizer.

It forbids objects from being resurrected, resulting in prompt cleanup, as well as allowing cycles of objects to be cleaned up. Its definition also allows attaching cleanup functions to objects the caller does not own, and possibly attaching multiple cleanup functions to a single object.

However, it is still fundamentally a finalization mechanism, so to avoid restricting the GC implementation, it does not guarantee that the cleanup function will ever run.

Similar to finalizers' restriction on the object not being reachable from the finalizer function, ptr must not be reachable from the value passed to the cleanup function, or from the cleanup function. Usually this results in a memory leak, but the common case of accidentally passing ptr as s out of convenience can be easily caught.

In terms of interactions with finalizers, the cleanup function will run after the value pointed to by ptr becomes unreachable and its finalizer has run (and that finalizer does not make the object reachable again). That is, if an object has both a cleanup function and a finalizer, the cleanup function is guaranteed to run after the finalizer. In other words, the cleanup function tracks object resurrection.

Design discussion

Avoiding allocations in the implementation of AddCleanup

AddCleanup needs somewhere to store cleanupValue until cleanup is ready to be called. Naively, it could just put that value in an any variable somewhere, but this would result in an unnecessary additional allocation.

In the actual implementation, a cleanup will be represented as a runtime "special," an off-heap manually-managed linked-list node data structure whose individual fields are sometimes explicitly inspected by the GC as roots, depending on the "special" type (for example, a finalizer special treats the finalizer function as a root).

Since each "special" is already specially (ha) treated by the GC, we can play some interesting tricks. For example, we could type-specialize specials and store cleanupValue directly in the "special." As long as we retain the type information for cleanupValue, we can get the GC to scan it directly in the special. But this is quite complex.

To begin with, I propose just specializing for word-sized cleanup values. If the cleanup value fits in a pointer-word, we store that directly in the special, and otherwise fall back on the equivalent of an any. This would cover many use-cases. For example, cleanup values that are already heap-allocated pointers wouldn't require an additional allocation. Also, simple cases like passing off a file descriptor to a cleanup function would not create an allocation.

Why func(S) and not func()?

The choice to require an explicit parameter to the cleanup function is to reduce the risk of the cleanup function accidentally closing over ptr. It also makes it easier for a caller to avoid allocating a closure for each cleanup.

Why func(S) and not chan S?

Channels are an attractive alternative because they allow users to build their own finalization queues. The downside however is that each channel owner needs its own goroutine for this to be composable, or some third party package needs to exist to accumulate all these channels and select over them (likely with reflection). It's much simpler if that package is just the runtime: there's already a system goroutine to handle the finalization queue. While this does mean that the handling of finalization is confined to an implementation detail, that's rarely an issue in practice and having the runtime handle it is more resource-efficient overall.

Why return Cleanup instead of *Cleanup?

While Cleanup is a handle and it's nice to represent that handle's unique identity with an explicit pointer, it also forces an allocation of Cleanup's contents in many cases. By returning a value, we can avoid an additional allocation.

Why not have the type arguments (T and/or S) on Cleanup too?

It's not necessary for the implementation of Cleanup for the type arguments to be available, since the internal representation will not even contain a reference to ptr, cleanup, or cleanupValue directly. It does close the door to obtaining these values from Cleanup in a type-safe way, but that's OK: the caller of AddCleanup can already package those up together if it wants to.

@gopherbot gopherbot added this to the Proposal milestone May 20, 2024
@randall77
Copy link
Contributor

Another possibility:

func AddCleanup[T any](ptr *T, cleanup func(T)) Cleanup

When cleaning up, we pass a (shallow) copy of *T to the cleanup function.

This gets rid of the need to store cleanupValue anywhere. It is effectively the final state of the object.

On the downside, this may get weird if you're putting a cleanup on a type defined in another package, as you might not be able to do much with the argument.

@cespare
Copy link
Contributor

cespare commented May 20, 2024

@mknyszek I find it hard from reading the proposed signatures and doc comments to understand what S and cleanupValue are. Could you clarify that a bit? It might also help to get an example of how one would use the new API. For instance, how would one use AddCleanup to replace the use of SetFinalizer to close a file when it is GCed?

Another question: would AddCleanup replace some or all uses of SetFinalizer in the standard library? Are there instances where it cannot work, or would be undesirable?

@Merovius
Copy link
Contributor

Merovius commented May 20, 2024

In terms of interactions with finalizers, the cleanup function will always run the first time the value pointed to by ptr becomes unreachable. That is, if an object has both a cleanup function and a finalizer, the cleanup function is guaranteed to run before the finalizer. In other words, the cleanup function does not track object resurrection and will not run again if the finalizer does resurrect the object.

What's the reasoning here? This means that you can not assume that ptr is unreachable when the cleanup runs. e.g. in the case of go4.org/intern (yes, I know that this particular case will be obsolete by #62483, but just for illustration) that would mean you might get different Values for the same interned string. This seems to combine particularly badly with the idea that you can attach cleanups to values you don't own.

If this was done the other way around - cleanups run after finalizers and only if ptr did not get resurrected - you could rely on ptr being unreachable after the cleanup. And if there are no finalizers, ISTM that everything could work the same. So you'd get better invariants now at no cost for The Bright Future of no Finalizers™.

Or is there something (perhaps in the implementation of the runtime) that I'm unaware of?

@mknyszek
Copy link
Contributor Author

When cleaning up, we pass a (shallow) copy of *T to the cleanup function.

Unfortunately, I think that requires treating the contents of ptr as a root, so you can no longer reclaim ptr if it participates in a cycle.

@mknyszek I find it hard from reading the proposed signatures and doc comments to understand what S and cleanupValue are. Could you clarify that a bit? It might also help to get an example of how one would use the new API.

The idea behind this API is to decouple cleanup from the object being freed, so S and cleanupValue represent that decoupling. I can understand that at first glance it seems odd, but this is really the way to avoid a lot of the footguns. Most of the time, finalizers (and cleanup functions) are necessary for cleaning up things that the GC is unaware of, like file descriptors, memory passed from C, etc. You pass that to the cleanup function, not the object itself.

For instance, how would one use AddCleanup to replace the use of SetFinalizer to close a file when it is GCed?

f, _ := Open(...)
runtime.AddCleanup(f, func(fd uintptr) { syscall.Close(fd) }, f.Fd())

I'm taking a lot of liberties in this snippet, but that would be the general idea. I can update the proposal later.

@randall77
Copy link
Contributor

Unfortunately, I think that requires treating the contents of ptr as a root, so you can no longer reclaim ptr if it participates in a cycle.

That's a good point.

Most of the time, finalizers (and cleanup functions) are necessary for cleaning up things that the GC is unaware of, like file descriptors, memory passed from C, etc. You pass that to the cleanup function, not the object itself.

What about this case?

type T struct {
    buf unsafe.Pointer // pointer to some memory allocated with C.malloc
    ...
}

During the lifetime of a T, we may change buf several times, each time with a free/malloc pair.
When the T goes dead, we want to free the last value that was in buf.

How do I write a cleanup for that? I think I would need to Stop/AddCleanup each time I updated buf. I don't see a way to set a cleanup when T is allocated that does the right thing. Unless buf is indirect somehow.
But maybe that's the intention, that you have to Stop/AddCleanup each time?

@mknyszek
Copy link
Contributor Author

@Merovius I think your point about interacting with existing objects using finalizers is interesting. AFAIK there's nothing in the implementation preventing the semantics you propose. I'll have to give this some more thought.

@mknyszek
Copy link
Contributor Author

Most of the time, finalizers (and cleanup functions) are necessary for cleaning up things that the GC is unaware of, like file descriptors, memory passed from C, etc. You pass that to the cleanup function, not the object itself.

What about this case?

type T struct {
    buf unsafe.Pointer // pointer to some memory allocated with C.malloc
    ...
}

During the lifetime of a T, we may change buf several times, each time with a free/malloc pair. When the T goes dead, we want to free the last value that was in buf.

How do I write a cleanup for that? I think I would need to Stop/AddCleanup each time I updated buf. I don't see a way to set a cleanup when T is allocated that does the right thing. Unless buf is indirect somehow. But maybe that's the intention, that you have to Stop/AddCleanup each time?

Ah, that's interesting. Yeah, you would need to Stop/AddCleanup each time. I don't see a way around that (unless, like you say, you add an indirection on buf, and pass that to the AddCleanup), and having to do that makes the malloc/free somewhat more expensive compared to what you can do with finalizers.

I'm inclined to say that requiring the indirection isn't that bad. It's going to result in an additional allocation for each T that exists, but that allocation will be bound to the lifetime of T. In other words, it's kinda like extending T by 8 bytes. And I think the fact that you can release T's memory immediately (as opposed to waiting an extra GC cycle) may actually make AddCleanup win out in the long run.

@randall77
Copy link
Contributor

The indirect scheme is basically the same thing you would do with finalizers if, e.g., the T's were in a cycle. You would allocate a child object, point T to it, put buf in the child object, and put a finalizer on the child object.

Having the option to Stop/AddCleanup each time instead of using the indirection technique might be useful.

I'm not sure I'm arguing for or against anything at this point. Just trying to understand.

@RogerDilmer
Copy link

What about deferCleanup as a name. Defer is already very understandable by Go developers (albeit in a different context). Add doesn't really say anything about when it happens.

@bjorndm
Copy link

bjorndm commented May 21, 2024

Ruby does something similar for finalizers. Basically the finalizer is run after the object is deallocated. They are also guaranteed to run if the object is deallocated. The Ruby finalizer is not allowed to reference the object that it finalizes. In stead you have to reference to any members through a closure. In my experience this works better than Go finalizers. I would recommend Go adopts the Ruby way of implementing finalizers.

https://ruby-doc.org/core-3.0.0/ObjectSpace.html

@rsc rsc changed the title proposal: runtime: add AddCleanup and deprecate SetFinalizer proposal: runtime: add AddCleanup and deprecate SetFinalizer Jun 26, 2024
@rsc
Copy link
Contributor

rsc commented Jun 27, 2024

Here is a nice (imho) use of AddCleanup where SetFinalizer would not be okay: #67552 (comment).

@rsc
Copy link
Contributor

rsc commented Jul 25, 2024

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@perj
Copy link

perj commented Jul 25, 2024

The SetFinalizer documentation specifies that the function runs in a new go routine, I think this API documentation should also specify that (as long as that is indeed the case).

@adonovan
Copy link
Member

The SetFinalizer documentation specifies that the function runs in a new go routine, I think this API documentation should also specify that (as long as that is indeed the case).

Indeed. Read sec 3.5 of Destructors, Finalizers, and Synchronization, in which Hans Boehm argues that finalizers must be asychronous, for the rationale.

@mknyszek
Copy link
Contributor Author

mknyszek commented Jul 31, 2024

@perj This is maybe a little pedantic, but it's not quite true that it runs in a new goroutine. :) From the SetFinalizer docs (emphasis mine):

... When the garbage collector finds an unreachable block with an associated finalizer, it clears the association and runs finalizer(obj) in a separate goroutine. ...
...
A single goroutine runs all finalizers for a program, sequentially. If a finalizer must run for a long time, it should do so by starting a new goroutine.

I agree that we should copy this part of the docs to AddCleanup, as I was fully intending for the implementation to reuse this mechanism. I updated the proposal to include this text.

We do have the option here of changing the behavior of cleanups to always start on a new goroutine. It's more user-friendly (in that blocking in a cleanup function won't delay other cleanups), but may result in greater resource costs. I do think a valid question is whether we want to retain the existing sequential behavior.

Perhaps one middle-ground is to relax the constraints and mostly reuse the same goroutine, but if a cleanup goroutine blocks, then we start a new one. My only concern with this is that data races between cleanup functions may be much harder to detect if they're not fully asynchronous by default.

@mateusz834
Copy link
Member

@mknyszek If a new goroutine is desired for a finalizer it can always go func, so it might not be a huge issue.

@mknyszek
Copy link
Contributor Author

mknyszek commented Jul 31, 2024

Just to summarize, here are the three choices a described above as well as their pros/cons. There may be more options.

Guaranteed sequential execution of cleanups (What SetFinalizer does, and what is currently proposed.)

  • Pros:
    • No chance for data races between cleanups.
    • Minimal resource overheads (one goroutine only).
  • Cons:
    • One cleanup may hold up other cleanups. User must explicitly create a new goroutine.

Cleanups always run in a new goroutine

  • Pros:
    • Maximizes chance to detect races between cleanups.
    • No cleanup may hold up any other cleanup.
  • Cons:
    • Creating a new goroutine per cleanup can be expensive if there are many cleanups. Often, the extra concurrency is unnecessary (for example, closing a file).

Cleanups may run in a new goroutine

  • Pros:
    • No cleanup may hold up any other cleanup.
    • Minimal resource overheads (exactly as much concurrency as you need).
  • Cons:
    • Low chance to detect races between cleanups.

@rsc
Copy link
Contributor

rsc commented Jul 31, 2024

SetFinalizer has many footguns but I don't think I've seen many people complain about the fact that one can block others. It seems okay to leave just a single goroutine. I did a tiny bit of wordsmithing in the comment.

Reading the SetFinalizer comment, there is a lot of good advice and caveats that seem to carry over (zero-size allocations, small pointer-free allocations, globals, not using them for flushing buffers, how to use KeepAlive properly, memory model interactions). Let's not hash them out in the proposal process, but the real implementation's doc comment should probably carry them forward.

@rsc
Copy link
Contributor

rsc commented Aug 14, 2024

Have all remaining concerns about this proposal been addressed?

The details are in the top comment under the "Proposal" heading.

@mknyszek
Copy link
Contributor Author

I think the only remaining question is whether cleanups should run before or after finalizers when both are present. @Merovius made a case for running cleanups after any attached finalizer completes, so that they can rely on the object they're attached to being truly dead. If there are no finalizers present, only cleanups and/or weak pointers, then the cleanups still get to run immediately and everything is great.

This is different from the semantics proposed for weak pointers, which are to go nil the first time an object becomes unreachable since the weak pointer was created. Meanwhile, cleanups would track object resurrection, but not resurrect objects themselves.

So far, I've been unable to come up with a situation where running cleanups after finalizers would be problematic, but I think there is a case to be made that running cleanups before finalizers would be problematic. But, I suspect cleanups don't have the same problem as weak pointers with respect to tracking resurrection, because there's no way for cleanups to reference the object they're attached to.

So, I guess I'm inclined to just change that one part of the proposal. If anyone has additional thoughts, I'd appreciate them. I'll update the proposal at the top of this issue.

@ianlancetaylor
Copy link
Contributor

I don't have an opinion, but I want to at least raise another possibility: calling SetFinalizer and AddCleanup on the same object panics.

Both of these functions are touchy in practice, and in the common case of releasing some external resource require careful use of runtime.KeepAlive (for example see all the KeepAlive calls sprinkled around the os and net packages). Since people can't use them without careful attention to all uses of the object, it's difficult to imagine any case where somebody might want both finalizers and cleanups.

@dominikh
Copy link
Member

ISTM that such a restriction would make it dangerous to use AddCleanup on objects not "owned" by the caller, because they cannot control whether SetFinalizer has been called on the objects or not.

@ianlancetaylor
Copy link
Contributor

It's dangerous in any case to use AddCleanup on objects that you don't own.

@dominikh
Copy link
Member

But the proposal explicitly states

Its definition also allows attaching cleanup functions to objects the caller does not own

and #67552 (comment) demonstrates one such use case.

@mknyszek
Copy link
Contributor Author

I agree with @dominikh, part of the point of AddCleanup is that it enables adding it to objects you don't own.

Ignoring finalizers for a moment, I don't see a reason why this would be a problem. With finalizers, the chance of object resurrection and catching the object in an inconsistent state is a real problem. But you can't actually reference the object from a cleanup function. You'd have to capture some of its state ahead of time to actually have any meaningful impact on it.

Also, I think it's important that cleanups compose with finalizers, specifically for examples like #67552 (comment) to be able to work. There are plenty of existing objects with finalizers; it seems too restrictive to me to prevent objects from having both.

@ianlancetaylor
Copy link
Contributor

It depends on what the cleanup does.

It's a fair point that adding a cleanup to an object that you don't own is OK as long as the cleanup only affects data that you do own, and that the object doesn't know about.

But if cleanups are used for data that the object does know about, then that data may be modified in ways that the object doesn't expect. A perhaps-obvious example: if an object has associated memory allocated by C, and uses a cleanup to free that C memory, then if the final use of the object is to call a C function, it is possible for the garbage collector to free the memory while the C function is still using it. That may sound esoteric, but in fact it happens all the time for people using SWIG.

Today there are objects with finalizers. But unless there is a reason that people might prefer finalizers to cleanups, we can encourage people to move to cleanups. I certainly have no objection to deciding how finalizers and cleanups should cooperate. But I don't see any reason to expect that that will be a real problem for very many people.

@mknyszek
Copy link
Contributor Author

I agree completely. Maybe the only thing I'd add to the proposal is that we should explicitly state in the documentation that it's bad form (and likely to be buggy) for a cleanup on an object you don't own to affect that object. That won't prevent misuse, but it's better than nothing.

@mknyszek
Copy link
Contributor Author

FYI @RLH posted about a channel-based cleanup API in the weak pointers proposal: #67552 (comment)

My reply there: #67552 (comment).

@RLH
Copy link
Contributor

RLH commented Aug 26, 2024

Moving channel based conversation here.

Upon further reflection one can achieve a channel based approach simply by having the AddCleanup's function do nothing but put the value on a channel and pass it along to a goroutine under user control.

@dezzeus
Copy link

dezzeus commented Aug 28, 2024

It's not totally clear to me if this allows the following:
let's say that I have a struct with a pointer to the handler of an external resource; am I able to register a cleanup function that (when there are no more references to such struct values) uses the very last struct reference, retrieve the handler and call the Close() method on it to dispose of the external resource ? If it's allowed, is it guaranteed to be executed ?

@linkdata
Copy link

If it's allowed, is it guaranteed to be executed ?

Neither AddCleanup nor SetFinalizer is guaranteed to execute.

@dezzeus
Copy link

dezzeus commented Aug 28, 2024

If it's allowed, is it guaranteed to be executed ?

Neither AddCleanup nor SetFinalizer is guaranteed to execute.

Even if I force a call to runtime.GC() ?

@ianlancetaylor
Copy link
Contributor

Neither AddCleanup nor SetFinalizer is guaranteed to execute.

Even if I force a call to runtime.GC() ?

Yes, even so. No guarantees.

let's say that I have a struct with a pointer to the handler of an external resource; am I able to register a cleanup function that (when there are no more references to such struct values) uses the very last struct reference, retrieve the handler and call the Close() method on it to dispose of the external resource ?

My understanding of what you wrote and of AddCleanup is that this will not work. As the proposal above says:

ptr must not be reachable from the value passed to the cleanup function, or from the cleanup function

In other words, if the struct references have cleanups, the cleanup function that you register can't look at any of those struct references.

@rsc
Copy link
Contributor

rsc commented Aug 28, 2024

Sounds like we've converged on cleanups after finalizers because resurrection subtleties are handled much better that way.

@rsc
Copy link
Contributor

rsc commented Aug 29, 2024

Have all remaining concerns about this proposal been addressed?

The details are in the top comment under the "Proposal" heading.

@dezzeus
Copy link

dezzeus commented Aug 29, 2024

ptr must not be reachable from the value passed to the cleanup function, or from the cleanup function

In other words, if the struct references have cleanups, the cleanup function that you register can't look at any of those struct references.

Just to understand, can it actually be enforced ? if one misbehaves and writes something like the following, what would be the expected behavior ?

var ptr *T
var anything S
disposer := func(_ S) { ptr.Dispose() }
runtime.AddCleanup(ptr, disposer, anything)

@Merovius
Copy link
Contributor

@dezzeus It's in the godoc comment from the proposal:

If ptr is reachable from cleanup or arg, ptr will never be collected and the cleanup will never run.

@dezzeus
Copy link

dezzeus commented Aug 29, 2024

Sorry, my bad; actually I was expecting it, but I was also envisioning other possibilities enforced by either the compiler or the runtime.

@ianlancetaylor
Copy link
Contributor

It might be possible to implement some sort of checking mode in which we detect whether the pointer is reachable from the cleanup. But that's not part of this proposal, and I doubt that it would ever be the default behavior.

@mknyszek
Copy link
Contributor Author

mknyszek commented Aug 29, 2024

Not to veer too far off-topic, but we may be able to leverage GODEBUG=gccheckmark=1 mode to create such a checking mode, I think fairly easily. Because that debug mode exists, we have the infrastructure for separate, resettable mark state for each object. In theory, the way this could work is:

  • At the end of each GC cycle, during the STW, enable checkmarks.
  • Unlike checkmark mode, do not reset the mark state of the roots.
  • Iterate over all the cleanup functions in the program, calling runtime.scanobject on their closure pointers and their args.
  • Call runtime.gcDrain as checkmark mode does now.
  • Iterate over all the objects in the program with cleanup functions and check if their checkmark bits are set.
    • If any are set, report the objects as unreclaimable and exit.
  • Disable checkmarks, reset checkmark state.

We could also, just in this mode, take a stack trace each time a cleanup function is created to help guide users to the root of the problem. It occurs to me that we could also do something similar for finalizers.

At face value, this is slow, especially since it's during a STW, but I suspect the vast majority of cleanup functions will have a very shallow object graph. Simultaneously, it's just a debugging tool, and slow is better than nothing here. It's also possible I'm missing some subtlety that makes this not quite work.

@rsc
Copy link
Contributor

rsc commented Sep 4, 2024

Based on the discussion above, this proposal seems like a likely accept.
— rsc for the proposal review group

The details are in the top comment under the "Proposal" heading.

@rsc
Copy link
Contributor

rsc commented Sep 11, 2024

No change in consensus, so accepted. 🎉
This issue now tracks the work of implementing the proposal.
— rsc for the proposal review group

The details are in the top comment under the "Proposal" heading.

@rsc rsc changed the title proposal: runtime: add AddCleanup and deprecate SetFinalizer runtime: add AddCleanup and deprecate SetFinalizer Sep 11, 2024
@rsc rsc modified the milestones: Proposal, Backlog Sep 11, 2024
@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Sep 11, 2024
@DmitriyMV
Copy link
Contributor

DmitriyMV commented Oct 23, 2024

Is this planned for Go 1.234?

@mknyszek mknyszek modified the milestones: Backlog, Go1.24 Oct 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. Proposal Proposal-Accepted
Projects
Status: Accepted
Development

No branches or pull requests