воскресенье, 3 февраля 2019 г.

Веб-приложение на Go: введение в пакет net/http, пример веб-сервера на Go

В качестве введения в пакет net/http рассмотрим полностью рабочий пример простого веб-сервера:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Функция main начинается с вызова http.HandleFunc, который сообщает пакету http обрабатывать все корневые веб запросы ("/") с помощью handler.

Затем он вызывает http.ListenAndServe, указывая, что он должен прослушивать порт 8080 на любом интерфейсе (":8080"). (Не беспокойтесь о втором параметре, nil, пока.) Эта функция будет блокироваться до завершения программы.

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

Функция handler имеет тип http.HandlerFunc. Он принимает http.ResponseWriter и http.Request как его аргументы.

Значение http.ResponseWriter собирает ответ HTTP-сервера; написав в него, мы отправляем данные HTTP-клиенту.

http.Request - это структура данных, которая представляет клиентский HTTP-запрос. r.URL.Path является компонентом пути URL запроса. Конечный [1:] означает "создать под-срез Path от 1-го символа до конца." Это удаляет ведущий "/" из имени пути.

Если вы запустите эту программу и обратитесь к URL:

http://localhost:8080/monkeys

программа представит страницу, содержащую:

Hi there, I love monkeys!


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


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

Веб-приложение на Go: структуры данных, сохранение и загрузка страницы

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

type Page struct {
    Title string
    Body  []byte
}

Тип []byte означает «byte срез». (См. Срезы) Элемент Body является []byte, а не string, потому что это тип, ожидаемый io библиотекой, которую мы будем использовать, как вы увидите далее.

Структура Page описывает, как данные страницы будут храниться в памяти. Но как насчет постоянного хранения? Мы можем решить эту проблему, создав метод save на Page:

func (p *Page) save() error {
    filename := p.Title + ".txt"
    return ioutil.WriteFile(filename, p.Body, 0600)
}

Сигнатура этого метода гласит: "Это метод с именем save, который принимает в качестве получателя p указатель на Page. Не принимает параметров и возвращает значение типа error."

Этот метод сохраняет Body структуры Page в текстовом файле. Для простоты мы будем использовать Title в качестве имени файла.

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

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

Помимо сохранения страниц, мы также хотим загружать страницы:

func loadPage(title string) *Page {
    filename := title + ".txt"
    body, _ := ioutil.ReadFile(filename)
    return &Page{Title: title, Body: body}
}

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

Функции могут возвращать несколько значений. Стандартная функция библиотеки io.ReadFile возвращает []byte и error. В loadPage ошибка еще не обрабатывается; «пустой идентификатор» представленный символом подчеркивания (_) используется для удаления возвращаемого значения ошибки (по сути, присвоение значения ничему).

Но что произойдет, если ReadFile обнаружит ошибку? Например, файл может не существовать. Мы не должны игнорировать такие ошибки. Давайте изменим функцию для возврата *Page и error.

func loadPage(title string) (*Page, error) {
    filename := title + ".txt"
    body, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return &Page{Title: title, Body: body}, nil
}

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

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

func main() {
    p1 := &Page{Title: "TestPage", 
                Body: []byte("This is a sample Page.")}
    p1.save()
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))
}

После компиляции и выполнения этого кода файл с именем TestPage.txt будет создан, содержащий содержимое p1. Затем файл будет считан в структуру p2 и ее элемент Body будет выведен на экран.

Вы можете скомпилировать и запустить программу следующим образом:

$ go build wiki.go
$ ./wiki
This is a sample Page.

(Если вы используете Windows, вы должны ввести «wiki» без «./» для запуска программы.)

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

package main

import (
  "fmt"
  "io/ioutil"
)

type Page struct {
  Title string
  Body  []byte
}

func (p *Page) save() error {
  filename := p.Title + ".txt"
  return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error) {
  filename := title + ".txt"
  body, err := ioutil.ReadFile(filename)
  if err != nil {
    return nil, err
  }
  return &Page{Title: title, Body: body}, nil
}

func main() {
  p1 := &Page{Title: "TestPage", 
              Body: []byte("This is a sample Page.")}
  p1.save()
  p2, _ := loadPage("TestPage")
  fmt.Println(string(p2.Body))
}


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


Веб-приложение на Go: введение

В серии постов веб-приложение на Go мы осветим следующие разделы:

  • Создание структуры данных с использованием методов load и save
  • Использование пакета net/http для создания веб-приложений
  • Использование пакета html/template для обработки HTML шаблонов
  • Использование пакета regexp для проверки ввода пользователя
  • Использование замыканий

Начало работы

Вам потребуется компьютер с ОС Linux, OS X или Windows для запуска Go. Мы будем использовать $ для представления командной строки.

Установите Go.

Создайте новый каталог для нового приложения внутри GOPATH и перейдите к нему:

$ mkdir gowiki
$ cd gowiki

Создайте файл с именем wiki.go, откройте его в своем предпочитаемом редакторе и добавьте следующие строки:

package main

import (
    "fmt"
    "io/ioutil"
)

Мы импортируем пакеты fmt и ioutil из стандартной библиотеки Go. Позже, когда мы реализуем дополнительную функциональность, мы будем добавим больше пакетов в это объявление import.


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


Эффективный 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, который мы можем изучить для получения дополнительной информации об ошибке.


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