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

proposal: sync/v2: new package #71076

Open
ianlancetaylor opened this issue Jan 1, 2025 · 96 comments
Open

proposal: sync/v2: new package #71076

ianlancetaylor opened this issue Jan 1, 2025 · 96 comments
Labels
Proposal v2 An incompatible library change
Milestone

Comments

@ianlancetaylor
Copy link
Member

ianlancetaylor commented Jan 1, 2025

Proposal Details

The math/rand/v2 package has been successful. Let's consider another v2 package: sync/v2.

This is an update of #47657.

Background

The current sync package provides Map and Pool types. These types were designed before Go supported generics. They work with values of type any.

Using any is not compile-time-type-safe. Nothing prevents using a key or value of any arbitrary type with a sync.Map. Nothing prevents storing a value of any arbitrary type into a sync.Pool. Go is in general a type-safe language. For all the reasons why Go map and slice types define the their key and element types, sync.Map and sync.Pool should as well.

Also, using any is inefficient. Converting a non-pointer value to any requires a memory allocation. This means that building a sync.Map with a key type of string requires an extra allocation for every value stored in the map. Storing a slice type in a sync.Pool requires an extra allocation.

These issues are easily avoided by changing sync.Map and sync.Pool to be generic types.

While we can't change Map and Pool to be generic in the current sync package because of compatibility concerns (see the discussion at #48287), we could consider adding new generic types to the existing sync package, as proposed at #47657. However, that will leave us with a confusing wart in the sync package that we can never remove. Having both sync.Pool and (say) sync.PoolOf is confusing. Deprecating sync.Pool still leaves us with a strange name for the replacement type.

Introducing a sync/v2 package will permit us to easily transition from the current package to a new package. It's true that future users will have to know to import "sync/v2" rather than "sync". However, few people write their own imports these days, and this transition is easily handled by goimports and similar tools.

Proposal

In the sync/v2 package, the following types and functions will be the same as in the current sync package:

func OnceFunc(f func()) func()
func OnceValue[T any](f func() T) func() T
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
type Cond struct{ ... }
    func NewCond(l Locker) *Cond
type Locker interface{ ... }
type Mutex struct{ ... }
type Once struct{ ... }
type RWMutex struct{ ... }
type WaitGroup struct{ ... }

The existing Map type will be replaced by a new type that takes two type parameters. Note that the original Range method is replaced with an All method that returns an iterator.

// Map is like a Go map[K]V but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// ...and so forth
type Map[K comparable, V any] struct { ... }

// Load returns the value stored in the map for a key, or the zero value if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map[K, V]) Load(key K) (value V, ok bool)

// Store sets the value for a key.
func (m *Map[K, V]) Store(key K, value V)

// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool)

// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool)

// Delete deletes the value for a key.
func (m *Map[K, V]) Delete(key K)

// Swap swaps the value for a key and returns the previous value if any.
// The loaded result reports whether the key was present.
func (m *Map[K, V]) Swap(key K, value V) (previous V, loaded bool)

// CompareAndDelete deletes the entry for key if its value is equal to old.
// This panics if V is not a comparable type.
//
// If there is no current value for key in the map, CompareAndDelete
// returns false.
func (m *Map[K, V]) CompareAndDelete(key K, old V) (deleted bool)

// CompareAndSwap swaps the old and new values for key
// if the value stored in the map is equal to old.
// This panics if V is not a comparable type.
func (m *Map[K, V]) CompareAndSwap(key K, old, new V) (swapped bool)

// Clear deletes all the entries, resulting in an empty Map.
func (m *Map[K, V]) Clear()

// All returns an iterator over the keys and values in the map.
// ... and so forth
func (m *Map[K, V]) All() iter.Seq2[K, V]

// TODO: Consider Keys and Values methods that return iterators, like maps.Keys and maps.Values.

The existing Pool type will be replaced by a new type that takes a type parameter.

2025-01-04: The new version of Pool does not have an exported New field; instead, use NewPool to create a Pool that calls a function to return new values.

Note that the original Get method is replaced by one that returns two results, with the second being a bool indicating whether a value was returned. This is only useful if the New field is optional: we could also change this to require the New field to be set, or to provide a default implementation that returns the zero value of T.

// A Pool is a set of temporary objects of type T that may be individually saved and retrieved.
// ...and so forth
type Pool[T any] struct {
    ...
}

// NewPool returns a new pool. If the newf argument is not nil, then when the pool is empty,
// newf is called to fetch a new value. This is useful when the values in the pool should be initialized.
func NewPool[T any] (newf func() T) *Pool[T]

// Put adds x to the pool.
func (p *Pool[T]) Put(x T)

// Get selects an arbitrary item from the Pool, removes it from the
// Pool, and returns it to the caller.
// ...and so forth
//
// If Get does not have a value to return, and p was created with a call to [NewPool] with a non-nil argument,
// Get returns the result of calling the function passed to [NewPool].
func (p *Pool[T]) Get() T
@ianlancetaylor ianlancetaylor added this to the Proposal milestone Jan 1, 2025
@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Jan 1, 2025
@gabyhelp
Copy link

gabyhelp commented Jan 1, 2025

@mateusz834
Copy link
Member

There is one issue I’ve encountered with the current sync.Pool is that the New func() field is exported. This makes it easy to accidentally call New directly instead of using the Get method. Maybe we want to hide it somehow?

How the Pool is going to behave with non-pointers? Will Put() cause allocation for let's say []byte, instead of *[]byte

@Jorropo
Copy link
Member

Jorropo commented Jan 1, 2025

Part of me wants to change to:

type CondLocker[T any] interface {
	Locker
	*T
}

type Cond[T any, L CondLocker[T]] struct {
	L T
}

// Random example pseudo code to prove this is exploitable
func (c *Cond[T, L]) Test() {
	var p L = &c.L
	p.Lock()
	p.Unlock()
}
// ...

this makes the zero value of a cond valid if the locker's zero value is valid (like Mutex),
it also allows to remove 2 pointers from Cond,
and makes it easier for the compiler to remove a virtual dispatch inside Cond.

Other part of me thinks this is too much #darkarts,
is not easily retro compatible with the old Locker interface approach,
and looks a lot like the painful parts of rust.

@Merovius
Copy link
Contributor

Merovius commented Jan 1, 2025

I'm wondering if there's even a need to make Cond polymorphic. Are there any sync.Locker implementations out there that are not just *Mutex or *RWMutex? That is, would it perhaps be better to just make it Cond struct{ L Mutex }, or have two types, or make it Cond[Locker Mutex|RWMutex] struct{ L Locker }?

I'm not familiar with the use cases for Cond (I have literally never used it). But part of that is the necessity of providing a Locker explicitly, which confused me as to how it was intended to be used.

@thepudds
Copy link
Contributor

thepudds commented Jan 1, 2025

I don't fully understand this piece:

type Map[K comparable, V any] struct { ... }

[...]

// CompareAndDelete deletes the entry for key if its value is equal to old.
// This panics if V is not a comparable type.
//
// If there is no current value for key in the map, CompareAndDelete
// returns false.
func (m *Map[K, V]) CompareAndDelete(key K, old V) (deleted bool)```

For panicking if V is not a comparable type and doing the equality check if V is a comparable type —
is that implementable in ordinary Go code in the current version of the compiler without resorting to reflect?

Is the intent of this proposal to use reflect, or some stdlib-only compiler or runtime magic, or would this be contingent on #45380 or some other generics enhancement?

In #47657 (comment), Ian had written:

One possibility is to permit generic types to have methods that use different, tighter, constraints. Those methods would only be available if the type argument(s) satisfied those constraints. I don't know whether that would be a good idea or not.

Separately, @Merovius pointed out in #47657 (comment):

I'll note that if we adopted #65394, you could have the constraints on Map be [K comparable, V any] and the constraints on CompareAndSwap be [K, V comparable] - that is, you would get those methods if and only if the value type is comparable as well. Having type-safety and flexibility.

... but I'm not sure what solution this proposal is relying upon (or maybe I misunderstood the issue 😅).

@Merovius
Copy link
Contributor

Merovius commented Jan 1, 2025

is that implementable in ordinary Go code in the current version of the compiler without resorting to reflect?

Just realized: A trivial way to get those semantics is any(v1) == any(v2). But still, I'd strongly prefer a type-safe solution.

@jub0bs
Copy link

jub0bs commented Jan 1, 2025

This is exciting!

Should Cond even be retained, though? @bcmills cogently lobbies for its deprecation. I'd rather see the inclusion of something like https://github.com/neild/gate.

@ianlancetaylor
Copy link
Member Author

@mateusz834

There is one issue I’ve encountered with the current sync.Pool is that the New func() field is exported. This makes it easy to accidentally call New directly instead of using the Get method. Maybe we want to hide it somehow?

I think the goals here include

  • zero value of sync.Pool should be useful
  • some sort of "new" function is desirable for pooled types that requires initialization
  • it should be easy to use sync.Pool as a package-scope variable

When those goals are put together, it seems hard to avoid having sync.Pool be a struct with an exported New field.

I am definitely open to suggestions.

How the Pool is going to behave with non-pointers? Will Put() cause allocation for let's say []byte, instead of *[]byte

I'm not specifying an implementation, but looking at the current implementation it seems straightforward for the v2 sync.Pool to use values of type T rather than type any, so I don't see why a Pool[[]byte] would allocate on Put. The main change is that we'll need to store an additional array of bool values in the deque to indicate whether a slot holds a valid value—the current code can use nil for an invalid value.

@ianlancetaylor
Copy link
Member Author

I think we could in principle drop Cond from sync/v2 if we made it available in some other package, perhaps in x/sync. I don't know whether that would be a good idea or not. I note that the current implementation of sync.Cond relies on runtime package support.

I think that in practical use a Cond should not incorporate a Locker, it should only refer to one. Condition variables are only useful in conjunction with some sort of mutex, but code will often want to use that mutex in other ways. For example, see connReader in the net/http package. It has a cond field that uses the mu field, but the code also uses the mu field in various ways that don't touch the cond field at all. Overloading the mutex into the condition variable will make this code more obscure.

If we were designing from scratch it might make sense to make the Cond simply use a Mutex, but it would not be surprising if there are Cond variables out there that use a RWMutex, and it seems unnecessary to break them.

@seankhliao
Copy link
Member

since the original sync package will continue to be available with Cond, do we really need to have another copy elsewhere?

@ianlancetaylor
Copy link
Member Author

@thepudds What I'm trying to express in CompareAndDelete and CompareAndSwap is isomorphic to the current documentation, which says "The old value must be of a comparable type." And as @Merovius says we can use any(v1) == any(v2), which will panic if the values are not comparable. It's pretty horrible but I don't see another way to preserve the current semantics.

@neild
Copy link
Contributor

neild commented Jan 1, 2025

When those goals are put together, it seems hard to avoid having sync.Pool be a struct with an exported New field.

We could have a NewPool function that initializes a pool with a constructor function.

// NewPool returns a Pool that uses newf to construct new objects.
func NewPool[T any](newf func() T) *Pool[T]
  • Zero value Pool can behave as today.
  • No harder to construct a pool with a New function than it is now.
  • Package-scope variable initialization works. (var pool = NewPool(func() T { ... }))
  • Can't accidentally call Pool.New instead of Pool.Get.
  • Can't change the New function after pool construction, but that seems like a benefit rather than a limitation.

@ianlancetaylor
Copy link
Member Author

@seankhliao

since the original sync package will continue to be available with Cond, do we really need to have another copy elsewhere?

I think a goal here should be that people can, with a little bit of work, change from using sync to using sync/v2. We don't want a state in which people are using both the sync and sync/v2 package with no prospect of ever moving completely to the sync/v2 package.

@neild
Copy link
Contributor

neild commented Jan 1, 2025

I don't think we should change Pool.Get to return an additional bool.

A Pool must return a pointer or pointer-containing object to be useful. (A Pool is a pool of references to temporary objects. Since a Pool doesn't report when objects are evicted, those references must be garbage collected, and therefore must be pointers.)

If the pooled objects contain pointers, it is possible to distinguish between the zero value reference and an initialized reference.

Therefore, there is no need for Get to report whether it found an object. It can return the zero value if nothing is found, and the caller can check for the zero value if they care. Most callers won't care, and can use the result of Get directly.

@jub0bs

This comment has been minimized.

@DmitriyMV
Copy link
Contributor

@jub0bs this will force people to store pointers to the slices which was quite a big problem with the original Pool.

@dsnet
Copy link
Member

dsnet commented Jan 2, 2025

@neild's comment echos prior comments, which seems to be supported by various others.

As some empirical experience supporting dropping ok from sync.Pool.Get, we have syncs.Pool at Tailscale, which is a generic wrapper over the v1 sync.Pool. Our implementation drops ok, and we have not missed having it in our various usages throughout our codebase.

@mateusz834
Copy link
Member

mateusz834 commented Jan 2, 2025

Incidentally, wouldn't the advent of v2 be a good opportunity to enforce at compile time that pointers be used?

What about pooling maps? They are internally pointers, now we would have unnecessary allocations?

@jub0bs
Copy link

jub0bs commented Jan 2, 2025

Before considering folding golang.org/x/sync/errgroup into sync/v2, its API should be improved. Relevant issue (and comments): #57534.

@earthboundkid
Copy link
Contributor

Could it be type Pool[P ~[]T | ~*T, T any] then? Maps are an issue, but I don't see the harm in those being pointed to instead of direct, since it's a fairly uncommon need.

I'm in favor of dropping Cond fwiw. It's so misunderstood, the documentation for context.AfterFunc originally misused it.

@earthboundkid
Copy link
Contributor

Could it be type Pool[P ~[]T | ~*T, T any] then? Maps are an issue, but I don't see the harm in those being pointed to instead of direct, since it's a fairly uncommon need.

Playing with it on the playground, this is legal syntax, but the type inference requires both type parameters, which is unfortunate.

@Merovius
Copy link
Contributor

Merovius commented Jan 2, 2025

What exactly is the issue with just making it type Pool[T any]? What's the failure mode we are trying to address? It seems to me, at worst it's doing nothing. And presumably, if you are trying to use a sync.Pool, you do so to fix a problem and you'll figure out pretty quickly that it doesn't actually get fixed.

I also don't think there is a reason to forbid struct{ x A; b *B } from being put into a pool, to save allocations of Bs.

Restricting that does, IMO, nothing to help, but is unnecessarily restrictive.

@DmitriyMV
Copy link
Contributor

Could it be type Pool[P ~[]T | ~*T, T any] then?

That would prevent reusing hash.Hash instances, which is expected in cases where you reach for sync.Pool.

Maps are an issue, but I don't see the harm in those being pointed to instead of direct, since it's a fairly uncommon need.

There are other types which have pointers inside.

@earthboundkid
Copy link
Contributor

The doubleDeferSandwich is complex, but if you just use wg.Add(1)+go+defer wg.Done(), by default a panic will kill the whole application because nothing catches it. ISTM, a reasonable implementation is that panics should propagate up to the wg.Wait() call and runtime.Goexit() calls should just pass silently.

@ianlancetaylor
Copy link
Member Author

@nalgeon Fair enough, I do see that the rationale is not really expressed in this proposal. I just edited the proposal to add a background section explaining why I think it's a good idea. Thanks for pointing that out.

@ianlancetaylor
Copy link
Member Author

@valyala

I suppose you are going to create sync/v3 when it will appear that sync/v2 misses same "important" functionality, which cannot be introduced without breaking backwards compatibility.

Why do you suppose that? I think that you are trying to make a slippery slope argument, but a slippery slope requires showing that the facts leading to the first step are going to lead to future steps. In this case the first step is being driven by the introduction of generics into the language. If no comparable change is introduced in the language in the future, then there is no reason for us to ever create a sync/v3 package. Conversely, if a change as large as generics is added to Go in the future (which seems unlikely), then, you're right, it may be appropriate to add a sync/v3. But it's still not a slippery slope: it's a reaction to changing circumstances.

@mohamedattahri
Copy link

@valyala I'd add that it took 23 versions of Go before we got to v2s of standard library packages, which empirically proves a certain restraint.

@aohoyd
Copy link

aohoyd commented Jan 28, 2025

Having both sync.Pool and (say) sync.PoolOf is confusing

Why having both sync and sync/v2 is not confusing then?

@Merovius
Copy link
Contributor

@aohoyd

It's true that future users will have to know to import "sync/v2" rather than "sync". However, few people write their own imports these days, and this transition is easily handled by goimports and similar tools.

@aohoyd
Copy link

aohoyd commented Jan 29, 2025

@Merovius

Why Map and MapOf are confusing for you but sync and sync/v2 are not? By the same logic, users can always use MapOf instead of Map.

@ianlancetaylor
What about sync/atomic? What it will be then? sync/v2/atomic or sync/atomic/v2?

@thepudds
Copy link
Contributor

Hi @aohoyd, please see “Principles for evolving the Go standard library” if you haven’t already, which includes:

a new, incompatible version of a package will use that/package/v2 as its import path, following semantic import versioning

My understanding is this particular proposal is about the sync package, which is a different package than sync/atomic, and under this particular proposal sync/atomic would remain sync/atomic.

If sync/atomic at some point later evolved to a v2 package, my understanding is it would be sync/atomic/v2 (as suggested by the blog post quote above) and not sync/v2/atomic. I think a similar question came up late in the math/rand/v2 discussion about whether it should be math/v2/rand instead, but the conclusion there was math/rand/v2 was better,

@Merovius
Copy link
Contributor

Merovius commented Jan 29, 2025

@aohoyd The quoted section specifically explains the difference (and it has been explained a couple of times during this discussion). Simply repeating your question is not helpful. If you have further questions, please refer to that section to explain why you are dissatisfied with the explanation.

Though to be clear, I don't personally find it "confusing". I just think it's a bad name and I don't want to read bad names. And note that the type names appear more often and they appear in regular code (so they actually will be read, unlike import paths) and so having a good name for those is more important.

@icholy

This comment has been minimized.

@matttproud
Copy link
Contributor

matttproud commented Feb 2, 2025

@jub0bs in #71076 (comment):

This is exciting!

Should Cond even be retained, though? @bcmills cogently lobbies for its deprecation. I'd rather see the inclusion of something like https://github.com/neild/gate.

Without having looked too closely in how package gate is implemented, one thing I could potentially see this approach lacking is a distinction between wake-one (cf. (*sync.Cond).Signal) versus wake-all (cf. (*sync.Cond).Broadcast), which is useful when only one waiting goroutine can act on a change. This can make a huge difference in performance critical systems and middlewares, as using wake-all here leads to spurious wakeups of other goroutines that cannot act.

So I'd be loathe to suggest that condition variables are removed from the API unless something remains to efficiently handle these two wakeup cases.

@ianlancetaylor

This comment has been minimized.

@jub0bs
Copy link

jub0bs commented Feb 3, 2025

@matttproud True, package neild/gate does lack a wake-all functionality. But I'm confused by your comment. Doesn't wake-all cause spurious wakeups precisely when not all goroutines can act on a change?

@matttproud
Copy link
Contributor

matttproud commented Feb 3, 2025

Both forms of wakeup broadcast (wake-one, wake-all) can cause spurious waking. The question is the anticipated blast radius of the notification: wake-one could misnotify maximally one waiting goroutine, and wake-all could misnotify all waiting goroutines. Sometimes when designing a system that requires a condition variable, you know per the system's architecture (a priori) that the event associated with the variable should only wake up maximally one waiting (goroutine, thread, you-name-it). In that case, having the wake-one semantic is very helpful.

From what I could tell, package gate only had wake-all semantics, not wake-one. Looking at how (*sync.Cond).Signal is implemented, it appears to take advantage of runtime-internal APIs to handle wake-one. Not sure whether an external package would be able to have access to this.

Tangent:

What does intrigue me with package gate is that it does support — if not partially — context awareness. I would be interested potentially whether parts of package sync (or a future version thereof) could gain context awareness in places where operations are otherwise uninterruptible. Some examples:

  1. try to lock within bounds of a context's not being canceled
  2. (*sync.WaitGroup).Wait subject to a context's not being canceled

Some of these things can be implemented today with wrappers, but I suspect suboptimally due to needing to use an extra separate goroutine in certain cases. I realize that creating a linkage between package context and package sync is a touchy subject, but I think this should be explored.

@neild
Copy link
Contributor

neild commented Feb 3, 2025

github.com/neild/gate has wake-exactly-the-right-number-of-times semantics, and does not have spurious wakeups. Unlike a sync.Cond, it's level-triggered rather than edge-triggered: When you unlock a gate you set its state bit. Setting the bit to true wakes exactly one goroutine (if any) waiting on the bit and relocks the gate. When the woken goroutine unlocks the gate, it will either wake another waiter or not depending on the new state of the bit.

I've found it to be an immensely useful synchronization construct, but I think it's a distraction in this discussion: The implementation of gate is trivial and uses nothing but standard channel operations, so it doesn't need to be in std. It's not a drop-in replacement for a sync.Cond, so adding it to the sync package wouldn't let us remove Cond. I could get behind an argument that most uses of sync.Cond should use a gate instead, but even if we had consensus on that point of view (which I doubt we have) we still wouldn't deprecate sync.Cond.

I would be interested potentially whether parts of package sync (or a future version thereof) could gain context awareness in places where operations are otherwise uninterruptible.

The sync package is lower-level than the context package.

We could change that, introducing an internal/sync that both sync and context depend on, but I think that would just blur the lines on what the sync package is: Low-level synchronization primitives which require runtime support to implement.

@erincandescent
Copy link

There was some talk about changing the WaitGroup API to make it harder to misuse. I understand this, but think its a bit misguided. The problem isn't that WaitGroup (which is fundamentally low-level plumbing) is hard to use; the problem is that it's the only tool provided by the standard library

I think most people reaching for WaitGroup are better served by an errgroup.Group but that's in golang.org/x/sync not sync. And even that has the limitation that it doesn't catch panics, which it plausibly should (in pretty much every case where I've used errgroup what I really want is for panics from the spawned routines to be caught and either be re-emitted from .Wait() or turned into an error)

But I don't think that WaitGroup could move into to sync/v2 without causing circular dependencies :/

@earthboundkid
Copy link
Contributor

My takeaway from #18022 is that misuse of WaitGroups is common and it is difficult to write a proper vet check to prevent it. My preferences in order then would be:

  • Add .Go to WaitGroup v1 to help people transition. Create sync/v2 WaitGroup without Add/Done and with just .Go. Add generic Map and Pool to v2.
  • If we don't do that, add .Go to sync/v1 and add MapOf and PoolOf to v1.
  • If we don't do that, add v2 with generic Map and Pool but no other changes (the original proposal).

I rank the original proposal third in my set of preferences because I really don't see the point of adding v2 just to improve two names that aren't even that bad if we're not going to also fix the other problems that can't be fixed without breaking the API.

To me, if all we cared about was better names, we would replace the "WithContext" variations in every package that had context support added later with a v2. I don't think anyone thinks that would be a good idea. If v2 is worth doing it's worth doing because there's an API with a subtle flaw that can only be fixed by breaking compatibility, a la math/rand where there needed to be a whole new interface for seeding. The Source vs Source64 problem wasn't enough. It was that there was just no way to add a new algorithm as it was. In this case, the .Add/.Done API is a flaw based on the number of misuses seen in the wild.

Changing Cond also strikes me as the sort of thing that would be good for sync/v2, but I don't have a proper opinion about how to do that because I've never used it.

@TopherGopher

This comment has been minimized.

@Merovius
Copy link
Contributor

Merovius commented Feb 5, 2025

@TopherGopher

Given the (somewhat severe) controversy and contention around using "v2", what about if we go for a new name all together? E.g. sync/generics or genericsync or gsync.

Package names, just like type names, appear in regular code and are being read regularly. They should be good and all of these are worse than sync. That is, the argument against this is ultimately the same as the argument against MapOf/PoolOf.

@cespare
Copy link
Contributor

cespare commented Feb 5, 2025

@TopherGopher I don't see why we'd create a new name. The people who don't like this proposal seem to be opposed to creating a new package in general, not a v2 package in particular.

Also, a v2 package sends a clear signal: it indicates that new code should use the v2 package and that the v1 package is now an outdated version. That's what we want here. We don't want two different packages. We want a newer version of the existing package.

On a personal note, I use VS code. Anytime there's a version in a package name, I always have to do the import manually. Other imports work automatically because I can just use the package name, but I have to alias any "v*" names, so I always groan when I see a v. It's caused us to use the wrong package previously on multiple occasions for non-std modules we consume.

I think it would be worth getting to the bottom of these tooling issues. Going forward you should expect more v2 packages, not fewer. FWIW gopls handles them pretty well for me.

@TopherGopher

This comment has been minimized.

@Merovius

This comment has been minimized.

@mikeschinkel

This comment has been minimized.

@mikeschinkel

This comment has been minimized.

@Merovius

This comment has been minimized.

@mikeschinkel

This comment has been minimized.

@seankhliao
Copy link
Member

please try not to rehash past discussions / proposals

@erincandescent
Copy link

My takeaway from #18022 is that misuse of WaitGroups is common and it is difficult to write a proper vet check to prevent it. My preferences in order then would be:

* Add .Go to WaitGroup v1 to help people transition. Create sync/v2 WaitGroup without Add/Done and with just .Go. Add generic Map and Pool to v2.
* If we don't do that, add .Go to sync/v1 and add MapOf  and PoolOf to v1.
* If we don't do that, add v2 with generic Map and Pool but no other changes (the original proposal).

You ignored my point that removing the Add and Done APIs fundamentally reduces the utility of WaitGroup. There would be situations - admittedly rare, but not nonzero - in which you would need to reach into sync/v1 in order to access said functionality.

I think most people want errgroup or similar. A much more productive way to avoid this issue is to either

  1. Signpost people to errgroup from the documentation, or
  2. Import it wholesale somewhere into the stdlib; it's a very useful library which probably deserves it

I don't object to adding a .Go method to WaitGroup; but it can't really be the only method.

@borissmidt
Copy link

https://github.com/sourcegraph/conc
For the waitgroups/errorgroup conc could be wgood inspiration for the api. Where you can start with a waitgroups but can adjust it by changing it withError, withContext allowing you to take finegrained controll of whwt you need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Proposal v2 An incompatible library change
Projects
Status: Incoming
Development

No branches or pull requests