Skip to content

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).