понедельник, 14 декабря 2020 г.

Go style guides: имена пакетов, импорт

Порядок групп импорта

Следует иметь две группы импорта:

  • Стандартная библиотека
  • Все остальное

Это группировка, применяемая goimports по умолчанию.

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

import (
    "fmt"
    "os"
    "go.uber.org/atomic"
    "golang.org/x/sync/errgroup"
)

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

import (
    "fmt"
    "os"

    "go.uber.org/atomic"
    "golang.org/x/sync/errgroup"
)

Имена пакетов

При именовании пакетов выберите такое имя, которое:

  • Все в нижнем регистре. Без заглавных букв и подчеркиваний.
  • Не требует переименования с использованием именованного импорта на большинстве мест вызова.
  • Коротко и емко. Помните, что имя указывается полностью на каждом месте вызова.
  • Не во множественном числе. Например, net/url, а не net/urls.
  • Не "common", "util", "shared", "lib". Это неудачные, малоинформативные имена.

Имена функций

Следуйте соглашению сообщества Go об использовании MixedCaps для имен функций. Исключение делается для тестовых функций, которые могут содержать символы подчеркивания с целью группировки связанных тестовых случаев, например, TestMyFunction_WhatIsBeingTested.

Псевдонимы в импорте

Псевдоним импорта необходимо использовать, если имя пакета не соответствует последнему элементу пути импорта.

import (
    "net/http"

    client "example.com/client-go"
    trace "example.com/trace/v2"
)

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

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

import (
    "fmt"
    "os"

    nettrace "golang.net/x/trace"
)

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

import (
    "fmt"
    "os"
    "runtime/trace"

    nettrace "golang.net/x/trace"
)


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


суббота, 12 декабря 2020 г.

Go style guides: последовательность, группировка объявлений

Будьте последовательны.

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

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

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

Группировать похожие объявления

Go поддерживает группировку похожих объявлений.

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

import "a"
import "b"

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

import (
    "a"
    "b"
)

Это также относится к объявлениям констант, переменных и типов.

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

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

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

const (
    a = 1
    b = 2
)

var (
    a = 1
    b = 2
)

type (
    Area float64
    Volume float64
)

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

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

type Operation int

const (
    Add Operation = iota + 1
    Subtract
    Multiply
    ENV_VAR = "MY_ENV"
)

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

type Operation int

const (
    Add Operation = iota + 1
    Subtract
    Multiply
)

const ENV_VAR = "MY_ENV"

Группы не ограничены в том, где их можно использовать. Например, вы можете использовать их внутри функций.

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

func f() string {
    var red = color.New(0xff0000)
    var green = color.New(0x00ff00)
    var blue = color.New(0x0000ff)

    ...
}

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

func f() string {
    var (
        red   = color.New(0xff0000)
        green = color.New(0x00ff00)
        blue  = color.New(0x0000ff)
    )

    ...
}


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


четверг, 10 декабря 2020 г.

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

Укажите емкость контейнера, где это возможно, чтобы заранее выделить память для контейнера. Это минимизирует последующие выделения (путем копирования и изменения размера контейнера) по мере добавления элементов.

Указание подсказок емкости карты

По возможности предоставляйте подсказки (hint) по емкости при инициализации карт с помощью make().

make(map[T1]T2, hint)

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

Обратите внимание, что, в отличие от срезов, подсказки емкости карты не гарантируют полного упреждающего выделения, но используются для приблизительного определения количества требуемых сегментов хэш-карты. Следовательно, выделения могут все еще происходить при добавлении элементов в карту, даже до указанной емкости.

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

m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}

m создается без указания размера; во время назначения может быть больше выделений.

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

files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
    m[f.Name()] = f
}

m создается с подсказкой размера; во время назначения может быть меньше выделений.

Указание емкости среза

По возможности предоставляйте подсказки о емкости при инициализации срезов с помощью make(), особенно при планировании дальнейших добавлений в срез.

make([]T, length, capacity)

В отличие от карт, емкость среза не является подсказкой: компилятор выделит достаточно памяти для емкости среза, как это предусмотрено для make(), что означает, что последующие операции append() будут нести нулевые выделения (до тех пор, пока длина среза не будет соответствовать емкости (capacity), указанной при создании среза, после чего любые добавления потребуют изменения размера для хранения дополнительных элементов).

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

for n := 0; n < b.N; n++ {
    data := make([]int, 0)
    for k := 0; k < size; k++{
        data = append(data, k)
    }
}

BenchmarkBad    100000000    2.48s

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

for n := 0; n < b.N; n++ {
    data := make([]int, 0, size)
    for k := 0; k < size; k++{
        data = append(data, k)
    }
}

BenchmarkGood   100000000    0.21s


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


среда, 9 декабря 2020 г.

Go style guides: производительность, strconv вместо fmt, преобразования строки в байты

Рекомендации по производительности применимы только к часто выполняемому за время работы программы коду.

Предпочитайте strconv вместо fmt

При преобразовании примитивов в/из строк strconv быстрее, чем fmt.

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

for i := 0; i < b.N; i++ {
    s := fmt.Sprint(rand.Int())
}

BenchmarkFmtSprint    143 ns/op    2 allocs/op

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

for i := 0; i < b.N; i++ {
    s := strconv.Itoa(rand.Int())
}

BenchmarkStrconv    64.2 ns/op    1 allocs/op

Избегайте преобразования строки в байты

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

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

for i := 0; i < b.N; i++ {
    w.Write([]byte("Hello world"))
}

BenchmarkBad    50000000   22.2 ns/op

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

data := []byte("Hello world")
for i := 0; i < b.N; i++ {
    w.Write(data)
}

BenchmarkGood   500000000   3.25 ns/op


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


воскресенье, 6 декабря 2020 г.

Go style guides: избегайте использования init()

По возможности избегайте init(). Когда init() неизбежен или желателен, код должен попытаться:

  • Будьте полностью детерминированными, независимо от программной среды или вызова.
  • Избегайте зависимости от порядка или побочных эффектов других функций init(). Хотя порядок init() хорошо известен, код может меняться, и, таким образом, отношения между функциями init() могут сделать код хрупким и подверженным ошибкам.
  • Избегайте доступа или манипулирования глобальным состоянием или состоянием среды, таким как машинная информация, переменные среды, рабочий каталог, программные аргументы/входные данные и т. д.
  • Избегайте операций ввода-вывода, включая вызовы файловой системы, сети и системные вызовы.

Код, который не может удовлетворить эти требования, скорее всего, принадлежит как помощник, который будет вызываться как часть main() (или где-то еще в жизненном цикле программы), или быть написанным как часть самого main(). В частности, библиотеки, которые предназначены для использования другими программами, должны быть полностью детерминированными и не выполнять "магию инициализации".

Неудачный пример:

type Foo struct {
    // ...
}

var _defaultFoo Foo

func init() {
    _defaultFoo = Foo{
        // ...
    }
}

Более удачный пример:

var _defaultFoo = Foo{
    // ...
}

// или, лучше, для тестирования:

var _defaultFoo = defaultFoo()

func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

Неудачный пример:

type Config struct {
    // ...
}

var _config Config

func init() {
    // Плохо: на основе текущего каталога
    cwd, _ := os.Getwd()

    // Плохо: I/O
    raw, _ := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )

    yaml.Unmarshal(raw, &_config)
}

Более удачный пример:

type Config struct {
    // ...
}

func loadConfig() Config {
    cwd, err := os.Getwd()
    // обрабатываем err

    raw, err := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // обрабатываем err

    var config Config
    yaml.Unmarshal(raw, &config)

    return config
}

Учитывая вышеизложенное, некоторые ситуации, в которых init() может быть предпочтительным или необходимым, могут включать:

  • Сложные выражения, которые нельзя представить как отдельные присваивания.
  • Подключаемые хуки, такие как диалекты database/sql, реестры типов кодирования и т. д.
  • Оптимизация Google Cloud Functions и других форм детерминированных предварительных вычислений.

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


суббота, 5 декабря 2020 г.

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

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

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

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

var error string
// `error` затеняет встроенный идентификатор

// или

func handleErrorMessage(error string) {
    // `error` затеняет встроенный идентификатор
}

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

var errorMessage string
// `error` относится к встроенному идентификатору

// или

func handleErrorMessage(msg string) {
    // `error` относится к встроенному идентификатору
}

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

type Foo struct {
    // Хотя эти поля технически не
    // составляют затенение, поиск для
    // строк `error` или` string` теперь
    // неоднозначен.
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` и` f.error`     
    // визуально похожи
    return f.error
}

func (f Foo) String() string {
    // `string` и` f.string`
    // визуально похожи
    return f.string
}

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

type Foo struct {
    // Строки `error` и` string`
    // теперь однозначны.
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

Обратите внимание, что компилятор не будет генерировать ошибки при использовании предопределенных идентификаторов, но такие инструменты, как go vet, должны правильно указывать на эти и другие случаи затенения.


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


пятница, 4 декабря 2020 г.

Go style guides: избегайте встраивания типов в общедоступные структуры

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

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

type AbstractList struct {}

// Add добавляет объект в список.
func (l *AbstractList) Add(e Entity) {
    // ...
}

// Remove удаляет объект из списка.
func (l *AbstractList) Remove(e Entity) {
    // ...
}

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

// ConcreteList - это список сущностей.
type ConcreteList struct {
    *AbstractList
}

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

// ConcreteList - это список сущностей.
type ConcreteList struct {
    list *AbstractList
}

// Add добавляет объект в список.
func (l *ConcreteList) Add(e Entity) {
    l.list.Add(e)
}

// Remove удаляет объект из списка.
func (l *ConcreteList) Remove(e Entity) {
    l.list.Remove(e)
}

Go позволяет встраивание типов как компромисс между наследованием и композицией. Внешний тип получает неявные копии методов встроенного типа. Эти методы по умолчанию делегируются тому же методу встроенного экземпляра.

Структура также получает поле с тем же именем, что и тип. Итак, если встроенный тип является общедоступным, поле является общедоступным. Для обеспечения обратной совместимости каждая будущая версия внешнего типа должна сохранять встроенный тип.

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

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

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

// AbstractList - это обобщенная реализация
// для разного рода списков сущностей.
type AbstractList interface {
    Add(Entity)
    Remove(Entity)
}

// ConcreteList - это список сущностей.
type ConcreteList struct {
    AbstractList
}

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

// ConcreteList - это список сущностей.
type ConcreteList struct {
    list AbstractList
}

// Add добавляет объект в список.
func (l *ConcreteList) Add(e Entity) {
    l.list.Add(e)
}

// Remove удаляет объект из списка.
func (l *ConcreteList) Remove(e Entity) {
    l.list.Remove(e)
}

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

  • Добавление методов во встроенный интерфейс - критическое изменение.
  • Удаление методов из встроенной структуры - критическое изменение.
  • Удаление встроенного типа - критическое изменение.
  • Замена встроенного типа, даже альтернативой, удовлетворяющей тому же интерфейсу, является критическим изменением.

Хотя написание методов делегирования утомительно, дополнительные усилия скрывают детали реализации, оставляют больше возможностей для изменений, а также устраняют косвенное указание на обнаружение полного интерфейса List в документации.


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