Future Proposals — Senior¶
Forward-looking content ages. Verify statuses against
github.com/golang/go/issuesbefore acting on anything below.
Table of Contents¶
- What this file is
- A policy for experimental features in production
- Isolating experimental code behind build tags
- Adopting synctest in a real test suite
- Migrating to range-over-func without breaking APIs
- Designing for a structured-concurrency future
- Influencing and tracking proposals
- Anti-patterns at scale
- Cheat sheet
- Self-assessment checklist
- Summary
- Further reading
What this file is¶
Middle surveyed the features; this file is about governing their adoption in a real codebase. The senior responsibility is to capture the value of new concurrency features without exposing production to experimental churn, and to position today's code so that landing proposals are an easy migration rather than a rewrite.
A policy for experimental features in production¶
Adopt a written rule for the team:
- Stable, released features (range-over-func since 1.23): adopt freely once the minimum supported Go version allows it.
GOEXPERIMENT-gated features (synctestearly on): allowed in tests only, never in shipped binaries; CI builds production with the experiment off.- Proposed/unaccepted designs: do not depend on them; instead, use the stable equivalent (
errgroup,context) and keep the code shaped so a future migration is local.
This policy turns "should we use this?" from a per-PR debate into a checkbox.
Isolating experimental code behind build tags¶
When you must touch an experimental API (almost always in tests), fence it so the main build never sees it.
//go:build goexperiment.synctest
package mypkg_test
import "testing/synctest"
// experimental-only test helpers here
Production code and CI release builds compile without the tag and are unaffected. The experimental surface is contained to files that literally cannot enter the release binary. Apply the same discipline to any vendored experimental package: keep the import behind a tag and provide a stable fallback for the default build.
Adopting synctest in a real test suite¶
testing/synctest is the highest-leverage recent feature for senior engineers because it kills an entire class of flaky, slow tests. Adoption strategy:
- Identify tests that use real
time.Sleep, tickers, or timeouts — these are your flake sources. - Wrap them in
synctest.Runso virtual time advances deterministically when all goroutines block. - Gate them behind the experiment build tag until your minimum Go version ships it stable.
- Measure CI wall-time improvement; timeout tests drop from seconds to microseconds.
The payoff is both speed and determinism: a test that asserts "this times out after 30s" runs instantly and never flakes on a loaded CI runner.
Migrating to range-over-func without breaking APIs¶
Range-over-func lets you offer iterator-based APIs, but introducing one shouldn't break existing callers. Provide the iterator alongside the existing slice/channel API:
// Existing API (keep it).
func (s *Store) All() []Item { ... }
// New iterator API (additive). Note the goroutine-cleanup contract.
func (s *Store) Iter() func(func(Item) bool) {
return func(yield func(Item) bool) {
// if this launches goroutines, stop+join them when yield returns false
for _, it := range s.items {
if !yield(it) {
return
}
}
}
}
The senior concern is the cleanup contract: any goroutines or resources an iterator owns must be released when the consumer breaks (yield returns false). Document it, test it with goleak, and never expose an iterator that leaks on early break.
Designing for a structured-concurrency future¶
You can't use language-level structured concurrency yet, but you can write code that will migrate trivially when (if) it lands:
- Scope goroutine lifetimes with
errgroupso every goroutine has an owner that waits for it. This is the same shape structured concurrency would enforce. - Thread
contexteverywhere, so cancellation is already plumbed. - Never spawn a bare
go func()whose lifetime exceeds its caller without an explicit owner and shutdown path.
Code written this way already has the "no goroutine outlives its scope" property by convention; a future construct would just make it enforced.
Influencing and tracking proposals¶
- Watch the relevant golang/go issues and the proposal review minutes; status changes between releases.
- Prototype against experimental APIs in a throwaway branch to give feedback — that's how proposals improve — but keep it out of
main. - When a proposal lands, do the migration in one focused PR, behind tests, replacing the stable-equivalent shim you'd been using.
Anti-patterns at scale¶
- Experimental APIs in release binaries — they break between versions; gate them to tests.
- Iterators that leak goroutines on early break — violate the cleanup contract.
- Removing a stable API to push everyone onto a new iterator — make it additive.
- Betting architecture on an unaccepted proposal — design with
errgroup/contextinstead. - Real-time sleeps in tests when
synctestis available — slow, flaky CI. - No team policy on experimental features — endless per-PR debate.
Cheat sheet¶
| Feature class | Production rule |
|---|---|
| Released (range-over-func) | adopt when min Go version allows; keep additive |
GOEXPERIMENT (synctest early) | tests only, behind build tag |
| Proposed/unaccepted | shim with errgroup/context; migrate when it lands |
| Iterator with goroutines | enforce cleanup-on-break; test with goleak |
Self-assessment checklist¶
- My team has a written policy for experimental concurrency features.
- I gate experimental imports behind build tags so release builds are clean.
- I can adopt
synctestto remove flaky timeout tests. - I introduce iterator APIs additively and enforce their cleanup contract.
- I shape goroutine lifetimes with
errgroup/contextfor a structured-concurrency-ready design. - I track proposal status and migrate in one focused PR when features land.
Summary¶
Senior adoption of future features is a governance problem: ship stable features additively, fence GOEXPERIMENT code behind build tags so it never enters release binaries, and model unaccepted proposals with today's stable tools (errgroup, context). testing/synctest is the standout near-term win — it eliminates flaky, slow timeout tests. When you expose range-over-func iterators, treat the goroutine-cleanup-on-break contract as mandatory and verify it with leak detection. Write goroutine lifetimes as if structured concurrency already existed, so that when it lands the migration is local.
Further reading¶
- "Range over function types" — https://go.dev/blog/range-functions
testing/synctestissue — https://github.com/golang/go/issues/67434- Go proposal process — https://github.com/golang/proposal
golang.org/x/sync/errgroup— https://pkg.go.dev/golang.org/x/sync/errgroup