вторник, 24 ноября 2020 г.

Go style guides: избегайте изменяемых глобальных переменных

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

Неудачный вариант:

// sign.go

var _timeNow = time.Now

func sign(msg string) string {
    now := _timeNow()
    return signWithTime(msg, now)
}

// sign_test.go

func TestSign(t *testing.T) {
    oldTimeNow := _timeNow
    _timeNow = func() time.Time {
        return someFixedTime
    }
    defer func() { _timeNow = oldTimeNow }()

    assert.Equal(t, want, sign(give))
}

Более удачный вариант:

// sign.go

type signer struct {
    now func() time.Time
}

func newSigner() *signer {
    return &signer{
        now: time.Now,
    }
}

func (s *signer) Sign(msg string) string {
    now := s.now()
    return signWithTime(msg, now)
}

// sign_test.go

func TestSigner(t *testing.T) {
    s := newSigner()
    s.now = func() time.Time {
        return someFixedTime
    }

    assert.Equal(t, want, s.Sign(give))
}


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


воскресенье, 22 ноября 2020 г.

Go style guides: не используйте panic

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

Неудачный вариант:

func run(args []string) {
    if len(args) == 0 {
        panic("требуется аргумент")
    }
    // ...
}

func main() {
    run(os.Args[1:])
}

Более удачный вариант:

func run(args []string) error {
    if len(args) == 0 {
        return errors.New("требуется аргумент")
    }
    // ...
    return nil
}

func main() {
    if err := run(os.Args[1:]); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Panic/recover не является стратегией обработки ошибок. Программа должна паниковать только тогда, когда происходит что-то безвозвратное, например, нулевое разыменование (nil dereference, попытка получить значение по nil адресу, например в ситуации когда переменная с типом указателя структуры содержит nil и происходит попытка разыменования указателя). Исключением является инициализация программы: плохие вещи при запуске программы, которые должны прервать выполнение программы, могут вызвать panic.

var _statusTemplate = template.Must(
     template.New("name").Parse("_statusHTML"))

Даже в тестах предпочтительнее паники t.Fatal или t.FailNow, чтобы тест был отмечен как неудачный.

Неудачный вариант:

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
    panic("не удалось настроить тест")
}

Более удачный вариант:

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
    t.Fatal("не удалось настроить тест")
}


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


суббота, 21 ноября 2020 г.

Go style guides: обертывание ошибок, обработка ошибок утверждения типа

Обертывание ошибок

Есть три основных варианта распространения ошибок в случае сбоя вызова:

  • Верните исходную ошибку, если нет дополнительного контекста для добавления и вы хотите сохранить исходный тип ошибки.
  • Добавьте контекст, используя "github.com/pkg/errors".Wrap, чтобы сообщение об ошибке предоставляло больше контекста и "github.com/pkg/errors".Cause можно использовать для извлечения исходной ошибки.
  • Используйте fmt.Errorf, если вызывающим абонентам не нужно обнаруживать или обрабатывать этот конкретный случай ошибки.

Рекомендуется добавлять контекст, где это возможно, чтобы вместо неопределенной ошибки, такой как "connection refused" ("соединение отклонено"), вы получали более полезные ошибки, такие как "call service foo: connection refused" ("вызов службы foo: соединение отклонено").

При добавлении контекста к возвращаемым ошибкам сохраняйте краткость контекста, избегая фраз вроде "failed to" ("не удалось"), которые констатируют очевидное и накапливаются по мере того, как ошибка просачивается через стек.

Менее удачный вариант:

s, err := store.New()
if err != nil {
    return fmt.Errorf("failed to create new store: %s", err)
}

Вывод:

failed to x: failed to y: failed to create new store: the error

Более удачный вариант:

s, err := store.New()
if err != nil {
    return fmt.Errorf("new store: %s", err)
}

Вывод:

x: y: new store: the error

Однако после отправки ошибки в другую систему должно быть ясно, что сообщение является ошибкой (например, тег err или префикс "Failed" в журналах).

Обработка ошибок утверждения типа

Форма единственного возвращаемого значения утверждения типа вызовет панику из-за неправильного типа. Поэтому всегда используйте идиому "comma ok".

Неудачный вариант:

t := i.(string)

Более удачный вариант:

t, ok := i.(string)
if !ok {
    // корректно обрабатываем ошибку
}


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


четверг, 19 ноября 2020 г.

Go style guides: типы ошибок

Существуют различные варианты объявления ошибок:

  • errors.New для ошибок с простыми статическими строками
  • fmt.Errorf для форматированных строк ошибок
  • настраиваемые типы, реализующие метод Error()
  • обернутые ошибки с помощью "github.com/pkg/errors".Wrap

При возврате ошибок учитывайте следующее, чтобы определить лучший выбор:

  • Это простая ошибка, не требующая дополнительной информации? Если да, то errors.New должно хватить.
  • Нужно ли клиентам обнаруживать и обрабатывать эту ошибку? В таком случае следует использовать настраиваемый тип и реализовать метод Error().
  • Распространяете ли вы ошибку, возвращаемую нижестоящей функцией? Если да, используете обертывание ошибок.
  • В противном случае используйте fmt.Errorf.

Если клиенту необходимо обнаружить ошибку, и вы создали простую ошибку, используя errors.New, используйте var для ошибки.

Менее удачный вариант:

// package foo

func Open() error {
    return errors.New("could not open")
}

// package bar

func use() {
    if err := foo.Open(); err != nil {
        if err.Error() == "could not open" {
            // обработка
        } else {
            panic("unknown error")
        }
    }
}

Более удачный вариант:

// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
    return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
    if err == foo.ErrCouldNotOpen {
        // обработка
    } else {
        panic("unknown error")
    }
}

Более удачный вариант с версии Go 1.13:

// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
    return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
    if errors.Is(err, foo.ErrCouldNotOpen) {
        // обработка
    } else {
        panic("unknown error")
    }
}

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

Менее удачный вариант:

func open(file string) error {
    return fmt.Errorf("file %q not found", file)
}

func use() {
    if err := open("testfile.txt"); err != nil {
        if strings.Contains(err.Error(), "not found") {
            // обработка
        } else {
            panic("unknown error")
        }
    }
}

Более удачный вариант:

type errNotFound struct {
    file string
}

func (e errNotFound) Error() string {
    return fmt.Sprintf("file %q not found", e.file)
}

func open(file string) error {
    return errNotFound{file: file}
}

func use() {
    if err := open("testfile.txt"); err != nil {
        if _, ok := err.(errNotFound); ok {
            // обработка
        } else {
            panic("unknown error")
        }
    }
}

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

// package foo

type errNotFound struct {
    file string
}

func (e errNotFound) Error() string {
    return fmt.Sprintf("file %q not found", e.file)
}

func IsNotFoundError(err error) bool {
    _, ok := err.(errNotFound)
    return ok
}

func Open(file string) error {
    return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
    if foo.IsNotFoundError(err) {
        // handle
    } else {
        panic("unknown error")
    }
}


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


пятница, 13 ноября 2020 г.

Go style guides: используйте "time", чтобы управлять временем

Время сложно. К неверным предположениям, которые часто делают относительно времени, относятся следующие.

  1. В сутках 24 часа
  2. В часе 60 минут
  3. В неделе 7 дней
  4. В году 365 дней и другие

Например, 1 означает, что добавление 24 часов к моменту времени не всегда дает новый календарный день.

Поэтому всегда используйте пакет "time" при работе со временем, потому что он помогает справиться с этими неверными предположениями более безопасным и точным образом.

Использование time.Time для моментов времени

Используйте time.Time при работе с моментами времени и методы на time.Time при сравнении, сложении или вычитании времени.

Неудачный вариант:

func isActive(now, start, stop int) bool {
    return start <= now && now < stop
}

Хороший вариант:

func isActive(now, start, stop time.Time) bool {
    return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

Использовать time.Duration для периодов времени

Используйте time.Duration при работе с периодами времени.

Неудачный вариант:

func poll(delay int) {
    for {
      // ...
      time.Sleep(time.Duration(delay) * time.Millisecond)
    }
}

poll(10) // это были секунды или миллисекунды?

Хороший вариант:

func poll(delay time.Duration) {
    for {
        // ...
        time.Sleep(delay)
    }
}

poll(10*time.Second)

Возвращаясь к примеру добавления 24 часов к моменту времени, метод, который мы используем для добавления времени, зависит от намерения. Если нам нужно то же время дня, но на следующий календарный день, мы должны использовать Time.AddDate. Однако, если мы хотим, чтобы момент времени гарантированно был на 24 часа позже предыдущего, мы должны использовать Time.Add.

newDay := t.AddDate(0 /* years */, 0, /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)

Использование time.Time и time.Duration с внешними системами

По возможности используйте time.Duration и time.Time во взаимодействии с внешними системами. Например:

  • Флаги командной строки: flag поддерживает time.Duration через time.ParseDuration
  • JSON: encoding/json поддерживает кодирование time.Time в виде строки RFC 3339 с помощью метода UnmarshalJSON.
  • SQL: database/sql поддерживает преобразование столбцов DATETIME или TIMESTAMP в time.Time и обратно, если базовый драйвер поддерживает это
  • YAML: gopkg.in/yaml.v2 поддерживает time.Time как строку RFC 3339 и time.Duration через time.ParseDuration.

Если невозможно использовать time.Duration в этих взаимодействиях, используйте int или float64 и включите единицу измерения в имя поля.

Например, поскольку encoding/json не поддерживает time.Duration, единица измерения включается в имя поля.

Неудачный вариант:

// {"interval": 2}
type Config struct {
    Interval int `json:"interval"`
}

Хороший вариант:

// {"intervalMillis": 2000}
type Config struct {
    IntervalMillis int `json:"intervalMillis"`
}

Когда невозможно использовать time.Time в этих взаимодействиях, если не согласована альтернатива, используйте строковые и форматные метки времени, как определено в RFC 3339. Этот формат используется по умолчанию Time.UnmarshalText и доступен для использования во Time.Format и time.Parse по time.RFC3339.

Хотя на практике это обычно не является проблемой, имейте в виду, что пакет "time" не поддерживает синтаксический анализ меток времени с дополнительными секундами (leap seconds), а также не учитывает дополнительные секунды в вычислениях. Если вы сравните два момента времени, разница не будет включать дополнительные секунды, которые могли произойти между этими двумя моментами.


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


среда, 11 ноября 2020 г.

Go style guides: начинайте перечисления с единицы

Стандартный способ введения перечислений (enum) в Go - объявить настраиваемый тип и const группу с помощью iota. Поскольку переменные имеют значение по умолчанию 0, вам обычно следует начинать перечисления с ненулевого значения.

Неудачный вариант:

type Operation int

const (
    Add Operation = iota
    Subtract
    Multiply
)

// Add=0, Subtract=1, Multiply=2

Хороший вариант:

type Operation int

const (
    Add Operation = iota + 1
    Subtract
    Multiply
)

// Add=1, Subtract=2, Multiply=3

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

type LogOutput int

const (
    LogToStdout LogOutput = iota
    LogToFile
    LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2


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


воскресенье, 8 ноября 2020 г.

Go style guides: размер канала - один или нет

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

Спорный вариант:

// Должно хватить на кого угодно!
c := make(chan int, 64)

Хороший вариант:

// Размер один
c := make(chan int, 1) // или
// Небуферизованный канал, нулевой размер
c := make(chan int)


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


суббота, 7 ноября 2020 г.

Go style guides: defer для приборки

Используйте defer для очистки ресурсов, таких как файлы и блокировки.

Неудачный вариант:

p.Lock()
if p.count < 10 {
    p.Unlock()
    return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// разблокировку легко пропустить 
// из-за многократного возврата

Хороший вариант:

p.Lock()
defer p.Unlock()

if p.count < 10 {
    return p.count
}

p.count++
return p.count

// более читаемый вариант

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


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


пятница, 6 ноября 2020 г.

Go style guides: копирование срезов и карт на границах

Срезы и карты содержат указатели на базовые данные, поэтому будьте осторожны со сценариями, когда их нужно скопировать.

Получение срезов и карт

Помните, что пользователи могут изменять карту или срез, полученный вами в качестве аргумента, если вы сохраняете ссылку на него.

Неудачный вариант:

func (d *Driver) SetTrips(trips []Trip) {
    d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// Вы хотели изменить d1.trips?
trips[0] = ...

Хороший вариант:

func (d *Driver) SetTrips(trips []Trip) {
    d.trips = make([]Trip, len(trips))
    copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// Теперь мы можем изменить trips[0], 
// не затрагивая d1.trips.
trips[0] = ...

Возврат срезов и карт

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

Неудачный вариант:

type Stats struct {
    mu sync.Mutex
    counters map[string]int
}

// Snapshot возвращает текущую статистику.
func (s *Stats) Snapshot() map[string]int {
    s.mu.Lock()
    defer s.mu.Unlock()

    return s.counters
}

// snapshot больше не защищен мьютексом, поэтому любой
// доступ к snapshot повод для гонки данных.
snapshot := stats.Snapshot()

Хороший вариант:

type Stats struct {
    mu sync.Mutex
    counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
    s.mu.Lock()
    defer s.mu.Unlock()

    result := make(map[string]int, len(s.counters))
    for k, v := range s.counters {
        result[k] = v
    }
    return result
}

// Snapshot теперь является копией.
snapshot := stats.Snapshot()


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


Go style guides: мьютексы с нулевым значением действительны

Нулевое значение sync.Mutex и sync.RWMutex допустимо, поэтому вам почти никогда не понадобится указатель на мьютекс.

Неудачный вариант:

mu := new(sync.Mutex)
mu.Lock()

Хороший пример:

var mu sync.Mutex
mu.Lock()

Если вы используете структуру по указателю, то мьютекс может быть полем без указателя.

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

type smap struct {
    sync.Mutex // только для неэкспортируемых типов

    data map[string]string
}

func newSMap() *smap {
    return &smap{
        data: make(map[string]string),
    }
}

func (m *smap) Get(k string) string {
    m.Lock()
    defer m.Unlock()

    return m.data[k]
}

Для экспортируемых структур:

type SMap struct {
    mu sync.Mutex

    data map[string]string
}

func NewSMap() *SMap {
    return &SMap{
        data: make(map[string]string),
    }
}

func (m *SMap) Get(k string) string {
    m.mu.Lock()
    defer m.mu.Unlock()

    return m.data[k]
}

Встраивайте частные типы или типы, которые должны реализовывать интерфейс Mutex. Для экспортируемых типов используйте частное поле.


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


четверг, 5 ноября 2020 г.

Go style guides: приемники и интерфейсы

Методы с приемниками значений могут вызываться как для указателей, так и для значений. Методы с получателями указателей могут вызываться только для указателей или адресуемых значений.

Например:

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = str
}

sVals := map[int]S{1: {"A"}}

// Вы можете вызвать только Read, используя значение
sVals[1].Read()

// Это не будет компилироваться:
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// Вы можете вызвать как Read, так и Write, 
// используя указатель
sPtrs[1].Read()
sPtrs[1].Write("test")

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

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

// Следующее не компилируется, 
// поскольку s2Val является значением, 
// а для f нет получателя значения.
//   i = s2Val


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