Неизвестное значение 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
упакован, он никогда не будет запускать повторную попытку.
Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming