Показаны сообщения с ярлыком Эффективный Go. Показать все сообщения
Показаны сообщения с ярлыком Эффективный Go. Показать все сообщения

суббота, 2 февраля 2019 г.

Эффективный Go: веб-сервер

В завершение серии постов Эффективный Go мы рассмотрим полностью готовую программу, веб-сервер. Это на самом деле своего рода веб-сервер. Google предоставляет сервис на chart.apis.google.com, который делает автоматическое форматирование данных в диаграммах и графиках. Его трудно использовать в интерактивном режиме, хотя бы потому что вам нужно поместить данные в URL как запрос. Программа здесь предоставляет более приятный интерфейс для одной формы данных: с учетом короткого фрагмента текста, он вызывает на сервере карт для создания QR-кода, матрицы блоков, которые кодируют текст. Это изображение может быть захвачено камерой вашего мобильного телефона и интерпретировано как, например, URL-адрес, сохраняющий ввод текста на крошечной клавиатуре телефона.

Вот полная программа. Объяснение следует.

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"
)

var addr = flag.String("addr", ":1718", 
                "http service address") // Q=17, R=18

var templ = template.Must(
                template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()
    http.Handle("/", http.HandlerFunc(QR))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

func QR(w http.ResponseWriter, req *http.Request) {
    templ.Execute(w, req.FormValue("s"))
}

const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET">
<input maxLength=1024 size=70 
name=s value="" title="Text to QR Encode">
<input type=submit value="Show QR" name=qr>
</form>
</body>
</html>
`

Части до main должны быть легко понятны. Один флаг устанавливает HTTP-порт по умолчанию для нашего сервера. Переменная шаблона templ - это место, где происходит действие. Он строит шаблон HTML, который будет выполнен сервером для отображения страницы.

Функция main анализирует флаги и, используя механизм, о котором мы говорили выше, связывает функцию QR с корневым путем для сервера. Затем вызывается http.ListenAndServe для запуска сервера; он блокируется во время работы сервера.

QR просто получает запрос, который содержит данные формы, и выполняет шаблон для данных в значении формы с именем s.

Пакет шаблонов html/template имеет множество применений; эта программа только затрагивает его возможности. По сути, он переписывает фрагмент текста HTML на лету, заменяя полученные элементы из элементов данных, передаваемых в templ.Execute, в этом случае - значение формы. В тексте шаблона (templateStr) части, разделенные двойной скобкой, обозначают действия шаблона. Часть от {{if.}} до {{end}} выполняется только в том случае, если значение текущего элемента данных, называемое .(точка), не пустое. То есть, когда строка пуста, этот фрагмент шаблона подавляется.

Два фрагмента {{.}} говорят, что показывают данные, представленные шаблону - строка запроса - на веб-странице. Пакет HTML шаблона автоматически обеспечивает соответствующее экранирование, поэтому текст безопасен для отображения.

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

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


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


Эффективный Go: восстановление (recover)

Когда вызывается panic, в том числе неявно для ошибок времени выполнения (run-time errors), такие как индексация среза за пределами или сбой утверждения типа, оно немедленно останавливает выполнение текущей функции и начинает разматывать стек go-процедуры (goroutine), запуская любые отложенные функции по пути. Если это раскручивание достигает вершины стека go-процедуры, программа умирает. Тем не менее, можно использовать встроенную функцию restore для восстановления контроля go-процедуры и возобновления нормального исполнения.

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

Одно из применений restore - отключение отказавшей go-процедуры внутри сервера, не убивая другие исполняемые go-процедуры.

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

В этом примере, если do(work) паникует, результат будет сохранен в логе и go-процедура будет выходить чисто, не нарушая другие. Нет необходимости делать что-либо еще в отсроченном закрытии; вызов restore полностью обрабатывает ситуацию.

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

С нашим шаблоном восстановления, do функция (и все, что она вызывает) может выйти из любой плохой ситуации чисто, вызвав panic. Мы можем использовать эту идею для упрощения обработку ошибок в сложном программном обеспечении. Давайте посмотрим на идеализированную версию пакета regexp, который сообщает об ошибках парсиннга путем вызова panic с локальным типом ошибки. Вот определение Error, метода error и функции Compile.

// Error это тип ошибки разбора(парсинга); 
// он удовлетворяет error интерфейсу
type Error string
func (e Error) Error() string {
    return string(e)
}

// error это метод *Regexp, 
// который сообщает об ошибках разбора (парсинга),
// вызывая panic с Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile возвращает разобранное представление
// регулярного выражения.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse будет вызывать panic 
    // если есть ошибка разбора.
    defer func() {
        if e := recover(); e != nil {
            // Очищаем возвращаемое значение.
            regexp = nil  
            // Будет повторная panic 
            // если нет ошибки разбора.  
            err = e.(Error) 
        }
    }()
    return regexp.doParse(str), nil
}

Если doParse паникует, блок восстановления установит возвращаемое значение равным nil - отложенные функции могут изменять именованные возвращаемые значения. Затем происходит проверка, в назначении err, что проблема была в ошибке разбора, утверждая что он имеет локальный тип Error. Если этого не произойдет, утверждение типа не будет выполнено, что приведет к ошибке во время выполнения, которая продолжит раскручивать стек, как будто ничего не прерывалось. Эта проверка означает, что если происходит что-то неожиданное, например запрос индекса вне пределов, код не будет работать, даже если мы используем panic и recover для обработки ошибок разбора.

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

if pos == 0 {
    re.error("'*' недопустимо в начале выражения")
}

Хотя этот шаблон полезен, его следует использовать только внутри пакета. Parse превращает свои внутренние вызовы panic в значения error; он не подвергает panic своему клиенту. Это хорошее правило для использования в своей практике.

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


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


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

Обычный способ сообщить об ошибке вызывающей стороне - вернуть error как дополнительное возвращаемое значение. Канонический метод Read является хорошо известным примером; он возвращает количество прочитанных байтов и error. Но что, если ошибка не может быть возвращена? Иногда программа просто не может продолжаться.

Для этого есть встроенная функция panic, которая в действительности создает ошибку во время выполнения (run-time error), которая остановит программу. Функция принимает один аргумент произвольного типа - часто строку - для печати в качестве сообщения о том, что программа завершается. Это также способ указать, что что-то невозможное произошло, например, выход из бесконечного цикла.

// Игрушечная реализация корня куба 
// с использованием метода Ньютона.
func CubeRoot(x float64) float64 {
    z := x/3   // Произвольное начальное значение
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // миллион итераций не сходится; что-то не так.
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

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

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("нет значения $USER")
    }
}


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


пятница, 1 февраля 2019 г.

Эффективный Go: ошибки

Библиотечные процедуры часто должны возвращать какую-то индикацию ошибки вызывающему. Множественные вовзращаемые значения в Go делают простым возврат подробного описания ошибки вместе с нормальным возвращаемым значением. Рекомендуется использовать эту возможность для предоставления подробной информации об ошибках. Например, как мы увидим, os.Open не просто возвращает указатель nil при ошибке, он также возвращает значение ошибки, которая описывает, что пошло не так.

Как правило, ошибки имеют тип error, простой встроенный интерфейс.

type error interface {
    Error() string
}

Автор библиотеки может свободно реализовать этот интерфейс с более богатой моделью под капотом, что делает возможным не только увидеть саму ошибку, но и предоставить некоторый контекст. Как уже упоминалось, наряду с обычным *os.File возвращаемым значением, os.Open также возвращает значение ошибки. Если файл успешно открыт, ошибка будет nil, но когда есть проблема, он будет содержать os.PathError:

// PathError записывает ошибку и операцию и
// путь к файлу, который ее вызвал.
type PathError struct {
    Op string    // "open", "unlink", и т.д.
    Path string  // Связанный файл.
    Err error    // Возвращается системным вызовом.
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

Error в PathError генерирует похожую строку:

open /etc/passwx: no such file or directory

Такая ошибка, которая включает проблемное имя файла, операцию, и ошибку операционной системы, которую это вызвало, полезна даже если напечатана далеко от вызова, который вызвал ее; это гораздо более информативно, чем просто "no such file or directory".

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

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

for try := 0; try < 2; try++ {
    file, err = os.Create(filename)
    if err == nil {
        return
    }
    if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
        deleteTempFiles()  // Восстановить некоторое пространство.
        continue
    }
    return
}

Второй оператор if здесь - это утверждение типа. Если произойдет сбой, ok будет ложным, а e будет nil. Если все пройдет успешно, ok будет true, означающее что ошибка была типа *os.PathError, а затем e, который мы можем изучить для получения дополнительной информации об ошибке.


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


четверг, 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.


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