четверг, 31 января 2019 г.

Эффективный Go: текущий буфер

Инструменты параллельного программирования могут облегчить выражение даже не-многопоточных идей. Вот пример, извлеченный из RPC пакета. Клиентская программа выполняет цикл получения данных из какого-либо источника, возможно сети. Чтобы избежать выделения и освобождения буферов, она сохраняет свободный список, и использует буферизованный канал для его представления. Если канал пуст, выделяется новый буфер. Когда буфер сообщений готов, он отправляется на сервер serverChan.

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Берем буфер, если он доступен; 
        // иначе - аллоцируем новый.
        select {
        case b = <-freeList:
            // Получили буфер; больше ничего не делаем.
        default:
            // Нет свободных, поэтому аллоцируем новый.
            b = new(Buffer)
        }

        // Читаем следующее сообщение из сети.
        load(b)              
        serverChan <- b  // Отправляем на сервер.
    }
}

Цикл сервера получает каждое сообщение от клиента, обрабатывает его, и возвращает буфер в свободный список.

func server() {
    for {
        b := <-serverChan    // Ожидание работы.
        process(b)
        // Повторно используем буфер, если есть место.
        select {
        case freeList <- b:
            // Буфер в свободном списке; 
            // больше ничего не делаем.
        default:
            // Список свободных полон, просто продолжаем.
        }
    }
}

Клиент пытается получить буфер из freeList; если ни один не доступен, он выделяет новый. Отправка сервером в freeList возвращает b в список свободных если список не полный, иначе буфер выбрасывается, чтобы быть утилизированным сборщиком мусора. (Условие default в select операторах выполняются, когда ни один другой случай не готов, это означает, что select никогда не блокируется.) Эта реализация создает список без утечек памяти всего в несколько строк, полагаясь на буферизованный канал и сборщик мусора для учета.


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


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

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

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

type Vector []float64

// Применяеи операцию к v[i], v[i+1] ... до v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // сигнализируем что эта часть выполнена
}

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

const numCPU = 4 // количество ядер процессора

func (v Vector) DoAll(u Vector) {

    // Буферизация необязательна, но разумна
    c := make(chan int, numCPU)  
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, 
                   (i+1)*len(v)/numCPU, u, c)
    }
    // Опустошаем канал
    for i := 0; i < numCPU; i++ {
        <-c    // ждем завершения одного задания
    }
    // Все выполнено
}

Вместо того, чтобы создавать постоянное значение для numCPU, мы можем спросить среду выполнения, какое значение уместно. Функция runtime.NumCPU возвращает количество аппаратных ядер процессора на машине, таким образом мы можем написать:

var numCPU = runtime.NumCPU()

Также есть функция runtime.GOMAXPROCS, которая сообщает (или устанавливает) указанное пользователем количество ядер, которое может запустить Go программа одновременно. По умолчанию используется значение runtime.NumCPU, но оно может быть переопределено установкой одноименной переменной среды оболочки или вызывом этой функции с положительным номером. Вызов этой функции с нолем просто запрашивает значение. Поэтому, если мы хотим удовлетворить запрос ресурса пользователя, мы должны написать:

var numCPU = runtime.GOMAXPROCS(0)

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


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


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

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

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

type Request struct {
    args        []int
    f           func([]int) int
    resultChan  chan int
}

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

func sum(a []int) (s int) {
    for _, v := range a {
        s += v
    }
    return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Отправляем запрос (request)
clientRequests <- request
// Ждем ответ
fmt.Printf("answer: %d\n", <-request.resultChan)

На стороне сервера функция обработчика (handle function) - единственное, что изменяется.

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

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


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


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

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

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

// небуферизованный канал целых чисел
ci := make(chan int)

// небуферизованный канал целых чисел            
cj := make(chan int, 0)   
     
// буферизованный канал указателей на Files
cs := make(chan *os.File, 100)  

Небуферизованные каналы объединяют связь (обмен значением) с синхронизацией, гарантируя, что два вычисления (goroutines) находятся в известном состоянии.

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

c := make(chan int)  // Аллоцируем канал
// Начинаем сортировку в go-процедуре(goroutine); 
// когда она завершится, посылаем сигнал по каналу
go func() {
    list.Sort()
    c <- 1  // Отправляем сигнал; значение не важно
}()
doSomethingForAWhile()
<-c   // Ждем завершения сортировки; 
      // отменяем посланное значение

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

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

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Ожидание истечения активной очереди.
    process(r)  // Может занять длительное время.
    <-sem       // Выполнено; 
                // разрешаем выполнение следующего запроса.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Не ждем окончания handle
    }
}

Пока обработчики(handlers) в MaxOutstanding выполняют process, будут блокироваться попытки отправки в заполненный буфер канала, пока один из существующих обработчиков не завершит работу и не получит из буфера.

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

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Ошибочно; 
                         // смотрите объяснение ниже
            <-sem
        }()
    }
}

Ошибка в том, что в Go цикле for переменная цикла используется повторно для каждой итерации, поэтому req переменная является общей для всех программ. Это не то, что мы хотим. Нам нужно убедиться, что req уникален для каждой программы. Вот один из способов сделать это, передав значение req в качестве аргумента к замыканию в go-процедуре:

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

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

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Создаем новый экземпляров req 
                   // для go-процедуры
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

Это может показаться странным писать

req := req

но в Go это законно и идиоматично. Вы получаете свежую версию переменной с тем же именем, сознательно затеняя переменную цикла локально, но уникально для каждой go-процедуры.

Возвращаясь к общей проблеме написания сервера, другой подход, который хорошо управляет ресурсами, состоит в том, чтобы начать фиксированное количество go-процедур handle, которые все читают из канала запросов. Количество go-процедур ограничивает количество одновременных вызовов process. Следующая функция Serve также принимает канал, на котором будет сказано выйти; после запуска go-процедур он блокирует получение из этого канала.

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Стартуем handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Ждем того, что будет сказано выйти
}


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


Эффективный Go: параллелизм, go-процедуры (goroutines)

Разделение через общение

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

Параллельное программирование во многих средах затруднено ввиду тонкостей и особенностей, необходимых для правильного доступа к разделяемым переменным. Go поощряет другой подход, в котором общие значения передаются по каналам и, фактически, никогда активно не разделяется отдельными потоками исполнения. Только одна программа имеет доступ к значению в любой момент времени. Гонки данных не могут происходить, по замыслу. Для поощрения такого мышления существует лозунг:

Не общайтесь, разделяя память; вместо этого делитесь памятью, общаясь.

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

Один из способов обдумать эту модель - рассмотреть типичную однопоточную программу, которая работает на одном процессоре. Она не нуждается в синхронизации примитивов. Теперь запустите другой такой экземпляр; он тоже не нуждается в синхронизации. Теперь пусть эти экземпляры общаются; если связь является синхронизатором, по-прежнему нет необходимости для другой синхронизации. Unix-конвейеры (Unix pipelines), например, подходят для этой модели в совершенстве. Хотя подход Go к параллелизму берет свое начало в Связи последовательных процессов Хора (Hoare's Communicating Sequential Processes) (CSP), его также можно рассматривать как безопасное для типов обобщение каналов Unix.

Go-процедуры (Goroutines)

Они называются Go-процедуры (goroutines), потому что существующие термины - потоки (threads), сопрограммы (coroutines), процессы и т. д. - передают неточные значения. Go-процедура (goroutine) имеет простую модель: это функция, выполняющаяся одновременно с другими go-процедурами в том же адресном пространстве. Она легковесная, стоящая чуть больше, чем аллокация стекового пространства. И стеки стартуют малыми, поэтому они дешевы и растут выделяя (и освобождая) хранилище heap по мере необходимости.

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

Используйте префикс с ключевым словом go для вызова функции или метода для запуска вызова в новой Go-процедуре. Когда вызов завершается, Go-процедура выходит молча. (Эффект похож на нотацию & в оболочке Unix для запуска команды в фоне.)

// выполняем list.Sort параллельно; не ждите его.
go list.Sort()  

Литерал функции может быть полезен при вызове go-процедуры.

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  
    // Обратите внимание на круглые скобки 
    // - должна быть вызвана функция.
}

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

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


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


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

Эффективный Go: вложение (embedding)

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

Интерфейс вложения очень прост. Мы уже упоминали интерфейсы io.Reader и io.Writer; Вот их определения.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Пакет io также экспортирует несколько других интерфейсов, которые указывают объекты, которые могут реализовать несколько таких методов. Например, есть io.ReadWriter, интерфейс содержит как Read, так и Write. Мы могли бы указать io.ReadWriter, перечислив два метода явно, но это проще и лучше запоминается - встроить два интерфейса для формирования нового, например так:

// ReadWriter - это интерфейс, 
// который объединяет Reader и Writer интерфейсы.
type ReadWriter interface {
    Reader
    Writer
}

Это означает: ReadWriter может сделать, что Reader делает и что Writer делает; это объединение вложенных интерфейсов (которые должны быть непересекающимися наборами методов). Только интерфейсы могут быть вложены в интерфейсы.

Та же самая идея относится и к структурам, но с более далеко идущими последствиями. Пакет bufio имеет два типа структуры: bufio.Reader и bufio.Writer, каждая из которых, конечно, реализует аналогичные интерфейсы из пакета io. Кроме того bufio реализует буферизованное устройство чтения/записи, что он делает, объединяя читателя и писателя в одну структуру, используя вложения: перечисляет типы в структуре, но не дает им имена полей.

// ReadWriter сохраняет указатели на Reader и Writer.
// Он реализует io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

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

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

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

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

Вкладывая структуры напрямую, мы избегаем этого. Методы встроенных типов предоставляются бесплатно, что означает, что bufio.ReadWriter не только имеет методы bufio.Reader и bufio.Writer, но он также удовлетворяет всем трем интерфейсам: io.Reader, io.Writer и io.ReadWriter.

Есть важная деталь, которой вложение отличается от подкласса. Когда мы встраиваем тип, методы этого типа становятся методами внешнего типа, но когда они вызываются, получатель метода является внутренним типом, а не внешним. В нашем примере, когда метод Read для bufio.ReadWriter вызывается, он имеет тот же эффект, что и метод пересылки, описанный выше; получатель - это поле reader в ReadWriter, а не сам ReadWriter.

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

type Job struct {
    Command string
    *log.Logger
}

Тип Job теперь имеет Log, Logf и другие методы *log.Logger. Мы могли бы дать Logger имя поля, конечно, но это не обязательно. А теперь однажды инициализировав, мы можем логировать в Job:

job.Log("starting now...")

Logger является обычным полем структуры Job, поэтому мы можем инициализировать его обычным способом внутри конструктора для Job, например:

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

или с составным литералом,

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

Если нам нужно обратиться к вложенному полю напрямую, имя типа поля, игнорируя спецификатор пакета, служит именем поля, как это было в методе Read нашей структуры ReadWriter. Здесь, если нам нужно было получить доступ к *log.Logger Job переменной job, мы бы написали job.Logger, что было бы полезно, если бы мы хотели усовершенствовать методы Logger.

func (job *Job) Logf(format string, args ...interface{}) {
    job.Logger.Logf("%q: %s", 
                    job.Command, 
                    fmt.Sprintf(format, args...))
}

Вложение типов представляет проблему конфликтов имен, но правила, которые необходимо учитывать, просты. Во-первых, поле или метод X скрывает любой другой элемент X более глубоко вложенной части типа. Если log.Logger содержал поле или метод с именем Command, поле Command Job будет доминировать над ним.

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


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


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

Как мы обсуждали в постах ранее, тип не должен явно объявлять, что он реализует интерфейс. Вместо этого тип реализует интерфейс просто путем реализации методов интерфейса. На практике большинство преобразований интерфейса являются статическими и поэтому проверяются во время компиляции. Например, передача функции *os.File в функцию, ожидающую io.Reader, не скомпилируется, если *os.File не реализует интерфейс io.Reader.

Однако некоторые проверки интерфейса происходят во время выполнения. Один экземпляр находится в encoding/json пакете, который определяет Marshaler интерфейс. Когда кодировщик JSON получает значение, которое реализует этот интерфейс, кодировщик вызывает метод маршалинга значения, чтобы преобразовать его в JSON вместо того, чтобы делать стандартное преобразование. Кодировщик проверяет это свойство во время выполнения с утверждением типа (type assertion), например:

m, ok := val.(json.Marshaler)

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

if _, ok := val.(json.Marshaler); ok {
    fmt.Printf(
     "value %v of type %T implements json.Marshaler\n",
      val, val)
}

Единственное место, где возникает такая ситуация - это когда необходимо гарантировать в пакете, реализующем тип, что пакет фактически удовлетворяет интерфейсу. Если тип, например, json.RawMessage - тогда необходимо пользовательское представление JSON, оно должно реализовывать json.Marshaler, но нет статических преобразований, которые бы заставили компилятор проверить это автоматически. Если тип случайно не удовлетворяет интерфейсу, кодировщик JSON все еще будет работать, но не будет использовать пользовательскую реализацию. Чтобы гарантировать правильность реализации, глобальное объявление с использованием пустого идентификатора может быть использовано в пакете:

var _ json.Marshaler = (*RawMessage)(nil)

В этой декларации, назначение, включающее преобразование *RawMessage в Marshaler, требует, чтобы *RawMessage реализовал Marshaler, и это свойство будет проверено во время компиляции. Если интерфейс json.Marshaler изменится, этот пакет больше не будет компилироваться, и мы будем предупреждены, что его нужно обновить.

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


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


понедельник, 28 января 2019 г.

Эффективный Go: пустой идентификатор в импорте и переменных

Неиспользуемый импорт и переменные

Это ошибка - импортировать пакет или объявлять переменную без его/ее использования. Неиспользованный импорт приводит к раздуванию программы и медленной компиляции, в то время как переменная, которая инициализируется, но не используется, это по крайней мере потраченные впустую вычисления и, возможно, свидетельствует о большей ошибке. Когда программа находится в активной разработке, однако, часто возникают неиспользованные импорт и переменные, и это может быть раздражающим, удалять их только для продолжения компиляции, чтобы они снова понадобились позже. Пустой идентификатор обеспечивает обходной путь.

Эта наполовину написанная программа имеет два неиспользованных импорта (fmt и io) и неиспользованную переменную (fd), так что она не скомпилируется, но было бы неплохо увидеть, верен ли пока код.

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: использовать fd.
}

Чтобы заставить замолчать жалобы о неиспользованном импорте, используйте пустой идентификатор для ссылки на символ из импортируемого пакета. Аналогично, присвоение неиспользуемой переменной fd пустому идентификатору заставит замолчать ошибку о неиспользованной переменной. Эта версия программы компилируется.

package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

var _ = fmt.Printf // Для отладки; удалите в дальнейшем.
var _ io.Reader    // Для отладки; удалите в дальнейшем.

func main() {
    fd, err := os.Open("test.go")
    if err != nil {
        log.Fatal(err)
    }
    // TODO: использовать fd.
    _ = fd
}

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

Импорт для побочного эффекта

Неиспользуемый импорт, такой как fmt или io в предыдущем примере в конечном итоге должен быть использован или удален: пустые назначения свидетельствуют о незавершенной работе над кодом. Но иногда полезно импортировать пакет только для его побочных эффектов, без какого-либо явного использования. Например, во время его функции init пакет net/http/pprof регистрирует обработчики HTTP, которые обеспечивают отладочную информацию. У него есть экспортированный API, но большинству клиентов нужна только регистрация обработчика и получить доступ к данным через веб-страницу. Чтобы импортировать пакет только для его побочных эффектов, переименуйте пакет на пустой идентификатор:

import _ "net/http/pprof"

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


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


воскресенье, 27 января 2019 г.

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

Мы уже упоминали пустой идентификатор пару раз в контексте for range циклов и карт. Пустой идентификатор может быть назначен или объявлен с любым значением любого типа, со значением отброшенным без каких-либо последствий. Это немного похоже в Unix на запись в файл /dev/null: это представляет значение доступное только для записи, для использования в качестве заполнителя, где переменная необходима, но фактическое значение не имеет значения. В этом и следующих постах будут представлены еще несколько вариантов использования пустого идентификатора.

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

Использование пустого идентификатора в цикле for range - это частный случай общей ситуации: множественное назначение.

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

if _, err := os.Stat(path); os.IsNotExist(err) {
    fmt.Printf("%s does not exist\n", path)
}

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

// Плохо! 
// Этот код завершится падением если путь не существует.
fi, _ := os.Stat(path)
if fi.IsDir() {
    fmt.Printf("%s is a directory\n", path)
}


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


суббота, 26 января 2019 г.

Эффективный Go: интерфейсы и методы

Поскольку почти все может иметь прикрепленные методы, почти все может удовлетворить интерфейс. Один иллюстративный пример находится в http пакете, который определяет интерфейс Handler. Любой объект, который реализует Handler, может обслуживать HTTP-запросы.

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

ResponseWriter сам по себе является интерфейсом, обеспечивающим доступ к методам, необходимым для возврата ответа клиенту. Эти методы включают стандартный метод Write, поэтому http.ResponseWriter можно использовать везде, где io.Writer может быть использован. Request - это структура, содержащая проанализированное представление запроса от клиента.

Для краткости давайте проигнорируем POST и предположим, что HTTP-запросы всегда GET; это упрощение не влияет на то, как обработчики настраиваются. Вот тривиальная, но полная реализация обработчика для подсчета, сколько раз страница посещена.

// Простой счетчик сервера.
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, 
                              req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

(Продолжая тему печати, обратите внимание, как Fprintf может печатать на http.ResponseWriter.) Для справки, вот как подключить такой сервер к узлу в дереве URL.

import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)

Но зачем делать Counter структурой? Целое число - это все, что нужно. (Получатель должен быть указателем, чтобы приращение было видно вызывающей стороне.)

// Упрощенный счетчик сервера.
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, 
                              req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

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

// Канал, который отправляет оповещение на каждый визит.
// (Вероятно вы звхотите, чтобы канал был буферизированным)
type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, 
                         req *http.Request) {
    ch <- req
    fmt.Fprint(w, "notification sent")
}

Наконец, скажем, мы хотели представить в /args аргументы используемые при вызове бинарного файла сервера. Легко написать функцию для вывода аргументов.

func ArgServer() {
    fmt.Println(os.Args)
}

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

// Тип HandlerFunc - это адаптер, позволяющий использовать
// обычные функции как обработчики HTTP. 
// Если f является функцией
// с соответствующей подписью, HandlerFunc(f) является
// Handler объекта, который вызывает f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP вызывает f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, 
                               req *Request) {
    f(w, req)
}

HandlerFunc - это тип с методом, ServeHTTP, поэтому значения этого типа могут обслуживать HTTP-запросы. Посмотрите на реализацию метода: получатель - это функция, f, и метод вызывает f. Это может показаться странным, но это не так уж отличается от, скажем, случая когда приемник является каналом, а метод отправляется по каналу.

Чтобы превратить ArgServer в HTTP-сервер, мы сначала изменим его, чтобы он имел правильную сигнатуру.

// Argument сервер.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

ArgServer теперь имеет ту же сигнатуру, что и HandlerFunc, поэтому он может быть преобразован в этот тип для доступа к его методам, как мы конвертировали Sequence в IntSlice для доступа к IntSlice.Sort. Код для установки сервера лаконичен:

http.Handle("/args", http.HandlerFunc(ArgServer))

Когда кто-то посещает страницу /args, обработчик, установленный на этой странице, имеет значение ArgServer и тип HandlerFunc. HTTP-сервер будет вызывать метод ServeHTTP этого типа, с ArgServer в качестве получателя, который в свою очередь вызовет ArgServer(с помощью вызова f(w, req) внутри HandlerFunc.ServeHTTP). Аргументы будут отображены.

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


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


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

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

В таких случаях конструктор должен возвращать значение интерфейса, а не тип реализации. Как пример, в хеш-библиотеках crc32.NewIEEE и adler32.New возвращают тип интерфейса hash.Hash32. Подстановка алгоритма CRC-32 для Adler-32 в Go программе требует только изменения вызова конструктора; остальная часть кода не зависит от изменения алгоритма.

Подобный подход позволяет алгоритмам потокового шифра в различных пакетах crypto быть отдельным от блочных шифров, которые они соединяют вместе. Интерфейс Block в пакете crypto/cipher указывает поведение блочного шифра, который обеспечивает шифрование одного блока данных. Затем по аналогии с пакетом bufio, пакеты шифров, которые реализуют этот интерфейс, могут быть использованы для построения потоковых шифров, представленных через интерфейс Stream, без знания деталей блока шифрования.

Интерфейсы crypto/cipher выглядят так:

type Block interface {
    BlockSize() int
    Encrypt(src, dst []byte)
    Decrypt(src, dst []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

Вот определение потока в режиме счетчика (CTR), который превращает блочный шифр в потоковый шифр; отметьте что детали блочного шифра абстрагированы:

// NewCTR возвращает поток, который шифрует/дешифрует, 
// используя данный блок в режиме счетчика. 
// Длина iv должна соответствовать размеру блока.
func NewCTR(block Block, iv []byte) Stream

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


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


Эффективный Go: преобразования интерфейсов и утверждения типа

Переключатели типов (type switch) являются формой преобразования: они принимают интерфейс и, для каждого case в switch, в некотором смысле, преобразовывают его в тип этого case. Вот упрощенная версия того, как код в fmt.Printf превращает значение в строкe с использованием переключателя типа. Если это уже строка, мы хотим, чтобы фактическое значение строки содержалось в интерфейсе, тогда как если оно имеет метод String мы хотим получить результат вызова метода.

type Stringer interface {
    String() string
}

// Значение, предоставленное вызывающим.
var value interface{} 
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

Первый case находит конкретное значение; второй преобразует интерфейс в другой интерфейс.

Что если есть только один тип, который нас интересует? Если мы знаем, что значение содержит string а мы просто хотим его извлечь? Переключатель типа с одним case подойдет, как и утверждение типа. Утверждение типа принимает значение интерфейса и извлекает из него значение указанного явного типа. Синтаксис заимствует из условия, открывающего переключатель типа, но с явным типом вместо ключевого слова type:

value.(typeName)

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

str := value.(string)

Но если окажется, что значение не содержит строку, программа завершится с ошибкой во время выполнения. Чтобы избежать этого, используйте идиому «comma, ok», чтобы безопасно проверить, является ли значение строкой:

str, ok := value.(string)
if ok {
    fmt.Printf("string value is: %q\n", str)
} else {
    fmt.Printf("value is not a string\n")
}

Если утверждение типа не выполнено, str все еще будет существовать и иметь тип string, но будет иметь нулевое значение, пустую строку.

В качестве иллюстрации возможности приведем if-else утверждение, которое эквивалентно переключателю типа, который мы привели в начале поста.

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}


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


Эффективный Go: интерфейсы, преобразования

Интерфейсы в Go предоставляют способ указать поведение объекта: если что-то может сделать это, тогда это можно использовать здесь. В предыдущих постах мы уже рассматривали пару простых примеров; пользовательские принтеры могут быть реализованы методом String, в то время как Fprintf может генерировать вывод для чего угодно с помощью метода Write. Интерфейсы только с одним или двумя методами распространены в коде Go и обычно им дается имя, полученное из метода, например io.Writer для чего-то, который реализует Write.

Тип может реализовывать несколько интерфейсов. Например, коллекция может быть отсортирована с помощью процедур в пакете sort, если он реализует sort.Interface, который содержит Len(), Less(i, j int) bool и Swap(i, j int), и он также может иметь собственный кастомный форматтер. В этом надуманном примере Sequence удовлетворяет обоим условиям.

type Sequence []int

// Методы требуемые sort.Interface.
func (s Sequence) Len() int {
    return len(s)
}
func (s Sequence) Less(i, j int) bool {
    return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
    s[i], s[j] = s[j], s[i]
}

// Метод для печати - сортирует элементы перед печатью.
func (s Sequence) String() string {
    sort.Sort(s)
    str := "["
    for i, elem := range s {
        if i > 0 {
            str += " "
        }
        str += fmt.Sprint(elem)
    }
    return str + "]"
}

Преобразования

Метод String в Sequence воссоздает работу, которую Sprint уже выполняет для срезов. Мы можем поделить усилия, если мы преобразуем Sequence в обычный []int перед вызовом Sprint.

func (s Sequence) String() string {
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

Этот метод является еще одним примером техники преобразования для вызова Sprintf безопасно из метода String. Поскольку два типа (Sequence и []int) одинаковы, если мы игнорируем имя типа, это допустимо проводить преобразование между ними. Преобразование не создает новое значение, оно просто временно действует как будто существующее значение имеет новый тип. (Существуют и другие допустимые преобразования, например, из целого числа в число с плавающей запятой, чтобы создать новое значение.)

Это идиома в Go программах - преобразовывать тип выражения для доступа к другому набору методов. В качестве примера мы могли бы использовать существующий тип sort.IntSlice, чтобы уменьшить весь пример:

type Sequence []int

// Метод для печати - сортирует элементы перед печатью.
func (s Sequence) String() string {
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

Теперь вместо Sequence реализуем несколько интерфейсов (сортировка и печать), мы используем возможность элемента данных быть преобразованными в несколько типов (Sequence, sort.IntSlice и []int), каждый из которых выполняет определенную часть работы. Это более необычно на практике, но может быть эффективным.


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


Эффективный Go: методы - указатели и значения

Указатели и значения

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

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

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // Тело точно такое же как в Append функции, 
    // определенной ранее.

    l := len(slice)
    if l + len(data) > cap(slice) {  // реаллоцируем
        // Аллоцирем в двойном размере требуемого, 
        // для будущего роста.
        newSlice := make([]byte, (l+len(data))*2)
        // copy функция предопределена 
        // и работает для любого типа среза.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

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

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Тело как ранее, без return.
    *p = slice
}

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

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Снова как ранее.
    *p = slice
    return len(data), nil
}

тогда тип *ByteSlice удовлетворяет стандартному интерфейсу io.Writer, что удобно. Например, мы можем распечатать в одном выражении.

var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)

Мы передаем адрес ByteSlice, потому что только *ByteSlice удовлетворяет io.Writer. Правило об указателях и значениях для получателей заключается в том, что методы-значения можно вызывать для указателей и значений, но методы указателей могут быть вызываны только на указателях.

Это правило возникает потому, что методы указателя могут модифицировать получателя; вызов их на значении приведет к тому, что метод получит копию значения, поэтому любые изменения будут отклонены. Поэтому язык не допускает этой ошибки. Однако есть удобное исключение. Когда значение адресуемое, язык заботится о частом случае вызова метода указателя на значение, вставляя адрес оператора автоматически. В нашем примере переменная b является адресуемой, поэтому мы можем вызвать его метод Write только с b.Write. Компилятор перепишет это в (&b).Write для нас.

Кстати, идея использования Write на срезе байтов занимает центральное место в реализации bytes.Buffer.


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


Эффективный Go: инициализация

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

Константы

Константы в Go - это просто константы. Они создаются во время компиляции, даже если они определены как локальные в функциях, и могут быть только числами, символами (рунами), строками или булевыми типами. Из-за ограничения времени компиляции выражения, которые определяют их должны быть константными выражениями, оценивающимися компилятором. Например,

1 << 3
является константным выражением, а math.Sin(math.Pi/4) нет, потому что вызов функции для math.Sin случится во время выполнения.

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

type ByteSize float64

const (
    // игнорируем первое значение, 
    // производя присваивание к пустому идентификатору
    _           = iota
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

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

func (b ByteSize) String() string {
    switch {
    case b >= YB:
        return fmt.Sprintf("%.2fYB", b/YB)
    case b >= ZB:
        return fmt.Sprintf("%.2fZB", b/ZB)
    case b >= EB:
        return fmt.Sprintf("%.2fEB", b/EB)
    case b >= PB:
        return fmt.Sprintf("%.2fPB", b/PB)
    case b >= TB:
        return fmt.Sprintf("%.2fTB", b/TB)
    case b >= GB:
        return fmt.Sprintf("%.2fGB", b/GB)
    case b >= MB:
        return fmt.Sprintf("%.2fMB", b/MB)
    case b >= KB:
        return fmt.Sprintf("%.2fKB", b/KB)
    }
    return fmt.Sprintf("%.2fB", b)
}

Выражение YB печатается как 1.00YB, в то время как ByteSize(1e13)печатается как 9.09TB.

Использование здесь Sprintf для реализации в ByteSize метода String безопасно (избегает бесконечного повторения) не из-за преобразования, а потому что он вызывает Sprintf с %f, который не является строковым форматом: Sprintf будет вызывать только метод String, когда ему нужна строка, и %f ожидает значение с плавающей точкой.

Переменные

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

var (
    home   = os.Getenv("HOME")
    user   = os.Getenv("USER")
    gopath = os.Getenv("GOPATH")
)

Функция init

Наконец, каждый исходный файл может определить свою собственную функцию init для установки любого необходимого состояния. (На самом деле каждый файл может иметь несколько init.) init вызывается после того как все объявления переменных в пакете были оценены их инициализаторами, и они оцениваются только после того, как все импортированные пакеты были инициализированы.

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

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath может быть переписан 
    // --gopath флагом в командной строке.
    flag.StringVar(&gopath, "gopath", gopath, 
                   "override default GOPATH")
}


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


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

В посте о срезах мы написали функцию Append для добавления элементов в срез:

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // реаллоцируем
        // Аллоцирем в двойном размере требуемого, 
        // для будущего роста.
        newSlice := make([]byte, (l+len(data))*2)
        // copy функция предопределена 
        // и работает для любого типа среза.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

Сигнатура append отличается от нашей пользовательской функции Append выше. Схематически:

func append(slice []T, elements ...T) []T

где T является заполнителем для любого данного типа. Вы не можете на самом деле написать функцию в Go, где тип T определяется вызывающим абонентом. Вот почему append встроен: он уждается в поддержке со стороны компилятора.

Что append делает - это добавляет элементы в конец среза и возвращает результат. Результат нужно вернуть потому что, как и в случае с нашим рукописным Append, основной массив может измениться. Вот простой пример:

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

печатает [1 2 3 4 5 6]. Таким образом, append работает немного похоже на Printf, собирая произвольное число аргументов.

Но что, если мы хотим сделать то, что делает наш Append, и добавить срез в срез? Легко: используйте ... при вызове места, как мы это делали при вызове Output выше. Следующий пример выдает результат, идентичный приведенному выше.

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

Без этого ... он не скомпилируется, потому что типы были бы неправильные; y не относится к типу int.


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


Эффективный Go: печать

Форматированная печать в Go использует стиль, похожий на стиль в C printf семье, но более богатая и более общая. Функции живут в fmt пакете и имеют заглавные имена: fmt.Printf, fmt.Fprintf, fmt.Sprintf и так далее. Строковые функции (Sprintf и т. д.) возвращают строку, а не заполняют предоставленный буфер.

Вам не нужно предоставлять строку формата. Для каждого из Printf, Fprintf и Sprintf есть другая пара функций, например Print и Println. Эти функции не принимают строку формата, а генерируют формат по умолчанию для каждого аргумента. Версии Println также содержат пробел между аргументами и добавляют новую строку к выводу, а версии Print добавляют пробелы только в том случае, если операнд с обеих сторон является строкой. В следующем примере каждая строка выдает один и тот же результат.

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

Функции форматированной печати fmt.Fprint и похожие на нее принимают в качестве первого аргумента любой объект, который реализует интерфейс io.Writer; переменные os.Stdout и os.Stderr являются известными примерами таких объектов.

Здесь вещи начинают расходиться с C. Во-первых, числовые форматы, такие как %d не принимают флаги для подписи или размера; вместо этого процедуры печати используют тип аргумента для определения этих свойств.

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

печатает

18446744073709551615 ffffffffffffffff; -1 -1

Если вы просто хотите преобразование по умолчанию, например десятичное для целых чисел, вы можете использовать формат, принимающий любые форматы, %v ("value"); результат точно такой же что будут производить Print и Println. Кроме того, этот формат может печатать любое значение, даже массивы, срезы, структуры и карты. Вот оператор печати для карты часовых поясов, определенной в предыдущем посте.

fmt.Printf("%v\n", timeZone)  
// или просто fmt.Println(timeZone)

который дает вывод

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

Конечно, для карт ключи могут быть выведены в любом порядке. При печати структуры измененный формат %+v аннотирует поля структуры с их именами, а для любого значения альтернативный format %#v печатает значение в полном синтаксисе Go.

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

печатает

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, 
                "EST":-18000, "UTC":0, "MST":-25200}

(Обратите внимание на амперсанды.) Этот формат строки в кавычках также доступен через %q, когда применяется к значению типа string или []byte. Альтернативный формат %#q будет использовать обратные кавычки, если это возможно. (Формат %q также применяется к целым числам и рунам, создавая руну константа в кавычках.) Кроме того, %x работает со строками, байтовыми массивами и байтовыми срезами, а также с целыми числами, генерируя длинную шестнадцатеричную строку, а с пробелом в формате (% x) - ставит пробелы между байтами.

Другой удобный формат - это %T, который печатает тип значения.

fmt.Printf("%T\n", timeZone)

печатает

map[string] int

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

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

для печати в формате

7/-2.35/"abc\tdef"

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

Наш метод String может вызывать Sprintf, потому что процедуры печати полностью реентерабельны и могут быть завернуты таким образом. Есть одна важная деталь, для понимания об этом подходе: не создавайте метод String, вызывая Sprintf таким образом, что он будет повторяться в вашем String методе до бесконечности. Это может произойти, если Sprintf вызов пытается распечатать получателя напрямую в виде строки, которая в свою очередь снова вызовет метод. Это распространенная ошибка, которую легко допустить, как показывает следующий пример.

type MyString string
func (m MyString) String() string {
    // Ошибка: будет возвращаться бесконечно.
    return fmt.Sprintf("MyString=%s", m) 
}

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

type MyString string
func (m MyString) String() string {
    // OK: обратите внимание на преобразование.
    return fmt.Sprintf("MyString=%s", string(m)) 
}

Другой метод печати заключается в передаче аргументов процедуры печати непосредственно другой такой программе. Сигнатура Printf использует тип ... interface {} в качестве последнего аргумента для указания, что означает - произвольное количество параметров (произвольного типа) может появиться после формата.

func Printf(format string, v ...interface{}) (n int, err error) {

В функции Printf v действует как переменная типа []interface{}, но если он передается другой функции с переменным числом, он действует как регулярный список аргументов. Вот реализация функции log.Println, которую мы использовали выше. Она передает свои аргументы непосредственно fmt.Sprintln для фактического форматирования.

// Println печатает стандартный логгер в манере fmt.Println
func Println(v ...interface{}) {
    // Output принимает параметры (int, string)
    std.Output(2, fmt.Sprintln(v...))  
}

Мы пишем ... после v во вложенном вызове Sprintln, чтобы сообщить компилятору рассматривать v как список аргументов; в противном случае он просто передал бы v как один аргумент среза.

В печати есть даже больше, чем мы рассмотрели здесь. Смотрите документацию по godoc для пакета fmt для деталей.

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

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // наибольший int
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}


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


Эффективный Go: карты (maps)

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

Карты могут быть построены с использованием обычного составного литерального синтаксиса с разделенными двоеточиями парами ключ-значение, поэтому их легко построить во время инициализации.

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

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

offset := timeZone["EST"]

Попытка получить значение карты с ключом, который отсутствует в карте вернет нулевое значение для типа из записей в карте. Например, если карта содержит целые числа, поиск несуществующего ключа вернет 0. Сет (набор уникальных значений) может быть реализован как карта с типом значения bool. Задайте для элемента карты значение true, чтобы поместить значение в сет, а затем проверяйте его с помощью простой индексации.

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

// условие будет false если персона не в карте
if attended[person] { 
    fmt.Println(person, " был на встрече")
}

Иногда вам нужно отличить отсутствующую запись от нулевого значения. Есть ли запись для "UTC" или это 0, потому что его нет в карте вообще? Вы можете различать с помощью формы множественного назначения.

var seconds int
var ok bool
seconds, ok = timeZone[tz]

По понятным причинам это называется “comma ok” идиома. В этом примере, если присутствует tz, seconds будет установлен соответствующим образом, и ok будет true; если нет, seconds будет установлен равным нулю, а ok быть false. Вот функция, которая объединяет это с хорошим сообщением об ошибке:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

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

_, present := timeZone[tz]

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

delete(timeZone, "PDT")


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


пятница, 25 января 2019 г.

Эффективный Go: двумерные срезы

Массивы и срезы в Go являются одномерными. Чтобы создать эквивалент двумерного массива или среза, необходимо определить массив массивов или срез срезов, например:

// 3x3 массив, на самом деле массив массивов.
type Transform [3][3]float64  

// Срез byte срезов.
type LinesOfText [][]byte     

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

text := LinesOfText{
    []byte("Now is the time"),
    []byte("for all good gophers"),
    []byte("to bring some fun to the party."),
}

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

// Аллоцируем срез самого верхнего уровня.
// Один ряд на каждый элемент y.
picture := make([][]uint8, YSize) 

// Цикл по рядам, аллоцируем срез для каждого ряда.
for i := range picture {
    picture[i] = make([]uint8, XSize)
}

И второй способ - как одно аллоцирование, разбитое на строки:

// Аллоцируем срез самого верхнего уровня, 
// такой же как и прежде.
// Один ряд на каждый элемент y.
picture := make([][]uint8, YSize) 

// Аллоцируем один большой срез для хранения всех пикселей.
// Имеем тип []uint8 даже хотя изображение это [][]uint8.
pixels := make([]uint8, XSize*YSize) 

// Цикл по рядам, создаем срез каждого ряда 
// от начала среза оставшихся пикселей.
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}


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


четверг, 24 января 2019 г.

Эффективный Go: срезы (slices)

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

Срезы содержат ссылки на базовый массив, и если вы назначите один срез к другому, оба будут ссылаться на один и тот же массив. Если функция принимает аргумент срез, изменения, которые она вносит в элементы среза, будут видны вызывающей стороне, аналогично передаче указателя на базовый массив. Read функция поэтому может принимать аргумент срез, а не указатель и счет; длина внутри среза устанавливает верхний предел того, сколько данных для чтения. Вот сигнатура метода Read типа File в пакете os:

func (f *File) Read(buf []byte) (n int, err error)

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

n, err := f.Read(buf[0:32])

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

var n int
var err error
for i := 0; i < 32; i++ {
    nbytes, e := f.Read(buf[i:i+1])  // Читаем один байт.
    if nbytes == 0 || e != nil {
        err = e
        break
    }
    n += nbytes
}

Длина среза может быть изменена до тех пор, пока он все еще вписывается в пределы базового массива; просто назначьте его на срез самого себя. Емкость среза, доступная встроенной функции cap, сообщает максимальную длину среза, которую можно предполагать. В следующем примере представлена функция для добавления данных в срез. Если данные превышает емкость, срез перераспределяется. Полученный срез возвращается. Функция использует тот факт, что len и cap являются законными применительно к nil и возвращает 0 в этом случае.

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // реаллоцируем
        // Аллоцирем в двойном размере требуемого, 
        // для будущего роста.
        newSlice := make([]byte, (l+len(data))*2)
        // copy функция предопределена 
        // и работает для любого типа среза.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

Мы должны вернуть срез позже, потому что, хотя Append может изменять элементы slice, сам срез (структура данных времени выполнения, содержащая указатель, длину и емкость) передается по значению.

Идея добавления к срезу очень полезна, и используется встроенной функцией append.


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


Эффективный Go: массивы

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

Существуют большие различия между способами работы массивов в Go и C. В Go

  • Массивы являются значениями. Присвоение одного массива другому копирует все элементы.
  • В частности, если вы передаете массив функции, она получит копию массива, а не указатель на него.
  • Размер массива является частью его типа. Типы [10]int и [20]int различны.

Свойство value может быть полезным, но также и дорогим; если вы хотите C-подобное поведение и эффективность, вы можете передать указатель на массив.

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // Отметьте явный оператор адреса

Но даже этот стиль не идиоматичен. Вместо этого используйте срезы.


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


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

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

make([]int, 10, 100)

аллоцирует массив из 100 int'ов, а затем создает структуру среза длиной 10 и вместимостью 100, указывающую на первые 10 элементов массива. (При создании среза емкость можно опустить) Напротив, new([] int) возвращает указатель на вновь выделенный обнуленную структуру среза, то есть указатель на значение фрагмента nil.

Следующие примеры иллюстрируют разницу между new и make.

// аллоцирует структуру среза; *p == nil; используется редко
var p *[]int = new([]int) 

// срез v теперь ссылается на новый массив из 100 int'ов      
var v  []int = make([]int, 100) 

// Излишне сложно:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// Идиоматично:
v := make([]int, 100)

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


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