Топ-10 самых распространенных ошибок в проектах Go. Часть 2


Предыдущая часть: Часть 1, Часть 2

Инициализация среза

Иногда конечная длина среза бывает известна. Допустим, нужно преобразовать срез Foo в срез Bar, что означает, что они среза будут иметь одинаковую длину.

Часто встречаются срезы, инициализированные следующим образом:

var bars []Bar bars := make([]Bar, 0)

Срез не является магической структурой. Он реализует стратегию роста при отсутствии свободного места. В этом случае автоматически создается новый массив (с большей емкостью), а все элементы копируются.

Предположим, что нужно повторить эту операцию роста несколько раз, так как []Foo содержит тысячи элементов. Сложность по времени амортизации (средняя) для вставки останется O(1), однако на практике это повлияет на производительность.

Поэтому, если конечная длина известна, то можно:

  • Инициализировать срез с заранее определенной длиной:
func convert(foos []Foo) []Bar { bars := make([]Bar, len(foos)) for i, foo := range foos { bars[i] = fooToBar(foo) } return bars }
  • Или инициализировать его с длиной 0 и заранее определенной емкостью:
func convert(foos []Foo) []Bar { bars := make([]Bar, 0, len(foos)) for _, foo := range foos { bars = append(bars, fooToBar(foo)) } return bars }

Какой вариант лучше? Первый немного быстрее, а во втором элементы более согласованы: независимо от того, известен ли начальный размер, добавление элемента в конце среза выполняется с использованием append.

Управление контекстом

context.Context довольно часто неправильно понимается разработчиками. Согласно официальной документации:

Контекст содержит дедлайн, сигнал отмены и другие значения в границах API.

Попробуем разобраться подробнее. Контекст может содержать:

  • Дедлайн. Это означает либо длительность (например, 250 мс), либо дату-время (например, 2019-01-08 01:00:00), при достижении которых необходимо отменить текущее действие.
  • Сигнал отмены (обычно <-chan struct {}). После получения сигнала необходимо остановить текущее действие. Например, получение двух запросов. Один для вставки данных, а другой для отмены первого запроса. Этого можно достичь, используя контекст отмены в первом вызове, который затем будет отменен при получении второго запроса.
  • Список ключ/значение (оба основаны на типе interface{}).

Также стоит добавить, что контекст является составным. Таким образом, он может содержать крайний срок и список ключ/значение. Кроме того, несколько горутин могут совместно использовать один и тот же контекст, поэтому сигнал отмены может останавливать несколько действий.

Приложение Go было основано на urfave/cli (библиотека для создания приложений командной строки в Go). После запуска разработчик наследует контекст приложения. Это означает, что при остановке приложения библиотека будет использовать этот контекст для отправки сигнала отмены.

Часто этот контекст передается напрямую, например, при вызове конечной точки gRPC. Этого делать не стоит.

Вместо этого нужно сообщить библиотеке gRPC о необходимости отмены запроса, например, при остановке приложения или через 100 мс.

Для этого можно создать сложный контекст. Если parent — это имя контекста приложения (созданного urfave/cli), то можно выполнить следующее:

ctx, cancel := context.WithTimeout(parent, 100 * time.Millisecond) response, err := grpcClient.Send(ctx, request)

Разобраться в контекстах не сложно, и это одна из лучших особенностей языка.

Отсутствие опции -race

Часто встречается такая ошибка, как проведение тестирования приложения Go без опции -race.

Несмотря на то, что Go был «разработан для упрощения параллельного программирования и сокращения количества ошибок», до сих пор возникают проблемы параллелизма.

Несмотря на то, что race detector в Go не справляется с этими проблемами, он является ценным инструментом, который стоит включить при тестировании приложений.

Использование filename в качестве входных данных

Еще одна распространенная ошибка — передача filename функции.

Например, нужно реализовать функцию для подсчета количества пустых строк в файле. Обычная реализация выглядит примерно так:

func count(filename string) (int, error) { file, err := os.Open(filename) if err != nil { return 0, errors.Wrapf(err, "unable to open %s", filename) } defer file.Close() scanner := bufio.NewScanner(file) count := 0 for scanner.Scan() { if scanner.Text() == "" { count++ } } return count, nil }

filename задается в качестве входных данных, поэтому его нужно открыть и затем реализовать логику, верно?

Предположим, что нужно реализовать модульные тестирования поверх этой функции, чтобы провести тестирование с обычным файлом, пустым файлом, файлом с другим типом кодировки и т. д. Управление превращается в непростую задачу.

Кроме того, чтобы реализовать ту же логику, например, для тела HTTP, нужно создать еще одну функцию.

Go предоставляет две отличные абстракции: io.Reader и io.Writer. Вместо передачи filename можно передать io.Reader, который абстрагирует источник данных.

Файл? Тело HTTP? Байтовый буфер? Это не важно, поскольку все еще используется тот же метод Read.

В данном случае можно даже буферизовать ввод, чтобы читать его построчно. Можно использовать bufio.Reader и его метод ReadLine:

func count(reader *bufio.Reader) (int, error) { count := 0 for { line, _, err := reader.ReadLine() if err != nil { switch err { default: return 0, errors.Wrapf(err, "unable to read") case io.EOF: return count, nil } } if len(line) == 0 { count++ } } }

Ответственность за открытие самого файла теперь делегирована клиенту count:

file, err := os.Open(filename) if err != nil { return errors.Wrapf(err, "unable to open %s", filename) } defer file.Close() count, err := count(bufio.NewReader(file))

Во второй реализации функция может быть вызвана независимо от фактического источника данных. Это облегчает модульные тестирования, благодаря возможности создать bufio.Reader из string:

count, err := count(bufio.NewReader(strings.NewReader("input")))

Горутины и переменные цикла

Последняя распространенная ошибка — использование горутинов с переменными цикла.

Каким будет результат следующего примера?

ints := []int{1, 2, 3} for _, i := range ints { go func() { fmt.Printf("%v\n", i) }() }

В этом примере каждая горутина имеет один и тот же экземпляр переменной, поэтому она будет выводить 3 3 3.

Есть два решения этой проблемы. Первый — передача значения переменной i в замыкание (внутренняя функция):

ints := []int{1, 2, 3} for _, i := range ints { go func(i int) { fmt.Printf("%v\n", i) }(i) }

И второе — создание еще одной переменной в области видимости цикла for:

ints := []int{1, 2, 3} for _, i := range ints { i := i go func() { fmt.Printf("%v\n", i) }() }

Выполнение вызова i := i может показаться странным, однако это допустимое действие. Нахождение в цикле предполагает нахождение в другой области видимости. Таким образом, i := i создает еще один экземпляр переменной с названием i.


Перевод статьи Teiva Harsanyi: The Top 10 Most Common Mistakes I’ve Seen in Go Projects


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.