Practice
Pointer (easy)
Что проверяем
- Базовое понимание структур
- Понимание работы с указателями
Условие задачи
Что выведет следующая программа?
package main
import "fmt"
type Person struct {
Name string
}
func main() {
person := &Person{
Name: "Bob",
}
fmt.Println(person.Name)
changeName(person)
fmt.Println(person.Name)
}
func changeName(person *Person) {
person = &Person{
Name: "Alice",
}
}
Ответ
Bob
Bob
Почему она выведет именно так?
- Функция
changeName
принимает копию указателя и в копию указателя записывается адрес нового объекта.
Новые требования к задаче
Как модифицировать программу, чтобы вывелось:
Bob
Alice
Ожидаемые изменения
// way 1
func changeName(person *Person) {
person.Name = "Alice"
}
// way 2
func changeName(person *Person) {
*person = Person{
Name: "Alice 2",
}
}
Примечание
Горутины в цикле (easy)
Что проверяем
- Базовое знание горутин
- Понимание областей видимости переменных
- Знание примитивов синхронизации
- Понимание проблем race conditions
Условие задачи
Что выведет следующая программа и почему?
package main
import "fmt"
// Находим максимальное четное число
func main() {
var max int
for i := 1000; i > 0; i-- {
go func() {
if i%2 == 0 && i > max {
max = i
}
}()
}
fmt.Printf("Maximum is %d", max)
}
Ответ
Невозможно предсказать, так как есть следующие проблемы:
- Проблема 1: Замыкание
Вывод не соответствует тому, что мы ожидаем - так как замыкание
i
в цикле связывает значение переменной (и на это ругнётсяgo vet
при сборке -loop variable i captured by func literal
). Каким будетi
в момент запуска первой горутины мы не знаем. Решение: передатьi
как параметр в анонимную функцию - Проблема 2: Синхронизация
Программа завершается не обязательно дождавшись присвоения хотя бы одного числа (запуска хотя бы одной горутины).
Решение: синхронизировать ожидание через
sync.WaitGroup
. - Проблема 3: race conditions
Переменная
max
будет использоваться множеством горутин, вызывая race condition. Значение переменной в итоге может получиться любым. Решение: можно решить разными путями, но базово - через Mutex. Важная ремарка - если править через Атомики только строчку присвоения - проблема не решится. Лочить обязательно необходимо также и строку со сравнением. Дополнительно: тут важно понять насколько кандидат вообще понимает принципы concurrency в ходе его рассуждений о решении. Если он сразу поставит лок перед условием - спросить "зачем мы лочим вообще все, если читать можно конкурентно без проблем"
Вопрос если кандидат использует defer
для wg.Done()
Дополнительный вопрос к секции: если кандидат использует defer для wg.Done() спросить осознанность его решения и почему он не написал линейно без defer. Можно уточнить тут про потенциальную доп. нагрузку, которую дает (или не дает defer). Поговорить про Low-cost defers
Новые требования к задаче
Как модифицировать программу, чтобы вывелось:
1000
Параллельный запрос URL адресов (easy)
Что проверяем
- Базовое знание горутин.
- Знание примитивов синхронизации
- Понимание работы context
- Паттерны межсервисного взаимодействия
Условие задачи
// Написать код, который будет выводить коды ответов на HTTP-запросы по двум URL адресам.
// Например главная страница страница Avito (https://www.avito.ru) и Go (https://go.dev)
// Запросы должны осуществляться параллельно.
Решение
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
resp, err := http.Get("https://www.avito.ru")
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Avito status: %s\n", resp.Status)
}()
go func() {
defer wg.Done()
resp, err := http.Get("https://go.dev")
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Go status: %s\n", resp.Status)
}()
wg.Wait()
}
Возможные ошибки
- Запросы происходят не параллельно
- При использовании
WaitGroup
вызовwg.Add(1)
делается внутри горутины - Нет проверки ошибок
Дополнительные вопросы
- Шедулинг горутин, преимущества горутин (что такое горутины в Go и как они соотносятся с потоками операционной системы)
- А если URL адресов будет 500 и запустить запросы по ним в 500 горутинах, будут и все они выполняться одновременно? Как это устроено “под капотом”?
- Как изменить этот код чтобы можно было передавать любое количество URL адресов?
- Как изменить этот код чтобы максимальное время его выполнения было 2 секунды?
- Advanced: Как изменить код, если мы захотим сделать утилиту демоном, который умеет безопасно завершаться (graceful shutdown). Ожидаем рассказ про обработку сигналов.
- Expert: Как можно оптимизировать создание TCP-соединений в HTTP/1.1? (ожидаем ответ про keep-alive) Как можно управлять keep-alive в HTTP-клиенте go? (ожидаем, что скажет про пул соединений, MaxIdleConns, IdleConnTimeout) Для чего управлять пулом? (контроль нагрузки на сервис, к которому обращаемся)
- Expert: Как в Linux понять, сколько у процесса открытых соединений? Приходилось ли дебажить на таком уровне? (Ожидаем, что расскажет про lsof, netstat или ss)
- Expert: Какие проблемы нужно учесть при запросах к удаленным серверам? (Могут быть проблемы недоступности, приемлемым решением было бы использование паттерна Circuit breaker).