Нужен аналог sync.Once: объект с методом, который выполняет переданную
функцию только при первом вызове, а при повторных ничего не делает. Кандидат
написал такую первую версию:
type Single struct {
fired bool
mu sync.Mutex
}
func (s *Single) Run(f func()) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.fired {
f()
s.fired = true
}
}
func main() {
s := Single{}
s.Run(func() { fmt.Println(10) })
s.Run(func() { fmt.Println(20) })
s.Run(func() { fmt.Println(30) })
// 10
}
Интервьюер: «В целом работает, но до sync.Once не дотягивает». Доработай Single.
type Single struct { /* … */ }
func (s *Single) Run(f func())
Требования
- Нулевое значение
Single готово к работе.
f выполняется ровно один раз, даже если Run вызывают одновременно из
сотни горутин.
- Никто не возвращается из
Run раньше, чем первая f закончилась. Всё,
что f записала, видно после возврата из любого Run без дополнительной
синхронизации.
- Паника в
f тоже считается выполнением. Паника долетает до того, кто
вызвал Run, а следующие вызовы Run ничего не делают (как у sync.Once).
В первой версии fired не выставится, и f вызовут повторно.
- Быстрый путь: после того как
f выполнилась, Run не берёт мьютекс,
ему достаточно одного атомарного чтения. Это Run на горячем пути, его
зовут на каждый запрос.
- Код проходит
go test -race.
// OnceValue возвращает функцию, которая при первом вызове вызывает f и запоминает
// результат, а дальше возвращает его. Аналог sync.OnceValue.
func OnceValue[T any](f func() T) func() T
f вызывается лениво, при первом вызове возвращённой функции, и не больше
одного раза, даже при конкурентных вызовах. Все получают одно и то же значение.
- Если
f запаниковала, каждый вызов возвращённой функции паникует с тем
же значением.
Устно: почему нельзя обойтись одним atomic.CompareAndSwap без мьютекса?
Что будет, если внутри f снова вызвать Run на том же объекте?
Разбор и эталонное решение — после своей попытки: на собеседовании подсказок не будет.
Сначала попробуй сам — подсказки не дают готовый ответ.