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

Go style guides: необработанные строки, инициализация ссылок на структуры и карт

Используйте необработанные строковые литералы, чтобы избежать экранирования

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

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

wantError := "unknown name:\"test\""

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

wantError := `unknown error:"test"`

Инициализация ссылок на структуры

Используйте &T{} вместо new(T) при инициализации ссылок на структуру, чтобы это было консистентно с инициализацией структуры.

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

sval := T{Name: "foo"}

// неконсистентно
sptr := new(T)
sptr.Name = "bar"

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

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

Инициализация карт

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

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

// Объявление и инициализация визуально похожи.

var (
    // m1 безопасно читать и записывать;
    // m2 запаникует при записи.
    m1 = map[T1]T2{}
    m2 map[T1]T2
)

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

// Объявление и инициализация визуально различны.

var (
    // m1 безопасно читать и записывать;
    // m2 запаникует при записи.
    m1 = make(map[T1]T2)
    m2 map[T1]T2
)

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

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

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

m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3

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

m := map[T1]T2{
    k1: v1,
    k2: v2,
    k3: v3,
}

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


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


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

Голые параметры в вызовах функций могут ухудшить читаемость. Добавляйте комментарии в C-стиле (/* ... */) для имен параметров, когда их значение неочевидно.

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

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)

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

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

А еще лучше заменить голые типы bool пользовательскими типами для более читаемого и безопасного кода. Это позволяет использовать более двух состояний (true/false) для этого параметра в будущем.

type Region int

const (
    UnknownRegion Region = iota
    Local
)

type Status int

const (
    StatusReady Status = iota + 1
    StatusDone
    // Возможно, в будущем у нас будет StatusInProgress.
)

func printInfo(name string, region Region, status Status)


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


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

Go style guides: уменьшайте область видимости переменных

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

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

err := ioutil.WriteFile(name, data, 0644)
if err != nil {
    return err
}

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

if err := ioutil.WriteFile(name, data, 0644); err != nil {
    return err
}

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

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

if data, err := ioutil.ReadFile(name); err == nil {
    err = cfg.Decode(data)
    if err != nil {
        return err
    }

    fmt.Println(cfg)
    return nil
} else {
    return err
}

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

data, err := ioutil.ReadFile(name)
if err != nil {
     return err
}

if err := cfg.Decode(data); err != nil {
    return err
}

fmt.Println(cfg)
return nil


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


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

Go style guides: nil - это допустимый срез

nil - это допустимый срез длины 0. Это означает, что,

  • Вы не должны явно возвращать срез нулевой длины. Вместо этого верните nil.

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

    if x == "" {
        return []int{}
    }
    

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

    if x == "" {
        return nil
    }
    

  • Чтобы проверить, пуст ли срез, всегда используйте len(s) == 0. Не проверяйте на nil.

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

    func isEmpty(s []string) bool {
        return s == nil
    }
    

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

    func isEmpty(s []string) bool {
        return len(s) == 0
    }
    

  • Нулевое значение (срез, объявленный с помощью var) можно сразу использовать без make().

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

    nums := []int{}
    // или, nums := make([]int)
    
    if add1 {
        nums = append(nums, 1)
    }
    
    if add2 {
        nums = append(nums, 2)
    }
    

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

    var nums []int
    
    if add1 {
        nums = append(nums, 1)
    }
    
    if add2 {
        nums = append(nums, 2)
    }
    

Помните, что, хотя это действительный срез, нулевой срез (nil slice) не эквивалентен выделенному срезу длины 0 - первый равен nil, а второй нет - и оба могут обрабатываться по-разному в разных ситуациях (например, при сериализации).


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


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

Go style guides: объявления локальных переменных

Краткие объявления переменных ( := ) следует использовать, если для переменной явно задано какое-либо значение.

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

var s = "foo"

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

s := "foo"

Однако бывают случаи, когда значение по умолчанию яснее при использовании ключевого слова var. Например, объявление пустых срезов.

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

func f(list []int) {
    filtered := []int{}
    for _, v := range list {
        if v > 10 {
            filtered = append(filtered, v)
        }
    }
}

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

func f(list []int) {
    var filtered []int
    for _, v := range list {
        if v > 10 {
            filtered = append(filtered, v)
        }
    }
}


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


вторник, 22 декабря 2020 г.

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

Вы почти всегда должны указывать имена полей при инициализации структур. Теперь это навязывается командой go vet.

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

k := User{"John", "Doe", true}

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

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

Исключение: имена полей могут быть опущены в тестовых таблицах, если имеется 3 или меньше полей.

tests := []struct{
    op Operation
    want string
}{
    {Add, "add"},
    {Subtract, "subtract"},
}


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


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

Go style guides: встраивание в структуры

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

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

type Client struct {
    version int
    http.Client
}

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

type Client struct {
    http.Client

    version int
}

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

Встраивание не должно:

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

Проще говоря, внедряйте сознательно и намеренно. Хорошая лакмусовая бумажка: "будут ли все эти экспортированные внутренние методы/поля добавлены непосредственно во внешний тип"; если ответ - "некоторые" или "нет", не вставляйте внутренний тип - вместо этого используйте поле.

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

type A struct {
    // Неудачно: 
    // A.Lock() и A.Unlock() теперь доступны, 
    // не предоставляет функциональной выгоды,
    // разрешает пользователям контролировать 
    // детали внутреннего устройства A.
    sync.Mutex
}

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

type countingWriteCloser struct {
    // Хорошо: функция Write() предоставлена
    // во внешнем слое для определенной
    // цели, а делегаты работают
    // с Write() внутреннего типа.
    io.WriteCloser

    count int
}

func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

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

type Book struct {
    // Плохо: указатель 
    // изменяет полезность нулевого значения
    io.ReadWriter

    // другие поля
}

// позже

var b Book
b.Read(...)  // panic: nil pointer
b.String()   // panic: nil pointer
b.Write(...) // panic: nil pointer

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

type Book struct {
    // Хорошо: имеет полезное нулевое значение
    bytes.Buffer

    // другие поля
}

// позже

var b Book
b.Read(...)  // ok
b.String()   // ok
b.Write(...) // ok

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

type Client struct {
    sync.Mutex
    sync.WaitGroup
    bytes.Buffer
    url.URL
}

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

type Client struct {
    mtx sync.Mutex
    wg  sync.WaitGroup
    buf bytes.Buffer
    url url.URL
}


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


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

Go style guides: используйте префикс _ для неэкспортируемых глобальных переменных

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

Исключение: неэкспортированные значения ошибок, перед которыми должен стоять префикс err.

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

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

// foo.go
package foo

const (
    defaultPort = 8080
    defaultUser = "user"
)

// bar.go
package foo

func Bar() {
    defaultPort := 9090
    ...
    fmt.Println("Default port", defaultPort)

    // Мы не увидим ошибки компиляции, 
    // если первая строка Bar() будет удалена.
}

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

// foo.go

const (
    _defaultPort = 8080
    _defaultUser = "user"
)


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


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

Go style guides: объявления переменных верхнего уровня

На верхнем уровне используйте стандартное ключевое слово var. Не указывайте тип, если он совпадает с типом, возвращаемым выражением.

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

var _s string = F()

func F() string { return "A" }

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

var _s = F()
// Поскольку F уже заявляет, что возвращает строку, 
// нам не требуется указывать тип снова.

func F() string { return "A" }

Укажите тип, если тип выражения не соответствует в точности желаемому типу.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F rвозвращает объект типа myError, но нам нужен error.


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


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

Go style guides: уменьшайте вложенность

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

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

for _, v := range data {
    if v.F1 == 1 {
        v = process(v)
        if err := v.Call(); err == nil {
            v.Send()
        } else {
            return err
        }
    } else {
        log.Printf("Invalid v: %v", v)
    }
}

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

for _, v := range data {
    if v.F1 != 1 {
        log.Printf("Invalid v: %v", v)
        continue
    }

    v = process(v)
    if err := v.Call(); err != nil {
        return err
    }
    v.Send()
}

Ненужный else

Если переменная установлена в обеих ветвях if, ее можно заменить одним if.

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

var a int
if b {
    a = 100
} else {
    a = 10
}

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

a := 10
if b {
    a = 100
}


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


Go style guides: группировка функций и упорядочение

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

Следовательно, экспортируемые функции должны появляться в файле первыми, после определений struct, const, var.

newXYZ()/NewXYZ() может появиться после определения типа, но перед остальными методами на приемнике.

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

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

func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}

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

type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}


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


понедельник, 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 в документации.


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