пятница, 14 февраля 2020 г.

Обработка ошибок в Golang

Если вы писали какой-либо код на Go, вы, вероятно, сталкивались со встроенным типом error. Код Go использует значения error, чтобы указать ненормальное состояние. Например, функция os.Open возвращает ненулевое значение error, когда не удается открыть файл.

func Open(name string) (file *File, err error)

Следующий код использует os.Open для открытия файла. Если возникает ошибка, она вызывает log.Fatal, чтобы распечатать сообщение об ошибке и остановить исполнение.

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// делаем что-либо с открытым *File f

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

Тип error

Тип error - это тип интерфейса. Переменная error представляет любое значение, которое может быть описано как строка. Вот объявление интерфейса:

type error interface {
    Error() string
}

Тип error, как и для всех встроенных типов, предварительно объявлен в блоке юниверса (universe block).

Наиболее часто используемая реализация error - это неэкспортируемый тип errorString пакета errors.

// errorString это тривиальная реализация error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

Вы можете создать одно из этих значений с помощью функции errors.New. Он принимает строку, которая преобразуется в error.errorString и возвращает значение error.

// New возвращает ошибку, которая форматируется как заданный текст.
func New(text string) error {
    return &errorString{text}
}

Вот как вы можете использовать errors.New:

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // реализация
}

Вызывающая сторона, передающая отрицательный аргумент в Sqrt, получает ненулевое значение ошибки (конкретное представление которого является значением errors.errorString). Вызывающая сторона может получить доступ к строке ошибки ("math: square root of..."), вызвав метод Error ошибки или просто распечатав его:

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

Пакет fmt форматирует значение ошибки, вызывая его строковый метод Error().

Ответственность за реализацию ошибки заключается в обобщении контекста. Ошибка, возвращаемая форматом os.Open как "open /etc/passwd: permission denied", а не просто "permission denied". Ошибка, возвращаемая нашим Sqrt, содержит информацию о недопустимом аргументе.

Чтобы добавить эту информацию, полезной функцией является Errorf пакета fmt. Она форматирует строку в соответствии с правилами Printf и возвращает ее как ошибку, созданную errors.New.

if f < 0 {
    return 0, fmt.Errorf("math: square root of negative number %g", f)
}

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

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

type NegativeSqrtError float64

func (f NegativeSqrtError) Error() string {
    return fmt.Sprintf("math: square root of negative number %g", float64(f))
}

Сложный вызывающий объект может затем использовать утверждение типа, чтобы проверить наличие NegativeSqrtError и обработать его специально, в то время как вызывающие элементы, которые просто передают ошибку в fmt.Println или log.Fatal, не увидят никаких изменений в поведении.

В качестве другого примера, пакет json указывает тип SyntaxError, который функция json.Decode возвращает, когда сталкивается с синтаксической ошибкой при разборе BLOB-объекта JSON.

type SyntaxError struct {
    msg    string // описание ошибки
    Offset int64  // ошибка произошла после чтения Offset байтов
}

func (e *SyntaxError) Error() string { return e.msg }

Поле Offset даже не отображается в стандартном формате ошибки, но абоненты могут использовать его для добавления информации о файлах и строках в свои сообщения об ошибках:

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

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

package net

type Error interface {
    error
    Timeout() bool   // ошибка истекла?
    Temporary() bool // ошибка временная?
}

Клиентский код может проверить net.Error с утверждением типа, а затем отличить временные сетевые ошибки от постоянных. Например, веб-сканер может перевести в спящий режим и повторить попытку, если он обнаружит временную ошибку, и в противном случае откажется.

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

Упрощение повторяющейся обработки ошибок

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

Рассмотрим приложение App Engine с обработчиком HTTP, который извлекает запись из хранилища данных и форматирует ее с помощью шаблона.

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Эта функция обрабатывает ошибки, возвращаемые функцией datastore.Get и методом Execute viewTemplate. В обоих случаях он представляет пользователю простое сообщение об ошибке с кодом состояния HTTP 500 ("Internal Server Error"). Это похоже на управляемый объем кода, но добавьте еще несколько обработчиков HTTP, и вы быстро получите много копий идентичного кода обработки ошибок.

Чтобы уменьшить количество повторений, мы можем определить наш собственный тип HTTP appHandler, который включает возвращаемое значение ошибки:

type appHandler func(http.ResponseWriter, *http.Request) error

Затем мы можем изменить нашу функцию viewRecord, чтобы она возвращала ошибки:

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

Это проще, чем оригинальная версия, но пакет http не понимает функции, которые возвращают ошибку. Чтобы это исправить, мы можем реализовать метод ServeHTTP интерфейса http.Handler в appHandler:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Метод ServeHTTP вызывает функцию appHandler и отображает возвращенную ошибку (если есть) пользователю. Обратите внимание, что получатель метода, fn, является функцией. (Go может сделать это!) Метод вызывает функцию, вызывая получателя в выражении fn(w, r).

Теперь при регистрации viewRecord в пакете http мы используем функцию Handle (вместо HandleFunc), так как appHandler является http.Handler (а не http.HandlerFunc).

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

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

Для этого мы создаем структуру appError, содержащую ошибку и некоторые другие поля:

type appError struct {
    Error   error
    Message string
    Code    int
}

Далее мы модифицируем тип appHandler, чтобы он возвращал значения *appError:

type appHandler func(http.ResponseWriter, *http.Request) *appError

(Обычно является ошибкой возвращать конкретный тип ошибки, а не error, но в данном случае это правильно, потому что ServeHTTP - единственное место, которое видит значение и использует его содержимое.)

И изменяем метод appHandler ServeHTTP, чтобы он отображал сообщение appError для пользователя с правильным кодом состояния HTTP и регистрировал полную ошибку на консоли разработчика:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e это *appError, а не os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

Наконец, мы обновляем viewRecord на новую сигнатуру функции и возвращаем больше контекста при обнаружении ошибки:

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

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

Это не конец; мы можем еще больше улучшить обработку ошибок в нашем приложении. Вот некоторые идеи:

  • дать обработчику ошибок красивый HTML-шаблон,
  • упростить отладку, записав трассировку стека в ответ HTTP, когда пользователь является администратором,
  • написать функцию конструктора для appError, которая хранит трассировку стека для упрощения отладки,
  • recover (восстанавливаться) от panic внутри appHandler, записав ошибку на консоль как "Critical" и сообщив пользователю "произошла серьезная ошибка". Это удобно, чтобы не подвергать пользователя непостижимым сообщениям об ошибках, вызванным ошибками программирования.

Заключение

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


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


четверг, 13 февраля 2020 г.

Defer, Panic, и Recover в Golang

Go имеет обычные механизмы управления потоком: if, for, switch, goto. Он также содержит утверждение (statement) go для запуска кода в отдельной goroutine. Но в этом посте мы обсудим некоторые из менее распространенных утверждений для управления потоком исполнения программы в Go: defer, panic, и recover.

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

Например, давайте посмотрим на функцию, которая открывает два файла и копирует содержимое одного файла в другой:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }

    written, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return
}

Это работает, но есть ошибка. Если вызов os.Create завершится неудачно, функция вернется без закрытия исходного файла. Это можно легко исправить, поместив вызов src.Close перед вторым return утверждением, но если бы функция была более сложной, проблему было бы не так легко заметить и решить. Вводя defer утверждения, мы можем гарантировать, что файлы всегда закрыты:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

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

Поведение defer утверждений является простым и предсказуемым. Есть три простых правила:

1. Аргументы отложенной функции оцениваются, когда оценивается оператор defer.

В этом примере выражение "i" вычисляется при отсрочке вызова Println. Отложенный вызов выведет "0" после возврата из функции.

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

2. Отложенные вызовы функций выполняются в порядке "последний пришел - первый вышел" (Last In First Out, LIFO) после возврата окружающей функции.

Эта функция печатает "3210":

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

3. Отложенные функции могут читать и присваивать возвращаемой функции именованные возвращаемые значения.

В этом примере отложенная функция увеличивает возвращаемое значение i после возврата окружающей функции. Таким образом, эта функция возвращает 2:

func c() (i int) {
    defer func() { i++ }()
    return 1
}

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

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

Recover - это встроенная функция, которая восстанавливает контроль над паникующими goroutine. Recover полезна только внутри отложенных (defer) функций. Во время обычного выполнения, вызов recover вернет nil и не будет иметь никакого другого эффекта. Если текущая goroutine вызывает panic, вызов recover захватит значение, переданное panic, и возобновит нормальное выполнение.

Вот пример программы, которая демонстрирует механику panic и recover:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

Функция g принимает int i и паникует, если i больше 3, иначе она вызывает себя с аргументом i + 1. Функция f откладывает функцию, которая вызывает recover и печатает восстановленное значение (если оно не равно нулю).

Программа выведет:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Если мы удалим отложенную (defer) функцию из f, panic не восстановится и достигнет вершины стека вызовов программы, завершив программу. Эта измененная программа выведет:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
 
panic PC=0x2a9cd8
[stack trace omitted]

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

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

Другие способы использования defer (кроме приведенный выше примера file.Close) включают освобождение мьютекса:

mu.Lock()
defer mu.Unlock()

печать нижнего колонтитула:

printHeader()
defer printFooter()

и другие.

Таким образом, оператор defer (с panic и recover или без них) обеспечивает необычный и мощный механизм управления потоком исполнения программы. Он может использоваться для моделирования ряда функций, реализованных структурами специального назначения в других языках программирования.


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


среда, 12 февраля 2020 г.

Срезы в Golang: внутреннее устройство и использование

Тип среза (slice) в Go предоставляет удобный и эффективный способ работы с последовательностями типизированных данных. Срезы аналогичны массивам на других языках, но имеют некоторые необычные свойства. В этом посте мы рассмотрим, что такое срезы и как они используются.

Массивы

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

Определение типа массива определяет длину и тип элемента. Например, тип [4]int представляет массив из четырех целых чисел. Размер массива фиксирован; его длина является частью его типа ([4]int и [5]int являются различными несовместимыми типами). Массивы можно индексировать обычным способом, поэтому выражение s[n] обращается к n-му элементу, начиная с нуля.

var a [4]int
a[0] = 1
i := a[0]
// i == 1

Массивы не нужно явно инициализировать; нулевое значение массива - это готовый массив, элементы которого сами обнуляются:

// a[2] == 0, нулевое значение типа int

Представление [4]int в памяти - это всего лишь четыре целочисленных значения, расположенных последовательно:

Массивы в Go являются значениями. Переменная массива обозначает весь массив; это не указатель на первый элемент массива (как в случае с C). Это означает, что когда вы присваиваете или передаете значение массива, вы будете делать копию его содержимого. (Чтобы избежать копирования, вы можете передать указатель на массив, но тогда это указатель на массив, а не массив.) Один из способов представить себе массивы - считать их некоей структурой, но с индексированными, а не именованными полями: составное значение с жестко заданным размером.

Литерал массива может быть указан так:

b := [2]string{"Ivan", "Anton"}

Или вы можете сделать так, чтобы компилятор подсчитал для вас элементы массива:

b := [...]string{"Ivan", "Anton"}

В обоих случаях тип b - это [2]string.

Срезы

Массивы имеют свое место, но они немного негибкие, поэтому вы не видите их слишком часто в коде Go. Срезы, однако, есть везде. Они основаны на массивах, чтобы обеспечить большую мощность и удобство.

Спецификация типа для среза: []T, где T - тип элементов среза. В отличие от типа массива, тип среза не имеет заданной длины.

Литерал среза объявляется так же, как литерал массива, за исключением того, что вы пропускаете количество элементов:

letters := []string{"a", "b", "c", "d"}

Срез может быть создан с помощью встроенной функции под названием make, которая имеет сигнатуру,

func make([]T, len, cap) []T

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

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

Если аргумент емкости (cap) опущен, по умолчанию используется указанная длина. Вот более краткая версия того же кода:

s := make([]byte, 5)

Длину и емкость среза можно проверить с помощью встроенных функций len и cap.

len(s) == 5
cap(s) == 5

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

Нулевое значение среза равно nil. Функции len и cap возвращают 0 для нулевого среза.

Срез также может быть сформирован путем "нарезки" существующего среза или массива. Разрезание выполняется путем указания полуоткрытого диапазона с двумя индексами, разделенными двоеточием. Например, выражение b [1:4] создает срез, включающий элементы с 1 по 3 из b (индексы полученного среза будут от 0 до 2).

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'} 
// используя то же хранилище, что и b

Начальный и конечный индексы выражения среза являются необязательными; по умолчанию они равны нулю и длине среза соответственно:

// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

Это также синтаксис для создания среза по массиву:

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // срез, ссылающийся на хранилище x

Внутренности среза

Срез - это дескриптор сегмента массива. Он состоит из указателя на массив, длины сегмента и его емкости (максимальной длины сегмента).

Наша переменная s, созданная ранее make([]byte, 5), имеет следующую структуру:

Длина (len) - это количество элементов, на которые ссылается срез. Емкость (cap) - это количество элементов в базовом массиве (начиная с элемента, на который указывает указатель среза). Различие между длиной и емкостью станет понятным, когда мы рассмотрим следующие несколько примеров.

Когда мы срезаем s, наблюдаем за изменениями в структуре данных среза и их отношением к базовому массиву:

s = s[2:4]

Нарезка не копирует данные среза. Он создает новое значение среза, которое указывает на исходный массив. Это делает операции среза такими же эффективными, как манипулирование индексами массива. Следовательно, изменение элементов (не самого среза) повторного среза изменяет элементы исходного среза:

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:] 
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

Ранее мы нарезали s до длины, меньшей его емкости. Мы можем увеличить s до его емкости, нарезав его снова:

s = s[:cap(s)]

Срез не может быть выращен за пределами его емкости. Попытка сделать это вызовет панику во время выполнения (runtime panic), так же как и при индексировании вне границ среза или массива. Точно так же срезы нельзя повторно разрезать ниже нуля для доступа к более ранним элементам в массиве.

Растущие срезы (функции copy и append)

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

// +1 в случае cap(s) == 0
t := make([]byte, len(s), (cap(s)+1)*2) 
for i := range s {
        t[i] = s[i]
}
s = t

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

func copy(dst, src []T) int

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

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

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

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

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    // если необходимо, перераспределяем
    if n > cap(slice) { 
        // выделяем вдвое больше, чем нужно, 
        // для будущего роста.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

Можно использовать AppendByte следующим образом:

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

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

Но большинству программ не требуется полный контроль, поэтому Go предоставляет встроенную функцию append, которая подходит для большинства целей; ее сигнатура

func append(s []T, x ...T) []T

Функция append добавляет элементы x в конец среза s и увеличивает его, если требуется большая емкость.

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

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

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}

// эквивалентно "append(a, b[0], b[1], b[2])"
a = append(a, b...) 

// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

Поскольку нулевое значение среза (nil) действует как срез нулевой длины, вы можете объявить переменную среза и затем добавить ее в цикл:

// Filter возвращает новый срез, содержащий только
// элементы s, которые удовлетворяют fn()
func Filter(s []int, fn func(int) bool) []int {
    var p []int // == nil
    for _, v := range s {
        if fn(v) {
            p = append(p, v)
        }
    }
    return p
}

Трюк со срезом

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

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

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

Этот код ведет себя как объявлено, но возвращенный []byte указывает на массив, содержащий весь файл. Поскольку срез ссылается на исходный массив, пока срез хранится вокруг сборщика мусора, он не может освободить массив; несколько полезных байтов файла сохраняют все содержимое в памяти.

Чтобы решить эту проблему, можно скопировать интересные данные в новый срез перед возвратом:

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}


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


пятница, 7 февраля 2020 г.

Трассировка HTTP запросов в Golang

Начиная с версии 1.7 в Go появилась трассировка HTTP, средство для сбора детальной информации в течение всего жизненного цикла запроса клиента HTTP. Поддержка трассировки HTTP обеспечивается
пакетом net/http/httptrace. Собранная информация может использоваться для устранения проблем с задержкой, мониторинга служб, написания адаптивных систем и многого другого.

HTTP события

В пакете httptrace есть несколько хуков для сбора информации о различных событиях во время выполнения HTTP-запроса. Эти события включают в себя:

  • Создание соединения
  • Повторное использование соединения
  • DNS-поиск
  • Запись запроса в сеть (на провод)
  • Чтение ответа

Отслеживание событий

Вы можете включить HTTP-трассировку, поместив *httptrace.ClientTrace, содержащий функции ловушек (hook functions), в context.Context запроса. Различные реализации http.RoundTripper сообщают о внутренних событиях, ища контекст *httptrace.ClientTrace и вызывая соответствующие функции ловушек.

Трассировка ограничена контекстом запроса, и пользователи должны поместить *httptrace.ClientTrace в контекст запроса, прежде чем они начнут запрос.

req, _ := http.NewRequest("GET", "http://example.com", nil)
trace := &httptrace.ClientTrace{
    DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
        fmt.Printf("DNS Info: %+v\n", dnsInfo)
    },
    GotConn: func(connInfo httptrace.GotConnInfo) {
        fmt.Printf("Got Conn: %+v\n", connInfo)
    },
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
if _, err := http.DefaultTransport.RoundTrip(req); err != nil {
    log.Fatal(err)
}

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

Трассировка с http.Client

Механизм трассировки предназначен для отслеживания событий в жизненном цикле одного http.Transport.RoundTrip. Тем не менее, клиент может совершить несколько обращений для выполнения HTTP-запроса. Например, в случае перенаправления URL-адреса зарегистрированные хуки будут вызываться столько раз, сколько клиент выполняет перенаправления HTTP, делая несколько запросов. Пользователи несут ответственность за распознавание таких событий на уровне http.Client. Программа ниже идентифицирует текущий запрос, используя оболочку http.RoundTripper.

package main

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

// transport это http.RoundTripper 
// который отслеживает выполняемый запрос
// и реализуек хуки для сообщения HTTP событий отслеживания
type transport struct {
    current *http.Request
}

// RoundTrip оборачивает http.DefaultTransport.RoundTrip
// чтобы отслеживать текущий запрос.
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
    t.current = req
    return http.DefaultTransport.RoundTrip(req)
}

// GotConn печатает информацию о том 
// было ли соединение использовано ранее для текущего запроса
func (t *transport) GotConn(info httptrace.GotConnInfo) {
    fmt.Printf("Connection reused for %v? %v\n", t.current.URL, info.Reused)
}

func main() {
    t := &transport{}

    req, _ := http.NewRequest("GET", "https://google.com", nil)
    trace := &httptrace.ClientTrace{
        GotConn: t.GotConn,
    }
    req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

    client := &http.Client{Transport: t}
    if _, err := client.Do(req); err != nil {
        log.Fatal(err)
    }
}

Программа выполнит перенаправление google.com на www.google.com и выведет:

Connection reused for https://google.com? false
Connection reused for https://www.google.com/? false

Transport в пакете net/http поддерживает трассировку запросов HTTP/1 и HTTP/2.

Если вы являетесь автором пользовательской реализации http.RoundTripper, вы можете поддержать трассировку, проверив контекст запроса для *httptest.ClientTrace и вызвав соответствующие хуки по мере возникновения событий.

Вывод

HTTP-трассировка является ценным дополнением к Go для тех, кто интересуется отладкой задержки HTTP-запросов и инструментами записи для сетевой отладки для исходящего трафика.


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


четверг, 6 февраля 2020 г.

Тип ServeMux пакета net/http в Golang

Тип ServeMux

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

Фиксированные имена шаблонов, корневые пути, например "/favicon.ico", или корневые поддеревья, например "/images/" (обратите внимание на закрывающий слэш). Более длинные шаблоны имеют приоритет над более короткими, поэтому, если есть обработчики, зарегистрированные как для "/images/", так и "/images/thumbnails/", последний обработчик будет вызываться для путей, начинающихся с "/images/thumbnails/", а первый будет получать запросы на любые другие пути в поддереве "/images/".

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

Если поддерево было зарегистрировано и получен запрос с именем корня поддерева без его косой черты, ServeMux перенаправляет этот запрос в корень поддерева (добавляя косую черту). Это поведение может быть отменено отдельной регистрацией пути без завершающего слэша. Например, регистрация "/images/" заставляет ServeMux перенаправить запрос "/images" на "/images/", если только "/images" не был зарегистрирован отдельно.

При желании шаблоны могут начинаться с имени хоста, ограничивая совпадения только с URL-адресами на этом хосте. Специфичные для хоста шаблоны имеют приоритет над общими шаблонами, так что обработчик может зарегистрироваться для двух шаблонов "/codesearch" и "codesearch.google.com/", не принимая также запросы на "http://www.google.com/".

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

type ServeMux struct {
    // содержит фильтруемые или неэкспортируемые поля
}

Функция NewServeMux

func NewServeMux() *ServeMux

NewServeMux выделяет и возвращает новый ServeMux.

Метод (*ServeMux) Handle

func (mux *ServeMux) Handle(pattern string, handler Handler)

Handle регистрирует обработчик для данного шаблона. Если обработчик для шаблона уже существует, Handle вызывает panic.

Пример использования ServeMux и его метода Handle:

package main

import (
  "fmt"
  "net/http"
)

type apiHandler struct{}

func (apiHandler) ServeHTTP(http.ResponseWriter, *http.Request) {}

func main() {
  mux := http.NewServeMux()
  mux.Handle("/api/", apiHandler{})
  mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
    // "/" паттерн соотвествует всему, 
    // поэтому нам нужно проверить
    // что это на самом деле запрос корня
    if req.URL.Path != "/" {
      http.NotFound(w, req)
      return
    }
    fmt.Fprintf(w, "Welcome to the home page!")
  })
  log.Fatal(http.ListenAndServe(":8080", mux))
}

Метод (*ServeMux) HandleFunc

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))

HandleFunc регистрирует функцию-обработчик для данного шаблона.

Метод (*ServeMux) Handler

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)

Появился начиная с версии Go 1.1

Handler возвращает обработчик для использования для данного запроса, консультируясь с r.Method, r.Host и r.URL.Path. Он всегда возвращает ненулевой обработчик. Если путь не в своей канонической форме, обработчик будет внутренне сгенерированным обработчиком, который перенаправляет на канонический путь. Если хост содержит порт, он игнорируется при сопоставлении обработчиков.

Путь и хост используются без изменений для запросов CONNECT.

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

Если нет зарегистрированного обработчика, который применяется к запросу, Handler возвращает обработчик “page not found” (страница не найдена) и пустой шаблон.

Метод (*ServeMux) ServeHTTP

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

ServeHTTP отправляет запрос обработчику, чей шаблон наиболее точно соответствует URL-адресу запроса.


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


Функции Get, Head, Post, PostForm пакета net/http в Golang

Функция Get

func Get(url string) (resp *Response, err error)

Get выдает GET по указанному URL. Если ответ является одним из следующих кодов перенаправления, Get следует за перенаправлением, максимум до 10 перенаправлений:

301 (Moved Permanently)
302 (Found)
303 (See Other)
307 (Temporary Redirect)
308 (Permanent Redirect)

Ошибка возвращается, если было слишком много перенаправлений или если произошла ошибка протокола HTTP. Ответ не-2xx не вызывает ошибку. Любая возвращаемая ошибка будет иметь тип *url.Error. Метод Timeout значения url.Error сообщит true, если запрос истек или был отменен.

Когда err равно nil, resp всегда содержит не-nil resp.Body. Вызывающий должен закрыть resp.Body когда закончил чтение с него.

Get - это обертка вокруг DefaultClient.Get.

Чтобы сделать запрос с пользовательскими заголовками, используйте NewRequest и DefaultClient.Do.

Пример использования Get:

package main

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

func main() {
  res, err := http.Get("http://www.google.com/robots.txt")
  if err != nil {
    log.Fatal(err)
  }
  robots, err := ioutil.ReadAll(res.Body)
  res.Body.Close()
  if err != nil {
    log.Fatal(err)
  }
  fmt.Printf("%s", robots)
}

Функция Head

func Head(url string) (resp *Response, err error)

Head отправляет HEAD на указанный URL. Если ответом является один из следующих кодов перенаправления, Head следует за перенаправлением, максимум до 10 перенаправлений:

301 (Moved Permanently)
302 (Found)
303 (See Other)
307 (Temporary Redirect)
308 (Permanent Redirect)

Head является оберткой вокруг DefaultClient.Head

Функция Post

func Post(url, contentType string, body io.Reader) (resp *Response, err error)

Post отправляет POST по указанному URL.

Вызывающий должен закрыть resp.Body когда закончит чтение с него.

Если предоставленное тело является io.Closer, оно закрывается после запроса.

Post является оберткой вокруг DefaultClient.Post.

Чтобы установить пользовательские заголовки, используйте NewRequest и DefaultClient.Do.

Функция PostForm

func PostForm(url string, data url.Values) (resp *Response, err error)

PostForm отправляет POST для указанного URL с ключами и значениями данных, закодированными в виде тела запроса.

Заголовок Content-Type установлен на application/x-www-form-urlencoded. Чтобы установить другие заголовки, используйте NewRequest и DefaultClient.Do.

Когда err равно nil, resp всегда содержит не-nil resp.Body. Вызывающий должен закрыть resp.Body, когда закончит чтение с него.

PostForm - это обертка вокруг DefaultClient.PostForm.


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


Тип Handler, функции FileServer, NotFoundHandler пакета net/http в Golang

Тип Handler

Handler отвечает на запрос HTTP.

ServeHTTP должен записать заголовки ответа и данные в ResponseWriter и затем вернуться. Возвращает сигнал о том, что запрос завершен; недопустимо использовать ResponseWriter или читать из Request.Body после или одновременно с завершением вызова ServeHTTP.

В зависимости от программного обеспечения HTTP-клиента, версии протокола HTTP и любых посредников между клиентом и сервером Go может быть невозможным чтение из Request.Body после записи в ResponseWriter. Осторожные обработчики должны сначала прочитать Request.Body, а затем ответить.

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

Если ServeHTTP вызывает panic, сервер (вызывающий ServeHTTP) предполагает, что эффект паники был изолирован для активного запроса. Он восстанавливает панику, записывает трассировку стека в журнал ошибок сервера и либо закрывает сетевое соединение, либо отправляет HTTP/2 RST_STREAM, в зависимости от протокола HTTP. Чтобы прервать обработчик, чтобы клиент увидел прерванный ответ, но сервер не зарегистрировал ошибку, вызовите panic со значением ErrAbortHandler.

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

Функция FileServer

func FileServer(root FileSystem) Handler

FileServer возвращает обработчик, который обслуживает HTTP-запросы с содержимым файловой системы с корнем в root.

Чтобы использовать реализацию файловой системы операционной системы, используйте http.Dir:

http.Handle("/", http.FileServer(http.Dir("/tmp")))

В особом случае возвращаемый файловый сервер перенаправляет любой запрос, оканчивающийся на "/index.html", по тому же пути, без конечного "index.html".

Примеры:

package main

import (
  "log"
  "net/http"
)

func main() {
  // Простой статический веб-сервер:
  log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc"))))
}

Пример:

package main

import (
  "net/http"
)

func main() {
  // Обслуживаем каталог на диске (/tmp) под альтернативным URL
  // путем (/tmpfiles/), используем StripPrefix для изменения запроса
  // пути URL до того, как FileServer его увидит:
  http.Handle("/tmpfiles/", http.StripPrefix("/tmpfiles/", http.FileServer(http.Dir("/tmp"))))
}

Функция NotFoundHandler

func NotFoundHandler() Handler

NotFoundHandler возвращает простой обработчик запроса, который отвечает на каждый запрос с ответом “404 page not found”.

Пример:

package main

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

func newPeopleHandler() http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "This is the people handler.")
  })
}

func main() {
  mux := http.NewServeMux()

  // Создаем тестовый обработчик для возврата 404
  mux.Handle("/resources", http.NotFoundHandler())

  // Создаем тестовый обработчик для возврата 200
  mux.Handle("/resources/people/", newPeopleHandler())

  log.Fatal(http.ListenAndServe(":8080", mux))
}

Функция StripPrefix

func StripPrefix(prefix string, h Handler) Handler

StripPrefix возвращает обработчик, который обслуживает HTTP-запросы, удаляя указанный префикс из пути URL запроса и вызывая обработчик h. StripPrefix обрабатывает запрос пути, который не начинается с префикса, отвечая сообщением об ошибке HTTP 404 not found.

Пример:

package main

import (
  "net/http"
)

func main() {
  // Обслуживаем каталог на диске (/tmp) по альтернативному URL
  // пути (/tmpfiles/), используем StripPrefix для изменения 
  // пути URL запроса до того, как FileServer его увидит:
  http.Handle("/tmpfiles/", http.StripPrefix("/tmpfiles/", http.FileServer(http.Dir("/tmp"))))
}


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