Используйте singleflight.Group с ключом name: совпадающие незавершённые вызовы получат общий результат, а разные имена выполнятся независимо. Это объединение запросов, не постоянный кэш.
Декоратор с объединением одновременных запросов к клиенту
Используйте singleflight.Group с ключом name: совпадающие незавершённые вызовы получат общий результат, а разные имена выполнятся независимо. Это объединение запросов, не постоянный кэш.
Короткий ответ
Что ответить на собеседовании
Подробный разбор
Ответ с пояснениями
Условие
Необходимо создать обёртку‑декоратор над интерфейсом Client, которая будет гарантировать, что при одновременных вызовах метода GetUser(name):
- если переданные имена совпадают (например, "Vasya"), реальная реализация Client.GetUser("Vasya") будет выполнена только один раз;
- если имена различаются ("Vasya" и "Oleg"), запросы обрабатываются независимо.
Метод Client.GetUser является ресурсоёмким и дорогим, поэтому важно, чтобы запросы с одинаковым параметром выполнялись единожды, а полученный результат распределялся между всеми горутинами, сделавшими запрос.
type User struct {
Id int64
}
type IClient interface {
GetUser(name string) (*User, error)
}
type Client struct {}
func (c Client) GetUser(name string) (*User, error) {
// дорогостоящая операция
return &User{}, nil
}
func NewClient() *Client {
return &Client{}
}
type Decorator struct {
client IClient
// ...
}
func (d *Decorator) GetUser(name string) (*User, error) {
// ...
}
func NewDecorator(c IClient) *Decorator {
return &Decorator{client: c}
}
Пример использования:
func main() {
client := NewClient()
changedClient := NewDecorator(client)
go func() {
vasya, err := changedClient.GetUser("Vasya")
// ...
}()
go func() {
vasya, err := changedClient.GetUser("Vasya")
// ...
}()
go func() {
vasya, err := changedClient.GetUser("Vasya")
// ...
}()
go func() {
vasya, err := changedClient.GetUser("Vasya")
// ...
}()
go func() {
oleg, err := changedClient.GetUser("Oleg")
// ...
}()
go func() {
oleg, err := changedClient.GetUser("Oleg")
// ...
}()
}
Задача требует реализации синхронизации запросов, кэширования результата на время выполнения и корректного распределения ошибки/результата между всеми вызывающими.
Ответ
Используйте singleflight.Group с ключом name: совпадающие незавершённые вызовы получат общий результат, а разные имена выполнятся независимо. Это объединение запросов, не постоянный кэш.
С импортом golang.org/x/sync/singleflight:
type Decorator struct {
client IClient
group singleflight.Group
}
func NewDecorator(c IClient) *Decorator {
return &Decorator{client: c}
}
func (d *Decorator) GetUser(name string) (*User, error) {
value, err, _ := d.group.Do(name, func() (any, error) {
return d.client.GetUser(name)
})
user, _ := value.(*User)
return user, err
}
После завершения очередной вызов снова обратится к client. Результат-указатель общий: не изменяйте его конкурентно или возвращайте копии. Интерфейс без context не позволяет отменить медленный запрос; main из примера также должен ждать завершения своих горутин.
Источник: pkg.go.dev.