четверг, 30 января 2020 г.

Функции пакета net/http, примеры

Функция Handle

func Handle(pattern string, handler Handler)

Handle регистрирует обработчик для данного шаблона в DefaultServeMux. Пример:

package main

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

type countHandler struct {
  mu sync.Mutex // guards n
  n  int
}

func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  h.mu.Lock()
  defer h.mu.Unlock()
  h.n++
  fmt.Fprintf(w, "count is %d\n", h.n)
}

func main() {
  http.Handle("/count", new(countHandler))
  log.Fatal(http.ListenAndServe(":8080", nil))
}

Функция HandleFunc

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

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

package main

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

func main() {
  h1 := func(w http.ResponseWriter, _ *http.Request) {
    io.WriteString(w, "Hello from a HandleFunc #1!\n")
  }
  h2 := func(w http.ResponseWriter, _ *http.Request) {
    io.WriteString(w, "Hello from a HandleFunc #2!\n")
  }

  http.HandleFunc("/", h1)
  http.HandleFunc("/endpoint", h2)

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

Функция ListenAndServe

func ListenAndServe(addr string, handler Handler) error

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

Обработчик обычно равен nil, и в этом случае используется DefaultServeMux.

ListenAndServe всегда возвращает ненулевую ошибку.

Пример:

package main

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

func main() {
  // Hello world, the web server

  helloHandler := func(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "Hello, world!\n")
  }

  http.HandleFunc("/hello", helloHandler)
  log.Fatal(http.ListenAndServe(":8080", nil))
}

Функция ListenAndServeTLS

func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error

ListenAndServeTLS действует идентично ListenAndServe, за исключением того, что она ожидает HTTPS-соединений. Кроме того, должны быть предоставлены файлы, содержащие сертификат и соответствующий закрытый ключ для сервера. Если сертификат подписан центром сертификации, certFile должен быть объединением сертификата сервера, любых промежуточных продуктов и сертификата CA.

package main

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

func main() {
  http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "Hello, TLS!\n")
  })

  // Можно использовать generate_cert.go в crypto/tls 
  // чтобы сгенерировать cert.pem и key.pem.
  log.Printf("About to listen on 8443. Go to https://127.0.0.1:8443/")
  err := http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)
  log.Fatal(err)
}


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


суббота, 25 января 2020 г.

Go модули: v2 и далее

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

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

Основные версии и пути модулей

Модули формализовали важный принцип в Go, правило совместимости импорта:

Если старый пакет и новый пакет имеют 
одинаковый путь импорта,
новый пакет должен быть обратно совместим 
со старым пакетом.

По определению, новая основная версия пакета не имеет обратной совместимости с предыдущей версией. Это означает, что новая основная версия модуля должна иметь путь к модулю, отличному от предыдущей версии. Начиная с версии v2 основная версия должна отображаться в конце пути к модулю (объявлена в выражении модуля в файле go.mod). Например, когда авторы модуля github.com/googleapis/gax-go разработали v2, они использовали новый путь к модулю github.com/googleapis/gax-go/v2. Пользователи, которые хотели использовать v2, должны были изменить свои импорт пакетов и требования к модулю на github.com/googleapis/gax-go/v2.

Потребность в суффиксах основной версии - это одно из отличий модулей Go от большинства других систем управления зависимостями. Суффиксы необходимы для решения проблемы diamond зависимости. Перед модулями Go gopkg.in позволял сопровождающим пакетов следовать тому, что мы теперь называем правилом совместимости импорта. С gopkg.in, если вы зависите от пакета, который импортирует gopkg.in/yaml.v1, и другого пакета, который импортирует gopkg.in/yaml.v2, конфликт не возникает, потому что два пакета yaml имеют разные пути импорта - они используют суффикс версии, как с модулями Go. Поскольку gopkg.in использует ту же методологию суффикса версии, что и модули Go, команда Go принимает .v2 в gopkg.in/yaml.v2 в качестве действительного суффикса основной версии. Это особый случай совместимости с gopkg.in: для модулей, размещенных в других доменах, требуется суффикс косой черты, например /v2.

Основные версии стратегий

Рекомендуемая стратегия заключается в разработке модулей v2+ в каталоге с именем суффикса основной версии.

github.com/googleapis/gax-go @ master branch
/go.mod    → module github.com/googleapis/gax-go
/v2/go.mod → module github.com/googleapis/gax-go/v2

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

Другие стратегии могут хранить основные версии в отдельных ветках. Однако, если исходный код v2+ находится в ветке хранилища по умолчанию (обычно master), инструменты, не поддерживающие версию - включая команду go в режиме GOPATH - могут не различать основные версии.

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

Публикация v2 и далее

Этот пост использует github.com/googleapis/gax-go в качестве примера:

$ pwd
/tmp/gax-go
$ ls
CODE_OF_CONDUCT.md  call_option.go  internal
CONTRIBUTING.md     gax.go          invoke.go
LICENSE             go.mod          tools.go
README.md           go.sum          RELEASING.md
header.go
$ cat go.mod
module github.com/googleapis/gax-go

go 1.9

require (
    github.com/golang/protobuf v1.3.1
    golang.org/x/exp v0.0.0-20190221220918-438050ddec5e
    golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
    golang.org/x/tools v0.0.0-20190114222345-bf090417da8b
    google.golang.org/grpc v1.19.0
    honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099
)
$

Чтобы начать разработку v2 на github.com/googleapis/gax-go, мы создадим новый каталог v2/ и скопируем в него наш пакет.

$ mkdir v2
$ cp *.go v2/
building file list ... done
call_option.go
gax.go
header.go
invoke.go
tools.go

sent 10588 bytes  received 130 bytes  21436.00 bytes/sec
total size is 10208  speedup is 0.95
$

Теперь создадим файл go.mod v2, скопировав текущий файл go.mod и добавив суффикс v2/ к пути модуля:

$ cp go.mod v2/go.mod
$ go mod edit -module github.com/googleapis/gax-go/v2 v2/go.mod
$

Обратите внимание, что версия v2 рассматривается как отдельный модуль от версий v0 / v1: оба могут сосуществовать в одной сборке. Итак, если ваш модуль v2+ имеет несколько пакетов, вы должны обновить их, чтобы использовать новый путь импорта /v2: в противном случае ваш модуль v2+ будет зависеть от вашего модуля v0 / v1. Например, чтобы обновить все ссылки на github.com/my/project на github.com/my/project/v2, вы можете использовать find и sed:

$ find . -type f \
    -name '*.go' \
    -exec sed -i -e 's,github.com/my/project,github.com/my/project/v2,g' {} \;
$

Теперь у нас есть модуль v2, но мы хотим поэкспериментировать и внести изменения перед публикацией релиза. До тех пор, пока мы не выпустим v2.0.0 (или любую версию без суффикса перед релизом), мы можем разрабатывать и вносить критические изменения в зависимости от нового API. Если мы хотим, чтобы пользователи могли экспериментировать с новым API, прежде чем мы официально сделаем его стабильным, мы можем опубликовать предварительную (pre-release) версию v2:

$ git tag v2.0.0-alpha.1
$ git push origin v2.0.0-alpha.1
$

Если мы довольны нашим v2 API и уверены, что нам не нужны какие-либо другие критические изменения, мы можем пометить v2.0.0:

$ git tag v2.0.0
$ git push origin v2.0.0
$

На данный момент есть две основные версии для поддержки. Обратно совместимые изменения и исправления ошибок приведут к появлению новых минорных и исправлений (например, v1.1.0, v2.0.1 и т. д.).

Вывод

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

Прерывание изменений в модуле v1+ всегда должно происходить в новом модуле vN+1. Когда выпущен новый модуль, это означает дополнительную работу для сопровождающих и пользователей, которым необходимо перейти на новый пакет. Поэтому сопровождающие должны проверить свои API-интерфейсы перед выпуском стабильного релиза и тщательно обдумать, действительно ли необходимы критические изменения после v1.


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


вторник, 21 января 2020 г.

Состояния гонки в Golang

Состояния гонки (Race conditions) являются одними из самых коварных и неуловимых ошибок программирования. Как правило, они вызывают ошибочные и загадочные сбои, часто после того, как код был запущен в работу. Хотя механизмы конкурентности Go упрощают написание чистого конкурентного кода, они не предотвращают состояние гонки. Требуется осторожность, усердие и тестирование. И инструменты могут помочь.

Начиная с версии 1.1 Go включает в себя детектор гонки, инструмент для определения состояний гонки в коде Go. В настоящее время он доступен для систем Linux, OS X и Windows с 64-разрядными процессорами x86.

Детектор гонки основан на библиотеке времени выполнения (runtime library) C/C++ ThreadSanitizer, которая использовалась для обнаружения многих ошибок во внутренней кодовой базе Google и в Chromium. Технология была интегрирована с Go в сентябре 2012 года; это часть непрерывного процесса сборки, где она отслеживает условия гонки по мере их возникновения.

Как это устроено

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

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

Использование детектора гонки

Детектор гонки полностью интегрирован с цепочкой инструментов go. Чтобы создать код с включенным детектором гонки, просто добавьте флаг -race в командную строку:

$ go test -race mypkg    // тестирование пакета
$ go run -race mysrc.go  // компиляция и запуск программы
$ go build -race mycmd   // сборка команды
$ go install -race mypkg // установка пакета

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

$ go get -race golang.org/x/blog/support/racy
$ racy

Примеры

Вот два примера реальных проблем, обнаруженных детектором гонки.

Пример 1: Timer.Reset

Первый пример - это упрощенная версия фактической ошибки, обнаруженной детектором гонки. Он использует таймер для печати сообщения после произвольной продолжительности от 0 до 1 секунды. Это повторяется в течение пяти секунд. Он использует time.AfterFunc для создания Timer для первого сообщения, а затем использует метод Reset для планирования следующего сообщения, каждый раз повторно используя Timer.

11 func main() {
12     start := time.Now()
13     var t *time.Timer
14     t = time.AfterFunc(randomDuration(), func() {
15         fmt.Println(time.Now().Sub(start))
16         t.Reset(randomDuration())
17     })
18     time.Sleep(5 * time.Second)
19 }
20 
21 func randomDuration() time.Duration {
22     return time.Duration(rand.Int63n(1e9))
23 }

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

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x8 pc=0x41e38a]

goroutine 4 [running]:
time.stopTimer(0x8, 0x12fe6b35d9472d96)
    src/pkg/runtime/ztime_linux_amd64.c:35 +0x25
time.(*Timer).Reset(0x0, 0x4e5904f, 0x1)
    src/pkg/time/sleep.go:81 +0x42
main.func·001()
    race.go:14 +0xe3
created by time.goFunc
    src/pkg/time/sleep.go:122 +0x48

Что поисходит здесь? Запущенная программа с детектором гонки выдает более ясный ответ:

==================
WARNING: DATA RACE
Read by goroutine 5:
  main.func·001()
     race.go:14 +0x169

Previous write by goroutine 1:
  main.main()
      race.go:15 +0x174

Goroutine 5 (running) created at:
  time.goFunc()
      src/pkg/time/sleep.go:122 +0x56
  timerproc()
     src/pkg/runtime/ztime_linux_amd64.c:181 +0x189
==================

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

Чтобы исправить состояние гонки, мы изменим код для чтения и записи переменной t только из основной go-процедуры:

11 func main() {
12     start := time.Now()
13     reset := make(chan bool)
14     var t *time.Timer
15     t = time.AfterFunc(randomDuration(), func() {
16         fmt.Println(time.Now().Sub(start))
17         reset <- true
18     })
19     for time.Since(start) < 5*time.Second {
20         <-reset
21         t.Reset(randomDuration())
22     }
23 }

Здесь main процедура полностью отвечает за установку и сброс таймера t, а новый канал reset сообщает о необходимости сброса таймера потокобезопасным способом.

Более простой, но менее эффективный подход - избегать повторного использования таймеров.

Пример 2: ioutil.Discard

Второй пример более тонкий.

Объект Discard пакета ioutil реализует io.Writer, но отбрасывает все записанные в него данные. Думайте об этом как /dev/null: место для отправки данных, которые вам нужно прочитать, но не хотите хранить. Это обычно используется с io.Copy, чтобы истощить reader, как это:

io.Copy(ioutil.Discard, reader)

Еще в июле 2011 года команда Go заметила, что использование Discard таким способом было неэффективным: функция Copy выделяет внутренний буфер 32 кБ при каждом вызове, но при использовании с Discard буфер не нужен, поскольку мы просто выбрасываем данные для чтения прочь. Идиоматическое использование Copy and Discard не должно быть таким дорогостоящим.

Исправление было простым. Если данный Writer реализует метод ReadFrom, вызов Copy выполняется следующим образом:

io.Copy(writer, reader)

делегирован этому потенциально более эффективному вызову:

writer.ReadFrom(reader)

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

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

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

Вот код с известной скоростью в io/ioutil, где Discard - это devNull, который разделяет один буфер между всеми своими пользователями.

var blackHole [4096]byte // разделяемый буфер

func (devNull) ReadFrom(r io.Reader) (n int64, err error) {
    readSize := 0
    for {
        readSize, err = r.Read(blackHole[:])
        n += int64(readSize)
        if err != nil {
            if err == io.EOF {
                return n, nil
            }
            return
        }
    }
}

Программа включает в себя тип trackDigestReader, который оборачивает io.Reader и записывает хэш-дайджест того, что он читает.

type trackDigestReader struct {
    r io.Reader
    h hash.Hash
}

func (t trackDigestReader) Read(p []byte) (n int, err error) {
    n, err = t.r.Read(p)
    t.h.Write(p[:n])
    return
}

Например, его можно использовать для вычисления хэша файла SHA-1 при его чтении:

tdr := trackDigestReader{r: file, h: sha1.New()}
io.Copy(writer, tdr)
fmt.Printf("File hash: %x", tdr.h.Sum(nil))

В некоторых случаях некуда было бы записывать данные - но все равно нужно было бы хэшировать файл - и поэтому использовался бы Discard:

io.Copy(ioutil.Discard, tdr)

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

func (t trackDigestReader) Read(p []byte) (n int, err error) {
    // буфер p это blackHole
    n, err = t.r.Read(p)
    // p может быть изменен другой goroutine здесь,
    // между Read выше и Write ниже
    t.h.Write(p[:n])
    return
}

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

Выводы

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


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


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

HTTP/2 Server Push в Golang

HTTP/2 предназначен для устранения многих недостатков HTTP/1.x. Современные веб-страницы используют много ресурсов: HTML, таблицы стилей, скрипты, изображения и так далее. В HTTP/1.x каждый из этих ресурсов должен запрашиваться явно. Это может быть медленным процессом. Браузер начинает с извлечения HTML-кода, а затем постепенно узнает о дополнительных ресурсах, анализируя и оценивая страницу. Поскольку сервер должен ждать, пока браузер сделает каждый запрос, сеть часто не используется и используется недостаточно.

Чтобы уменьшить задержку, в HTTP/2 был введен server push, который позволяет серверу передавать ресурсы в браузер, прежде чем они будут явно запрошены. Сервер часто знает о многих дополнительных ресурсах, которые понадобятся странице, и может начать отправлять (пушить) эти ресурсы, когда он отвечает на первоначальный запрос. Это позволяет серверу полностью использовать неактивную сеть и сократить время загрузки страницы.

На уровне протокола HTTP/2 server push осуществляется фреймами PUSH_PROMISE. PUSH_PROMISE описывает запрос, который сервер прогнозирует, что браузер запросит в ближайшем будущем. Как только браузер получает PUSH_PROMISE, он знает, что сервер доставит ресурс. Если браузер позже обнаружит, что ему нужен этот ресурс, он будет ждать завершения push, а не отправлять новый запрос. Это сокращает время ожидания браузера в сети.

Server Push в пакете net/http

В Go 1.8 появилась поддержка отправки (push) ответов от http.Server. Эта функция доступна, если запущенный сервер является сервером HTTP/2, а входящее соединение использует HTTP/2. В любом обработчике HTTP вы можете утверждать, поддерживает ли http.ResponseWriter push-запрос сервера, проверяя, реализует ли он новый интерфейс http.Pusher.

Например, если сервер знает, что app.js потребуется для отображения страницы, обработчик может инициировать push, если доступен http.Pusher:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    if pusher, ok := w.(http.Pusher); ok {
        // Push поддерживается.
        if err := pusher.Push("/app.js", nil); err != nil {
            log.Printf("Failed to push: %v", err)
        }
    }
    // ...
})

Вызов Push создает синтетический запрос для /app.js, синтезирует этот запрос в фрейм PUSH_PROMISE, а затем перенаправляет синтетический запрос в обработчик запросов сервера, который сгенерирует отправленный ответ. Второй аргумент для Push указывает дополнительные заголовки для включения в PUSH_PROMISE. Например, если ответ на /app.js зависит от Accept-Encoding, тогда PUSH_PROMISE должен включать значение Accept-Encoding:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    if pusher, ok := w.(http.Pusher); ok {
        // Push поддерживается.
        options := &http.PushOptions{
            Header: http.Header{
                "Accept-Encoding": r.Header["Accept-Encoding"],
            },
        }
        if err := pusher.Push("/app.js", options); err != nil {
            log.Printf("Failed to push: %v", err)
        }
    }
    // ...
})

Полностью рабочий пример доступен по адресу https://github.com/golang/blog/blob/master/content/h2push/server/main.go

Если вы запустите сервер (main.go) и загрузите https://localhost:8080, инструменты разработчика вашего браузера должны показать, что app.js и style.css были отправлены сервером.


Начните свои пуши, прежде чем ответить

Рекомендуется вызывать метод Push перед отправкой байтов ответа. В противном случае можно случайно генерировать дубликаты ответов. Например, предположим, что вы пишете часть ответа HTML:

<html>
<head>
    <link rel="stylesheet" href="a.css">...

Затем вы вызываете Push("a.css", nil). Браузер может проанализировать этот фрагмент HTML, прежде чем он получит ваш PUSH_PROMISE, и в этом случае браузер отправит запрос на a.css в дополнение к получению вашего PUSH_PROMISE. Теперь сервер сгенерирует два ответа для a.css. Вызов Push перед написанием ответа полностью исключает эту возможность.

Когда использовать Server Push

Подумайте об использовании push-сообщений сервера, когда ваша сетевая ссылка простаивает. Только что закончили отправку HTML для вашего веб-приложения? Не тратьте время на ожидание, начните использовать ресурсы, которые понадобятся вашему клиенту. Вы вкладываете ресурсы в свой HTML-файл, чтобы уменьшить задержку? Вместо того, чтобы вставлять, попробуйте push. Переадресация - еще одно хорошее время для использования push, поскольку почти всегда происходит потеря данных в обратном направлении, когда клиент выполняет перенаправление. Существует много возможных сценариев использования push.

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

Вывод

С Go 1.8 стандартная библиотека обеспечивает готовую поддержку HTTP/2 Server Push, предоставляя вам больше возможностей для оптимизации ваших веб-приложений.


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


воскресенье, 19 января 2020 г.

Работа с гонками данных (data race) в Golang

Гонки данных (Data race) являются одними из самых распространенных и трудных для отладки типов ошибок в конкурентных системах. Гонка данных происходит, когда две программы одновременно обращаются к одной и той же переменной, и по крайней мере одно из обращений является записью.

Вот пример гонки данных, которая может привести к сбоям и повреждению памяти:

func main() {
    c := make(chan bool)
    m := make(map[string]string)
    go func() {
        m["1"] = "a" // Первый конфликтующий доступ
        c <- true
    }()
    m["2"] = "b" // Второй конфликтующий доступ
    <-c
    for k, v := range m {
        fmt.Println(k, v)
    }
}

Использование детектора гонки данных

Чтобы помочь диагностировать такие ошибки, Go включает в себя встроенный детектор гонки данных. Чтобы использовать его, добавьте флаг -race к команде go:

$ go test -race mypkg    // тестирование пакета
$ go run -race mysrc.go  // запуск исходного файла
$ go build -race mycmd   // сборка команды
$ go install -race mypkg // установка пакета

Формат отчета

Когда детектор гонки обнаруживает гонку данных в программе, он печатает отчет. Отчет содержит трассировки стеков для конфликтующих доступов, а также стеки, в которых были созданы соответствующие go-процедуры (goroutines). Вот пример:

WARNING: DATA RACE
Read by goroutine 185:
  net.(*pollServer).AddFD()
      src/net/fd_unix.go:89 +0x398
  net.(*pollServer).WaitWrite()
      src/net/fd_unix.go:247 +0x45
  net.(*netFD).Write()
      src/net/fd_unix.go:540 +0x4d4
  net.(*conn).Write()
      src/net/net.go:129 +0x101
  net.func·060()
      src/net/timeout_test.go:603 +0xaf

Previous write by goroutine 184:
  net.setWriteDeadline()
      src/net/sockopt_posix.go:135 +0xdf
  net.setDeadline()
      src/net/sockopt_posix.go:144 +0x9c
  net.(*conn).SetDeadline()
      src/net/net.go:161 +0xe3
  net.func·061()
      src/net/timeout_test.go:616 +0x3ed

Goroutine 185 (running) created at:
  net.func·061()
      src/net/timeout_test.go:609 +0x288

Goroutine 184 (running) created at:
  net.TestProlongTimeout()
      src/net/timeout_test.go:618 +0x298
  testing.tRunner()
      src/testing/testing.go:301 +0xe8

Параметры

Переменная окружения GORACE устанавливает параметры детектора гонки. Формат такой:

GORACE="option1=val1 option2=val2"

Варианты:

log_path (по умолчанию stderr): 
детектор гонки записывает свой отчет 
в файл с именем log_path.pid. 
Специальные имена stdout и stderr приводят к записи отчетов 
в стандартный вывод 
и стандартную вывод ошибок соответственно.

exitcode (по умолчанию 66): 
состояние выхода, используемое 
при выходе после обнаруженной гонки.

strip_path_prefix (по умолчанию ""): 
удаляет этот префикс из всех путей к сообщаемым файлам, 
чтобы сделать отчеты более краткими.

history_size (по умолчанию 1): 
История доступа к памяти для каждой программы составляет 
32K * 2 ** history_size элементов. 
Увеличение этого значения позволяет избежать 
ошибки "не удалось восстановить стек" 
("failed to restore the stack")
в отчетах за счет увеличения использования памяти.

halt_on_error (по умолчанию 0): 
управляет выходом из программы 
после сообщения о первой гонке данных.

Пример:

$ GORACE="log_path=/tmp/race/report strip_path_prefix=/my/go/sources/" go test -race

Исключая тесты

При сборке с флагом -race команда go определяет дополнительную тег сборки race. Вы можете использовать тег, чтобы исключить некоторый код и тесты при запуске детектора гонки. Несколько примеров:

// +build !race

package foo

// Тест содержит гонку данных. Заведено issue.
func TestFoo(t *testing.T) {
    // ...
}

// Тест падает под детектором гонки из-за таймаутов.
func TestBar(t *testing.T) {
    // ...
}

// Тест выполняется слишком долго под детектором гонки.
func TestBaz(t *testing.T) {
    // ...
}

Как пользоваться

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

Типичные гонки данных

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

Гонка на счетчике цикла

func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func() {
            // Не та 'i' которую вы ожидаете.
            fmt.Println(i) 
            wg.Done()
        }()
    }
    wg.Wait()
}

Переменная i в литерале функции - это та же переменная, которая используется циклом, поэтому чтение в goroutine мчится с шагом цикла. (Эта программа обычно печатает 55555, а не 01234.) Программу можно исправить, сделав копию переменной:

func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            // Считывает локальную копию счетчика цикла.
            fmt.Println(j) 
            wg.Done()
        }(i)
    }
    wg.Wait()
}

Случайно разделяемая переменная

// ParallelWrite записывает данные в file1 и file2, 
// возвращает ошибки.
func ParallelWrite(data []byte) chan error {
    res := make(chan error, 2)
    f1, err := os.Create("file1")
    if err != nil {
        res <- err
    } else {
        go func() {
            // Эта err разделяемая с main goroutine,
            // поэтому выполнение записи вызывает 
            // гонку с выполнением записи ниже.
            _, err = f1.Write(data)
            res <- err
            f1.Close()
        }()
    }

    // Вторая конфликтующая запись в err.
    f2, err := os.Create("file2") 
    if err != nil {
        res <- err
    } else {
        go func() {
            _, err = f2.Write(data)
            res <- err
            f2.Close()
        }()
    }
    return res
}

Исправление заключается в том, чтобы вводить новые переменные в goroutine (обратите внимание на использование := ):

...
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...

Незащищенная глобальная переменная

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

var service map[string]net.Addr

func RegisterService(name string, addr net.Addr) {
    service[name] = addr
}

func LookupService(name string) net.Addr {
    return service[name]
}

Чтобы сделать код безопасным, защитите доступ с помощью мьютекса:

var (
    service   map[string]net.Addr
    serviceMu sync.Mutex
)

func RegisterService(name string, addr net.Addr) {
    serviceMu.Lock()
    defer serviceMu.Unlock()
    service[name] = addr
}

func LookupService(name string) net.Addr {
    serviceMu.Lock()
    defer serviceMu.Unlock()
    return service[name]
}

Примитивная незащищенная переменная

Гонки данных могут происходить и с переменными примитивных типов (bool, int, int64 и т. д.), Как в этом примере:

type Watchdog struct{ last int64 }

func (w *Watchdog) KeepAlive() {
    // Первый конфликтующий доступ.
    w.last = time.Now().UnixNano() 
}

func (w *Watchdog) Start() {
    go func() {
        for {
            time.Sleep(time.Second)
            // Второй конфликтующий доступ.
            if w.last < time.Now().Add(-10*time.Second).UnixNano() {
                fmt.Println("No keepalives for 10 seconds. Dying.")
                os.Exit(1)
            }
        }
    }()
}

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

Типичным решением этой гонки является использование канала или мьютекса. Чтобы сохранить поведение без блокировки, можно также использовать пакет sync/atomic.

type Watchdog struct{ last int64 }

func (w *Watchdog) KeepAlive() {
    atomic.StoreInt64(&w.last, time.Now().UnixNano())
}

func (w *Watchdog) Start() {
    go func() {
        for {
            time.Sleep(time.Second)
            if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() {
                fmt.Println("No keepalives for 10 seconds. Dying.")
                os.Exit(1)
            }
        }
    }()
}

Поддерживаемые системы

Детектор гонки работает на linux/amd64, linux/ppc64le, linux/arm64, freebsd/amd64, netbsd/amd64, darwin/amd64 и windows/amd64.

Время выполнения

Стоимость обнаружения гонки зависит от программы, но для типичной программы использование памяти может увеличиться в 5-10 раз, а время выполнения - в 2-20 раз.


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


пятница, 17 января 2020 г.

Работа с картами (map) в Golang

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

Декларация и инициализация

Тип map в Go выглядит следующим образом:

map[KeyType]ValueType

где KeyType может быть любым типом, который сопоставим (comparable) (подробнее об этом позже), а ValueType может быть любым типом вообще, включая другую карту!

Эта переменная m является картой строковых ключей для значений int:

var m map[string]int

Типы карт (map) являются ссылочными типами, такими как указатели или срезы (slice), и поэтому значение m выше равно nil; оно не указывает на инициализированную карту. Нулевая карта (nil map) ведет себя как пустая карта при чтении, но попытки записи в нулевую карту вызовут панику во время выполнения (runtime panic). Чтобы инициализировать карту, используйте встроенную функцию make:

m = make(map[string]int)

Функция make выделяет и инициализирует структуру данных hash map и возвращает значение карты, которое указывает на нее. Специфика этой структуры данных является деталью реализации среды выполнения и не определяется самим языком. В этой посте мы сосредоточимся на использовании карт, а не на их реализации.

Работа с картами

Go предоставляет знакомый синтаксис для работы с картами. Этот оператор устанавливает ключ "route" на значение 66:

m["route"] = 66

Этот оператор извлекает значение, хранящееся под ключом "route", и присваивает его новой переменной i:

i := m["route"]

Если запрошенный ключ не существует, мы получаем нулевое значение типа значения. В этом случае типом значения является int, поэтому нулевое значение равно 0:

j := m["root"]
// j == 0

Встроенная функция len возвращает количество элементов в карте:

n := len(m)

Встроенная функция delete удаляет запись в карте:

delete(m, "route")

Функция delete ничего не возвращает и ничего не сделает, если указанный ключ не существует.

Тесты присваивания с двумя значениями для существования ключа:

i, ok := m["route"]

В этом утверждении первому значению (i) присваивается значение, хранящееся под ключом "route". Если этот ключ не существует, i является нулевым значением типа значения (0). Второе значение (ok) является логическим (bool) значением, которое имеет значение true, если ключ существует в карте, и значение false, если нет.

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

_, ok := m["route"]

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

for key, value := range m {
    fmt.Println("Key:", key, "Value:", value)
}

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

commits := map[string]int{
    "rsc": 3711,
    "r":   2138,
    "gri": 1908,
    "adg": 912,
}

Тот же синтаксис может использоваться для инициализации пустой карты, которая функционально идентична использованию функции make:

m = map[string]int{}

Использование нулевых значений

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

Например, карта bool значений может использоваться как структура данных, подобная множеству (напомним, что нулевое значение для bool типа - false). Этот пример просматривает связанный список узлов и печатает их значения. Он использует карту указателей узлов для обнаружения циклов в списке.

type Node struct {
    Next  *Node
    Value interface{}
}
var first *Node

visited := make(map[*Node]bool)
for n := first; n != nil; n = n.Next {
    if visited[n] {
        fmt.Println("cycle detected")
        break
    }
    visited[n] = true
    fmt.Println(n.Value)
}

Выражение visited[n] является истинным (true), если n было посещено, или ложным (false), если n не присутствует. Нет необходимости использовать форму с двумя значениями, чтобы проверить наличие n в карте; нулевое значение по умолчанию делает это для нас.

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

type Person struct {
    Name  string
    Likes []string
}
var people []*Person

likes := make(map[string][]*Person)
for _, p := range people {
    for _, l := range p.Likes {
        likes[l] = append(likes[l], p)
    }
}

Чтобы напечатать список людей, которые любят сыр:

for _, p := range likes["cheese"] {
    fmt.Println(p.Name, "likes cheese.")
}

Чтобы напечатать количество людей, которые любят бекон:

fmt.Println(len(likes["bacon"]), "people like bacon.")

Обратите внимание, что так как range и len рассматривают нулевой срез как срез нулевой длины, эти два последних примера будут работать, даже если никто не любит сыр или бекон (хотя это маловероятно).

Типы ключей

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

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

hits := make(map[string]map[string]int)

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

n := hits["/doc/"]["au"]

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

func add(m map[string]map[string]int, path, country string) {
    mm, ok := m[path]
    if !ok {
        mm = make(map[string]int)
        m[path] = mm
    }
    mm[country]++
}
add(hits, "/doc/", "au")

С другой стороны, дизайн, в котором используется одна карта с struct ключом, устраняет всю эту сложность:

type Key struct {
    Path, Country string
}
hits := make(map[Key]int)

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

hits[Key{"/", "vn"}]++

И точно так же просто увидеть, сколько швейцарцев прочитали спецификацию:

n := hits[Key{"/ref/spec", "ch"}]

Конкурентность

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

Этот оператор объявляет переменную-счетчик, которая является анонимной структурой, содержащей карту и встроенный sync.RWMutex.

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

Для чтения со счетчика возьмите блокировку чтения (read lock):

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

Для записи в счетчик возьмите блокировку записи (write lock):

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

Порядок итерации

При итерации по карте с помощью range цикла порядок итераций не указывается и не гарантируется, что он будет одинаковым от одной итерации к следующей. Если вам требуется стабильный порядок итераций, вы должны поддерживать отдельную структуру данных, которая определяет этот порядок. В этом примере используется отдельный отсортированный срез ключей для печати map[int]string в порядке расположения ключей:

import "sort"

var m map[int]string
var keys []int
for k := range m {
    keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
    fmt.Println("Key:", k, "Value:", m[k])
}


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