Go

Неизвестное значение enum

Рассмотрим простой пример:

type Status uint32

const (
	StatusOpen Status = iota
	StatusClosed
	StatusUnknown
)

Enum создан с помощью iota, что приводит к следующему состоянию:

StatusOpen = 0
StatusClosed = 1
StatusUnknown = 2

Предположим, что тип Status является частью запроса JSON, и он будет маршализован или не маршализован. Можно создать следующую структуру:

type Request struct {
	ID        int    `json:"Id"`
	Timestamp int    `json:"Timestamp"`
	Status    Status `json:"Status"`
}

Затем получаем следующие запросы:

{
  "Id": 1234,
  "Timestamp": 1563362390,
  "Status": 0
}

Ничего особенного, status не будет маршализован в StatusOpen.

Возьмем еще один запрос, в котором значение status не установлено (по каким-либо причинам):

{
  "Id": 1235,
  "Timestamp": 1563362390
}

В этом случае поле Status структуры Request инициализируется нулевым значением (для типа uint32: 0). Следовательно, StatusOpen вместо StatusUnknown.

Лучше всего установить неизвестное значение enum на 0:

type Status uint32

const (
	StatusUnknown Status = iota
	StatusOpen
	StatusClosed
)

Если status не является частью запроса JSON, он инициализируется в StatusUnknown, как и ожидалось.

Бенчмаркинг

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

Одна из распространенных ошибок — быть обманутым некоторыми оптимизациями компилятора. Возьмем конкретный пример из библиотеки teivah/bitvector:

func clear(n uint64, i, j uint8) uint64 {
	return (math.MaxUint64<<j | ((1 << i) - 1)) & n
}

Эта функция очищает биты в заданном диапазоне. Для их сравнения можно выполнить следующее:

func BenchmarkWrong(b *testing.B) {
	for i := 0; i < b.N; i++ {
		clear(1221892080809121, 10, 63)
	}
}

В этом бенчмарке компилятор заметит, что clear — это функция leaf (без вызова других функций), поэтому он встроит ее. Затем он заметит отсутствие побочных эффектов. Таким образом, вызов clear будет удален, что приведет к неточным результатам.

Одним из вариантов решения может быть установка результата на глобальную переменную следующим образом:

var result uint64

func BenchmarkCorrect(b *testing.B) {
	var r uint64
	for i := 0; i < b.N; i++ {
		r = clear(1221892080809121, 10, 63)
	}
	result = r
}

В данном случае компилятор не будет знать, приводит ли вызов к побочным эффектам. Следовательно бенчмарк будет точным.

Указатели! Указатели повсюду!

Передача переменной по значению создает копию этой переменной, в то время как передача по указателю просто копирует адрес памяти.

Следовательно, передача указателя всегда будет быстрее.

Рассмотрим пример. Это бенчмарк для структуры данных объемом 0,3 КБ, которая передана и получена по указателю, а затем по значению. 0,3 КБ — это небольшой объем, но близкий к типу структур данных, встречающихся ежедневно.

При выполнении этих бенчмарков в локальной среде передача по значению более чем в 4 раза быстрее, чем передача по указателю. Это связано с особенностями управления памятью в Go.

Переменная может быть размещена в куче или стеке.

  • Стек содержит текущие переменные для данной горутины. После возврата функции переменные извлекаются из стека.
  • Куча содержит общие переменные (глобальные и т. д.).

Рассмотрим простой пример с возвратом значения:

func getFooValue() foo {
	var result foo
	// Do something
	return result
}

Переменная result создается текущей горутиной и помещается в текущий стек. Сразу после возврата функции клиент получает копию этой переменной, а сама переменная извлекается из стека. Она все еще существует в памяти до удаления другой переменной, однако к ней нельзя получить доступ.

Рассмотрим тот же пример, но с использованием указателя:

func getFooPointer() *foo {
	var result foo
	// Do something
	return &result
}

Переменная result так же создается текущей горутиной, однако клиент получает указатель (копию адреса переменной). Если переменная result извлекается из стека, клиент этой функции больше не сможет получить к ней доступ.

В этом сценарии компилятор Go переносит переменную result в кучу, в которой переменные могут использоваться совместно.

Передача указателей — это другой сценарий. Например:

func main()  {
	p := &foo{}
	f(p)
}

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

Данный результат является следствием получения среза в методе Read io.Reader вместо выполнения возврата. Возврат среза (который является указателем) перенес бы его в кучу.

Тогда почему стек такой быстрый? Есть две основные причины:

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

В заключение, при создании функции следует использовать значения вместо указателей. Указатель должен использоваться только при совместном использовании переменной.

При наличии проблем с производительностью можно выполнить проверку указателей. Узнать, когда компилятор выводит переменную в кучу, можно с помощью следующей команды: go build -gcflags "-m -m".

Прерывание for/switch или for/select

Что произойдет, если в следующем примере f возвратит true?

for {
  switch f() {
  case true:
    break
  case false:
    // Do something
  }
}

Мы вызываем оператор break, но он выполняет прерывание оператора switchа не цикла for.

Та же проблема с:

for {
  select {
  case <-ch:
  // Do something
  case <-ctx.Done():
    break
  }
}

break относится к оператору select, а не к циклу for.

Одним из возможных решений для прерывания for/switch или for/select является использование оператора break с меткой:

loop:
	for {
		select {
		case <-ch:
		// Do something
		case <-ctx.Done():
			break loop
		}
	}

Управление ошибками

Go не совершенен в исправлении ошибок. Текущая стандартная библиотека (до Go 1.13) предлагает функции только для создания ошибок, поэтому стоит взглянуть на pkg/errors.

Эта библиотека предоставляет хороший способ соблюдать следующее правило:

Ошибка должна быть обработана только один раз. Регистрация ошибки — это обработка ошибки.

С текущей стандартной библиотекой трудно соблюдать данное правило, поскольку приходится добавлять контекст к ошибке и формировать иерархию.

Рассмотрим пример ожиданий при вызове REST, который приводит к проблеме с БД:

unable to server HTTP POST request for customer 1234
 |_ unable to insert customer contract abcd
     |_ unable to commit transaction

При использовании pkg/errors можно выполнить следующее:

func postHandler(customer Customer) Status {
	err := insert(customer.Contract)
	if err != nil {
		log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
		return Status{ok: false}
	}
	return Status{ok: true}
}

func insert(contract Contract) error {
	err := dbQuery(contract)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
	}
	return nil
}

func dbQuery(contract Contract) error {
	// Сделайте что-либо и получите ошибку
	return errors.New("unable to commit transaction")
}

Первоначальную ошибку можно создать с errors.New. Средний слой insert оборачивает эту ошибку, добавляя к ней больше контекста. Затем родитель обрабатывает ошибку, регистрируя ее. Каждый слой либо возвращает, либо обрабатывает ошибку.

Можно также проверить причину ошибки для реализации повторной попытки. Предположим, что пакет db из внешней библиотеки имеет доступ к базе данных. Эта библиотека может возвращать временную ошибку db.DBError. Чтобы определить, нужно ли повторить попытку, следует проверить причину ошибки:

func postHandler(customer Customer) Status {
	err := insert(customer.Contract)
	if err != nil {
		switch errors.Cause(err).(type) {
		default:
			log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
			return Status{ok: false}
		case *db.DBError:
			return retry(customer)
		}

	}
	return Status{ok: true}
}

func insert(contract Contract) error {
	err := db.dbQuery(contract)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
	}
	return nil
}

Данный пример выполняется с помощью errors.Cause из pkg/errors.

Одна из распространенных ошибок заключается в частичном использовании pkg/errors. Проверка ошибки выполнена следующим образом:

switch err.(type) {
default:
  log.WithError(err).Errorf("unable to server HTTP POST request for customer %s", customer.ID)
  return Status{ok: false}
case *db.DBError:
  return retry(customer)
}

Если db.DBError упакован, он никогда не будет запускать повторную попытку.

Читайте также:


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