diff --git a/api/next/56102.txt b/api/next/56102.txt
new file mode 100644
index 0000000000..00e7252df8
--- /dev/null
+++ b/api/next/56102.txt
@@ -0,0 +1,3 @@
+pkg sync, func OnceFunc(func()) func() #56102
+pkg sync, func OnceValue[$0 interface{}](func() $0) func() $0 #56102
+pkg sync, func OnceValues[$0 interface{}, $1 interface{}](func() ($0, $1)) func() ($0, $1) #56102
diff --git a/doc/go1.21.html b/doc/go1.21.html
index 38678a93c2..911a8ddd19 100644
--- a/doc/go1.21.html
+++ b/doc/go1.21.html
@@ -83,3 +83,15 @@ Do not send CLs removing the interior tags from such phrases.
TODO: complete this section
+
+- sync
+ -
+
+ The new OnceFunc,
+ OnceValue, and
+ OnceValues
+ functions capture a common use of Once to
+ lazily initialize a value on first use.
+
+
+
diff --git a/src/cmd/compile/internal/test/inl_test.go b/src/cmd/compile/internal/test/inl_test.go
index 96dd0bf935..205b746dd8 100644
--- a/src/cmd/compile/internal/test/inl_test.go
+++ b/src/cmd/compile/internal/test/inl_test.go
@@ -180,6 +180,15 @@ func TestIntendedInlining(t *testing.T) {
"net": {
"(*UDPConn).ReadFromUDP",
},
+ "sync": {
+ // Both OnceFunc and its returned closure need to be inlinable so
+ // that the returned closure can be inlined into the caller of OnceFunc.
+ "OnceFunc",
+ "OnceFunc.func2", // The returned closure.
+ // TODO(austin): It would be good to check OnceValue and OnceValues,
+ // too, but currently they aren't reported because they have type
+ // parameters and aren't instantiated in sync.
+ },
"sync/atomic": {
// (*Bool).CompareAndSwap handled below.
"(*Bool).Load",
diff --git a/src/sync/oncefunc.go b/src/sync/oncefunc.go
new file mode 100644
index 0000000000..9ef8344132
--- /dev/null
+++ b/src/sync/oncefunc.go
@@ -0,0 +1,97 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sync
+
+// OnceFunc returns a function that invokes f only once. The returned function
+// may be called concurrently.
+//
+// If f panics, the returned function will panic with the same value on every call.
+func OnceFunc(f func()) func() {
+ var (
+ once Once
+ valid bool
+ p any
+ )
+ // Construct the inner closure just once to reduce costs on the fast path.
+ g := func() {
+ defer func() {
+ p = recover()
+ if !valid {
+ // Re-panic immediately so on the first call the user gets a
+ // complete stack trace into f.
+ panic(p)
+ }
+ }()
+ f()
+ valid = true // Set only if f does not panic
+ }
+ return func() {
+ once.Do(g)
+ if !valid {
+ panic(p)
+ }
+ }
+}
+
+// OnceValue returns a function that invokes f only once and returns the value
+// returned by f. The returned function may be called concurrently.
+//
+// If f panics, the returned function will panic with the same value on every call.
+func OnceValue[T any](f func() T) func() T {
+ var (
+ once Once
+ valid bool
+ p any
+ result T
+ )
+ g := func() {
+ defer func() {
+ p = recover()
+ if !valid {
+ panic(p)
+ }
+ }()
+ result = f()
+ valid = true
+ }
+ return func() T {
+ once.Do(g)
+ if !valid {
+ panic(p)
+ }
+ return result
+ }
+}
+
+// OnceValues returns a function that invokes f only once and returns the values
+// returned by f. The returned function may be called concurrently.
+//
+// If f panics, the returned function will panic with the same value on every call.
+func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
+ var (
+ once Once
+ valid bool
+ p any
+ r1 T1
+ r2 T2
+ )
+ g := func() {
+ defer func() {
+ p = recover()
+ if !valid {
+ panic(p)
+ }
+ }()
+ r1, r2 = f()
+ valid = true
+ }
+ return func() (T1, T2) {
+ once.Do(g)
+ if !valid {
+ panic(p)
+ }
+ return r1, r2
+ }
+}
diff --git a/src/sync/oncefunc_test.go b/src/sync/oncefunc_test.go
new file mode 100644
index 0000000000..3c523a5b62
--- /dev/null
+++ b/src/sync/oncefunc_test.go
@@ -0,0 +1,265 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sync_test
+
+import (
+ "bytes"
+ "runtime"
+ "runtime/debug"
+ "sync"
+ "testing"
+)
+
+// We assume that the Once.Do tests have already covered parallelism.
+
+func TestOnceFunc(t *testing.T) {
+ calls := 0
+ f := sync.OnceFunc(func() { calls++ })
+ allocs := testing.AllocsPerRun(10, f)
+ if calls != 1 {
+ t.Errorf("want calls==1, got %d", calls)
+ }
+ if allocs != 0 {
+ t.Errorf("want 0 allocations per call, got %v", allocs)
+ }
+}
+
+func TestOnceValue(t *testing.T) {
+ calls := 0
+ f := sync.OnceValue(func() int {
+ calls++
+ return calls
+ })
+ allocs := testing.AllocsPerRun(10, func() { f() })
+ value := f()
+ if calls != 1 {
+ t.Errorf("want calls==1, got %d", calls)
+ }
+ if value != 1 {
+ t.Errorf("want value==1, got %d", value)
+ }
+ if allocs != 0 {
+ t.Errorf("want 0 allocations per call, got %v", allocs)
+ }
+}
+
+func TestOnceValues(t *testing.T) {
+ calls := 0
+ f := sync.OnceValues(func() (int, int) {
+ calls++
+ return calls, calls + 1
+ })
+ allocs := testing.AllocsPerRun(10, func() { f() })
+ v1, v2 := f()
+ if calls != 1 {
+ t.Errorf("want calls==1, got %d", calls)
+ }
+ if v1 != 1 || v2 != 2 {
+ t.Errorf("want v1==1 and v2==2, got %d and %d", v1, v2)
+ }
+ if allocs != 0 {
+ t.Errorf("want 0 allocations per call, got %v", allocs)
+ }
+}
+
+func testOncePanicX(t *testing.T, calls *int, f func()) {
+ testOncePanicWith(t, calls, f, func(label string, p any) {
+ if p != "x" {
+ t.Fatalf("%s: want panic %v, got %v", label, "x", p)
+ }
+ })
+}
+
+func testOncePanicWith(t *testing.T, calls *int, f func(), check func(label string, p any)) {
+ // Check that the each call to f panics with the same value, but the
+ // underlying function is only called once.
+ for _, label := range []string{"first time", "second time"} {
+ var p any
+ panicked := true
+ func() {
+ defer func() {
+ p = recover()
+ }()
+ f()
+ panicked = false
+ }()
+ if !panicked {
+ t.Fatalf("%s: f did not panic", label)
+ }
+ check(label, p)
+ }
+ if *calls != 1 {
+ t.Errorf("want calls==1, got %d", *calls)
+ }
+}
+
+func TestOnceFuncPanic(t *testing.T) {
+ calls := 0
+ f := sync.OnceFunc(func() {
+ calls++
+ panic("x")
+ })
+ testOncePanicX(t, &calls, f)
+}
+
+func TestOnceValuePanic(t *testing.T) {
+ calls := 0
+ f := sync.OnceValue(func() int {
+ calls++
+ panic("x")
+ })
+ testOncePanicX(t, &calls, func() { f() })
+}
+
+func TestOnceValuesPanic(t *testing.T) {
+ calls := 0
+ f := sync.OnceValues(func() (int, int) {
+ calls++
+ panic("x")
+ })
+ testOncePanicX(t, &calls, func() { f() })
+}
+
+func TestOnceFuncPanicNil(t *testing.T) {
+ calls := 0
+ f := sync.OnceFunc(func() {
+ calls++
+ panic(nil)
+ })
+ testOncePanicWith(t, &calls, f, func(label string, p any) {
+ switch p.(type) {
+ case nil, *runtime.PanicNilError:
+ return
+ }
+ t.Fatalf("%s: want nil panic, got %v", label, p)
+ })
+}
+
+func TestOnceFuncGoexit(t *testing.T) {
+ // If f calls Goexit, the results are unspecified. But check that f doesn't
+ // get called twice.
+ calls := 0
+ f := sync.OnceFunc(func() {
+ calls++
+ runtime.Goexit()
+ })
+ var wg sync.WaitGroup
+ for i := 0; i < 2; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ defer func() { recover() }()
+ f()
+ }()
+ wg.Wait()
+ }
+ if calls != 1 {
+ t.Errorf("want calls==1, got %d", calls)
+ }
+}
+
+func TestOnceFuncPanicTraceback(t *testing.T) {
+ // Test that on the first invocation of a OnceFunc, the stack trace goes all
+ // the way to the origin of the panic.
+ f := sync.OnceFunc(onceFuncPanic)
+
+ defer func() {
+ if p := recover(); p != "x" {
+ t.Fatalf("want panic %v, got %v", "x", p)
+ }
+ stack := debug.Stack()
+ want := "sync_test.onceFuncPanic"
+ if !bytes.Contains(stack, []byte(want)) {
+ t.Fatalf("want stack containing %v, got:\n%s", want, string(stack))
+ }
+ }()
+ f()
+}
+
+func onceFuncPanic() {
+ panic("x")
+}
+
+var (
+ onceFunc = sync.OnceFunc(func() {})
+
+ onceFuncOnce sync.Once
+)
+
+func doOnceFunc() {
+ onceFuncOnce.Do(func() {})
+}
+
+func BenchmarkOnceFunc(b *testing.B) {
+ b.Run("v=Once", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ // The baseline is direct use of sync.Once.
+ doOnceFunc()
+ }
+ })
+ b.Run("v=Global", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ // As of 3/2023, the compiler doesn't recognize that onceFunc is
+ // never mutated and is a closure that could be inlined.
+ // Too bad, because this is how OnceFunc will usually be used.
+ onceFunc()
+ }
+ })
+ b.Run("v=Local", func(b *testing.B) {
+ b.ReportAllocs()
+ // As of 3/2023, the compiler *does* recognize this local binding as an
+ // inlinable closure. This is the best case for OnceFunc, but probably
+ // not typical usage.
+ f := sync.OnceFunc(func() {})
+ for i := 0; i < b.N; i++ {
+ f()
+ }
+ })
+}
+
+var (
+ onceValue = sync.OnceValue(func() int { return 42 })
+
+ onceValueOnce sync.Once
+ onceValueValue int
+)
+
+func doOnceValue() int {
+ onceValueOnce.Do(func() {
+ onceValueValue = 42
+ })
+ return onceValueValue
+}
+
+func BenchmarkOnceValue(b *testing.B) {
+ // See BenchmarkOnceFunc
+ b.Run("v=Once", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ if want, got := 42, doOnceValue(); want != got {
+ b.Fatalf("want %d, got %d", want, got)
+ }
+ }
+ })
+ b.Run("v=Global", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ if want, got := 42, onceValue(); want != got {
+ b.Fatalf("want %d, got %d", want, got)
+ }
+ }
+ })
+ b.Run("v=Local", func(b *testing.B) {
+ b.ReportAllocs()
+ onceValue := sync.OnceValue(func() int { return 42 })
+ for i := 0; i < b.N; i++ {
+ if want, got := 42, onceValue(); want != got {
+ b.Fatalf("want %d, got %d", want, got)
+ }
+ }
+ })
+}