Предыдущая часть: Часть 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). После запуска разработчик наследует контекст приложения. Это означает, что при остановке приложения библиотека будет использовать этот контекст для отправки сигнала отмены.

Telegram-канал по теме
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться

Часто этот контекст передается напрямую, например, при вызове конечной точки 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

Мы в Telegram — подписывайтесь
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться
NOP::Recruiter RemoteУдалённые ИТ-вакансии с вилками
Подписаться
Предыдущая статьяТоп-10 самых распространенных ошибок в проектах Go. Часть 1
Следующая статьяКак работает JavaScript