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


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


вторник, 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


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


пятница, 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.


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