Предыдущая часть: Часть 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
}
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.
Попробуем разобраться подробнее. Контекст может содержать:
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)
Разобраться в контекстах не сложно, и это одна из лучших особенностей языка.
Часто встречается такая ошибка, как проведение тестирования приложения Go без опции -race.
Несмотря на то, что Go был «разработан для упрощения параллельного программирования и сокращения количества ошибок», до сих пор возникают проблемы параллелизма.
Несмотря на то, что race detector в Go не справляется с этими проблемами, он является ценным инструментом, который стоит включить при тестировании приложений.
Еще одна распространенная ошибка — передача 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
:
Последняя распространенная ошибка — использование горутинов с переменными цикла.
Каким будет результат следующего примера?
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
Комментарии