среда, 23 января 2019 г.

Эффективный Go: конструкторы и составные литералы

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

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

Здесь выполняется много подготовительной однообразной работы. Мы можем упростить это используя составной литерал (composite literal), который является выражением, которое создает новый экземпляр каждый раз, когда он оценивается.

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}

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

return &File{fd, name, nil, 0}

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

return &File{fd: fd, name: name}

В качестве предельного случая, если составной литерал вообще не содержит полей, он создает нулевое значение для типа. Выражения new(File) и &File{} эквивалентны.

Составные литералы также могут быть созданы для массивов, срезов и карт, с метками полей, являющимися индексами или ключами карты в зависимости от ситуации. В следующем примере инициализация работает независимо от значений Enone, Eio и Einval, если они различны.

a := [...]string   {Enone: "no error", Eio: "Eio", 
                    Einval: "invalid argument"}
s := []string      {Enone: "no error", Eio: "Eio", 
                    Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", 
                    Einval: "invalid argument"}


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


Эффективный Go: данные, аллокация с помощью new

Go имеет два примитива для аллокации - встроенные функции new и make. Они делают разные вещи и относятся к разным типам, что может сбивать с толку, но правила просты. Давайте сначала поговорим о new. Это встроенная функция, которая выделяет память, но в отличие от своих однофамильцев в некоторых других языках она не инициализирует память, это только нули. То есть, new(T) выделяет обнуленное хранилище для нового элемента типа T и возвращает его адрес, значение типа *T. В терминологии Go он возвращает указатель на вновь назначенное нулевое значение типа T.

Поскольку память, возвращаемая new, обнуляется, полезно договориться при разработке ваших структур данных, что нулевое значение каждого типа может использоваться без дальнейшей инициализации. Это означает, что пользователь структуры данных может создать ее с помощью new и получить право на работу. Например, документация для bytes.Buffer гласит, что «Нулевое значение для Buffer является пустым буфером, готовым к использованию.» Точно так же sync.Mutex не имеет явного конструктора или метода Init. Вместо этого нулевое значение для sync.Mutex определяется как разблокированный мьютекс.

Свойство "нулевое значение является полезным" работает транзитивно. Рассмотрим следующее объявление типа.

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

Значения типа SyncedBuffer также готовы к использованию сразу после аллокации или просто декларации. В следующем фрагменте будут работать оба p и v правильно без дальнейшей договоренности.

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer


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


вторник, 22 января 2019 г.

Эффективный Go: оператор defer

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

// Contents возвращает содержимое файла как строку.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    // f.Close будет выполнена когда мы закончим.
    defer f.Close()  

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...)
        if err != nil {
            if err == io.EOF {
                break
            }
            // f будет закрыта если мы делаем возврат здесь
            return "", err  
        }
    }
    // f будет закрыта если мы делаем возврат здесь
    return string(result), nil 
}

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

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

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

Отложенные функции выполняются в порядке LIFO (last-in-first-out - последним зашел, первым вышел), поэтому этот код вызовет 4 3 2 1 0 для печати после возврата из функции. Более правдоподобный пример - простой способ отследить выполнение функции через программу. Мы могли бы написать пару простых трассировок рутины как следующие:

func trace(s string)   { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }

// Используем их:
func a() {
    trace("a")
    defer untrace("a")
    // делаем что-нибудь....
}

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

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

печатает

entering: b
in b
entering: a
in a
leaving: a
leaving: b

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


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


Эффективный Go: функции

Множественные возвращаемые значения

Одной из необычных особенностей Go является то, что функции и методы могут возвращать несколько значений. Эта форма может быть использована для улучшения пары неуклюжих идиом в программах на C: возвраты внутриполосных ошибок как -1 для EOF и изменение аргумента, переданного по адресу.

В C ошибка записи сигнализируется отрицательным числом с кодом ошибки скрытым в нестабильном месте. В Go Write может вернуть число и ошибку, например: "Да, вы написали несколько байтов, но не все, потому что вы заполнили всю память устройства". Сигнатура метода Write для файлов из пакета os:

func (file *File) Write(b []byte) (n int, err error)

и как говорится в документации, он возвращает количество записанных байтов и не-nil error, когда n != len(b). Это общепринятый стиль.

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

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

Вы можете использовать ее для сканирования чисел во входном срезе b следующим образом:

for i := 0; i < len(b); {
    x, i = nextInt(b, i)
    fmt.Println(x)
}

Именованные результирующие параметры

Возвращаемым или результирующим «параметрам» Go функции могут быть присвоены имена и они могут быть использованы как обычные переменные, как и входящие параметры. При именовании они инициализируются нулевыми значениями для своих типов, когда функция начинается; если функция выполняет оператор return без аргументов текущие значения результирующих параметров используется в качестве возвращаемых значений.

Имена не обязательны, но они могут сделать код короче и понятнее: это документация. Если мы назовем результаты nextInt, станет очевидно, который из возвращаемых int который.

func nextInt(b []byte, pos int) (value, nextPos int) {

Поскольку именованные результаты инициализируются и привязываются к неприукрашенному return, они могут упростить, а также уточнить. Вот версия io.ReadFull, который использует их:

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}


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


Эффективный Go: оператор switch

switch в Go более общий, чем в C. Выражения не должны быть константами или даже целыми числами, cases оцениваются сверху вниз, пока не будет найдено совпадение, и если switch не имеет выражения, он включается на true (то есть как будто бы было указано switch(true){}). Поэтому возможно - и идиоматично - писать if-else-if-else цепочку как switch.

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

Нет автоматического провала (то есть случая использования нескольких case одновременно для объединения их в один результат), но случаи (cases) могут быть представлены в разделенных запятыми списках.

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

Хотя они не так распространены в Go, как в других C-подобных языках, операторы break могут использоваться для раннего завершения switch. Иногда, однако, необходимо выйти из окружающего цикла (не switch цикла), и в Go это можно сделать, поставив метку в цикле и выходя (breaking) к этой метке. Следующий пример показывает оба варианта использования.

Loop:
    for n := 0; n < len(src); n += size {
        switch {
        case src[n] < sizeOne:
            if validateOnly {
                break
            }
            size = 1
            update(src[n])

        case src[n] < sizeTwo:
            if n+1 >= len(src) {
                err = errShortInput
                break Loop
            }
            if validateOnly {
                break
            }
            size = 2
            update(src[n] + src[n+1]<<shift)
        }
    }

Конечно, оператор continue также принимает необязательную метку, но это относится только к циклам.

Вот процедура сравнения для байтовых срезов, которая использует два оператора switch:

// Compare возвращает число сравнивая два байтовых среза,
// лексикографически.
// Результат будет 0 если a == b, -1 если a < b, 
// и +1 если a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

Переключатель типов (Type switch)

switch также может быть использован для обнаружения динамического типа переменной интерфейса. Такой type switch использует синтаксис утверждения типа (type assertion) с ключевым словом type в круглых скобках. Если switch объявляет переменную в выражении, переменная будет иметь соответствующий тип в каждом условии. Также идиоматично повторно использовать имя в таких случаях, фактически объявляя новую переменную с тем же именем, но с разным типом в каждом случае.

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    // %T печатает type, который t имеет
    fmt.Printf("неожидаемый тип %T\n", t)
case bool:
    // t имеет type bool
    fmt.Printf("boolean %t\n", t)
case int:
    // t имеет type int
    fmt.Printf("integer %d\n", t)
case *bool:
    // t имеет type *bool
    fmt.Printf("pointer к boolean %t\n", *t) 
case *int:
    // t имеет type *int
    fmt.Printf("pointer к integer %d\n", *t) 
}


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


Эффективный Go: цикл for

Цикл for в Go аналогичен - но не тот же самый - что в C. Он объединяет for и while, и не существует do-while. Есть три формы, только одна из которых имеет точки с запятой.

// Как C for
for init; condition; post { }

// Как C while
for condition { }
// Как C for(;;)
for { }

Короткие объявления позволяют легко объявить переменную индекса прямо в цикле.

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

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

for key, value := range oldMap {
    newMap[key] = value
}

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

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

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

sum := 0
for _, value := range array {
    sum += value
}

Пустой идентификатор имеет множество применений.

Для строк range делает большую часть работы за вас, выделяя отдельные Unicode символы путем разбора UTF-8. Ошибочные кодировки занимают один байт и производят замещающующий rune U+FFFD. (Имя (со связанным встроенным типом) rune - это терминология Go для единичного Unicode кодированного символа.) Цикл

// \x80 это невалидное UTF-8 кодирование
for pos, char := range "日本\x80語" { 
    fmt.Printf("character %#U starts 
               at byte position %d\n", char, pos)
}

печатает

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7

Наконец, в Go нет оператора запятой, а также ++ и -- являются заявлениями, а не выражениями. Таким образом, если вы хотите запустить несколько переменных в for Вы должны использовать параллельное присваивание (хотя это исключает ++ и --).

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}


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


Эффективный Go: повторная декларация и переназначение

Форма краткой декларации := работает следующим образом:

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

Объявление, которое вызывает os.Open

f, err := os.Open(name)

Этот оператор объявляет две переменные, f и err. Несколько строк спустя, вызов f.Stat

d, err := f.Stat()

который выглядит так, как будто он объявляет d и err. Однако обратите внимание, что err появляется в обоих операторах. Это дублирование допустимо: err объявляется первым оператором, но только переназначен во втором. Это означает, что вызов f.Stat использует существующую переменную err, которая объявлена ​​выше, и просто дает ей новое значение.

В объявлении := переменная v может появляться даже если она уже была объявлена, при условии:

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

Это необычное свойство - чистый прагматизм, облегчающий использование одного значения err, например, в длинной цепочке if-else. Вы увидите, что это часто используется.

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


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