воскресенье, 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


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


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

Go style guides: указатели на интерфейсы, проверка интерфейса

Указатели на интерфейсы

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

Интерфейс состоит из двух полей:

  • Указатель на некоторую информацию, зависящую от типа. Вы можете думать об этом как о "типе".
  • Указатель данных. Если сохраненные данные представляют собой указатель, они сохраняются напрямую. Если сохраненные данные представляют собой значение, то сохраняется указатель на значение.

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

Проверить соответствие интерфейса

При необходимости проверьте соответствие интерфейса во время компиляции. Это включает в себя:

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

Плохой пример:

type Handler struct {
  // ...
}

func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}

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

type Handler struct {
  // ...
}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

Утверждение var _ http.Handler = (*Handler)(nil) не будет скомпилировано, если *Handler когда-либо перестанет соответствовать интерфейсу http.Handler.

В правой части присваивания должно быть nil значение утвержденного типа. Это nil для типов указателей (например, *Handler), срезов и карт и пустая структура для типов структур.

type LogHandler struct {
  h   http.Handler
  log *zap.Logger
}

var _ http.Handler = LogHandler{}

func (h LogHandler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}


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


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

Срезы как аргументы в Golang

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

Первый пример. Срез передается функции в качестве аргумента, где он будет изменен.

https://play.golang.org/p/r5mKX5ErwLC

package main

import "fmt"

func change(abc []int) {
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4]
[4 4 4]

Кто-то может ожидать, что, когда мы передадим срез в функцию, мы получим его копию в функции, а изменения появятся только в функции. Но это не так. Срезы в Go имеют базовый массив и передаются по ссылке, а не по значению. Мы просто передаем ссылку на срез в функцию, затем меняем ее, и она меняет внешний срез.

Второй пример.

https://play.golang.org/p/5ruLrp6ZJJc

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4]
[1 2 3]

Что изменилось? Чем этот пример отличается от первого? Мы просто добавляем один элемент в срез внутри функции. Но это существенно меняет срез. Создает новый срез. Как? Когда мы создаем срез вне функции, он создает базовый массив размером для 3 элементов. Затем мы передаем ссылку в функцию. Но когда мы хотим добавить еще один элемент, у нас нет места для него в базовом массиве. Затем встроенная функция добавления создает новый массив и новый срез. Когда мы меняем в нем значения, мы не меняем первый срез.

Третий пример.

https://play.golang.org/p/0tKWomkDCk3

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 5)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    change(abc)
    abc = append(abc, 4)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4]
[1 2 3 4]

Почему, когда мы меняем срез вне функции, он не будет указывать на измененный срез в функции? Ну это просто совершенно разные срезы - и все. Когда мы используем append вне функции, он создает для него новый массив и новый срез.

Пример четвертый.

https://play.golang.org/p/uCDG59fJLFm

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    abc = append(abc, 4)
    change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4 4]
[4 4 4 4]

Почему этот пример отличается от третьего? Когда мы добавляем срез перед передачей его функции, он создает базовый массив с емкостью не только для одного добавленного элемента, но и с начальным числом элементов, кратным 1,5 - для 6 элементов. Мы можем это увидеть:

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    abc = append(abc, 4)
    fmt.Println(cap(abc))
    change(abc)
    fmt.Println(abc)
}

Вывод:

6
[4 4 4 4 4]
[4 4 4 4]

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

Эти примеры могут привести к неожиданным для кого-то результатам. Как этого избежать?

Просто верните срез из функции. Он вернет ссылку на новый срез, если он был создан.

package main

import "fmt"

func change(abc []int) []int {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
    return abc
}

func main() {
    abc := []int{1, 2, 3}
    abc = change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4]
[4 4 4 4]

И напишите модульные тесты. Скорее всего, они обнаружат неожиданное поведение. Если применимо, сначала напишите тесты - используйте Test Driven Development.


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