В этой статье, я хотел бы рассказать вам, как можно достаточно быстро и легко написать небольшое веб-приложение на языке Go, который, не смотря на юный возраст, успел завоевать расположение у многих разработчиков. Обычно, для подобных статей пишут искусственные приложения, вроде TODO листа. Мы же попробуем написать что-то полезное, что уже существует и используется.
Часто, при разработке сервисов, нужно понимать какие данные отправляются в другой сервис, а возможность перехватить траффик есть не всегда. И как раз для того, чтобы отлавливать подобные запросы, существует проект requestb.in, позволяющий собирать запросы по определённому урлу и отображать их в веб-интерфейсе. Написанием подобного же приложения мы и займёмся. Чтобы немного упростить себе задачу, возьмём за основу какой-нибудь фреймворк, например Martini.
В конечном итоге, у нас должен будет получится вот такой вот сервис:
Эта статья будет разделена на шаги, каждый из которых будет содержать код, хранящийся в отдельной ветке репозитория на GitHub. Вы всегда сможете запустить и посмотреть результаты, а так же поиграться с кодом.
Для запуска приложения нужно иметь на своей машине компилятор Go. Я исхожу из предположения, что он у вас уже есть и настроен так, как вам удобно. Если же нет, то узнать как это сделать вы можете на странице проекта.
В качестве среды для разработки, вы можете использовать то, что вам удобнее, благо, плагины для Go есть почти под каждый редактор. Наиболее популярнен GoSublime. Но я бы посоветовал IntelijIdea + go-lang-ide-plugin, который последнее время очень активно развивается, например из последнего добавленного — дебаг приложения.
Попробовать уже готовый сервис в работе можно по ссылке skimmer.tulu.la.
Для начала работы нужно склонировать репозиторий к себе на машину в какую-нибудь директорию, например так:
Вы можете добавить проект в своё рабочее окружение (подробнее об этом можно прочитать на сайте проекта), либо организовывать код, как вам удобно. Я же для простоты изложения, использую goenv, позволяющий указывать версии компилятора go и создавать чистое рабочее окружение в директории проекта.
Теперь нам нужно зайти в склонированную директорию skimmer и установить нужные зависимости командой:
После завершения установки зависимости, можно запустить проект:
У вас должен запуститься веб-сервис на порту 3000 (порт и хост можно указать через переменные окружения PORT и HOST соответственно). Теперь можно открыть его в браузере по адресу 127.0.0.1:3000 и попробовать уже готовый сервис в работе.
Впереди нас ждут следующие этапы:
Особая благодарность kavu за коррекцию первой и второй части статьи.
Приступим к разработке.
Загрузим код первого шага:
Для начала попробуем просто вывести запрос, приходящий к нам. Точка входа в любое приложение на Go, это функция main пакета main. Создадим в директории src файл main.go. В Martini уже есть заготовка приложения, добавляющая логи, обработку ошибок, возможность восстановления и роутер; и дабы не повторяться, мы воспользуемся ей.
Сам по себе Martini достаточно прост:
Он реализует интерфейс http.Handler, имплементируя метод ServeHTTP. Далее все приходящие запросы пропускаются через различные обработчики, хранящиеся в handlers и в конце выполняет Handler action.
Классический Martini:
В этом конструкторе создаётся объект типа Martini и Router, в обработчики handler через метод martini.Use добавляется логирование запросов, перехват panic (подробнее об этом механизме), отдача статики, и последним действием устанавливается обработчик роутера.
Мы будем перехватывать любые HTTP запросы к нашему приложению, используя метод
Если очень хочется — можно реализовать свою имплементацию обработчика адресов, но мы воспользуемся той, что идет в Martini по умолчанию.
Первым параметром указывается локейшен. Локейшены в Martini поддерживают параметры через
Используя готовую функцию DumpRequest из пакета httputil мы сохраняем структуру запроса http.Request, и записываем его в ответ http.ResponseWriter. Так же не забываем обрабатывать возможные ошибки. Функция api.Run просто запускает встроенный сервер go из стандартной библиотеки, указывая порт и хост, которые она берёт из параметров окружения PORT(3000 по умолчанию) и HOST.
Запустим наше первое приложение:
Попробуем отправить запрос к серверу:
Это была всего лишь проба сил, теперь приступим к написанию настоящего приложения.
Не забываем загрузить код:
Размещать код внутри пакета main не очень правильно, так как, например Google Application Engine создаёт свой пакет main, в котором уже подключаются ваши. Поэтому вынесем создание API в отдельный модуль, назовём его, например skimmer/api.go.
Теперь нам нужно создать сущность, в которой мы сможем хранить пойманные запросы, назовём её Bin, по аналогии с requestbin. Моделью у нас будет просто обычная структура данных Go.
Итак, наша модель Bin будет содержать поля с названием, количеством пойманных запросов, и датами создания и изменения. Каждое поле у нас так же описывается тэгом.
Так же мы описываем вспомогательную функцию NewBin, в которой происходит инициализация значений объекта Bin (своего рода конструктор):
Теперь более подробно про генерацию строк через объект rs. Он инициализирован следующим образом:
Сам код находится в файле utils.go. В функцию мы передаём массив символов, из которых нужно генерировать строчку и создаём объект RandomString:
Здесь мы используем пакет math/rand, предоставляющий нам доступ к генерации случайных чисел. Самое главное, посеять генератор перед началом работы с ним, чтобы у нас не получилась одинаковая последовательность случайных чисел при каждом запуске.
В методе Generate мы создаём массив байтов, и каждый из байтов заполняем случайным символом из строки pool. Получившуюся в итоге строку возвращаем.
Перейдём, собственно, к описанию Api. Для начала нам нужно три метода для работы с объектами типа Bin, вывода списка объектов, создание и получение конкретного объекта.
Ранее я писал, что martini принимает в обработчик функцию с интерфейсом HandlerFunc, на самом деле, принимаемая функция в Martini описывается как interface{} — то есть это может быть абсолютно любая функция. Каким же образом в эту функцию вставляются аргументы? Делается это при помощи известного паттерна — Dependency injection (далее DI) при помощи небольшого пакета inject от автора martini. Не буду вдаваться в подробности относительно того, как это сделано, вы можете посмотреть в код самостоятельно, благо он не большой и там всё довольно просто. Но если двумя словами, то при помощи уже упомянутого пакета reflect, получаются типы аргументов функции и после этого подставляются нужные объекты этого типа. Например когда inject видит тип *http.Request, он подставляет объект req *http.Request в этот параметр.
Мы можем сами добавлять нужные объекты для рефлексии через методы объекта Map и MapTo глобально, либо через объект контекста запроса martini.Context для каждого запроса отдельно.
Объявим временные переменные history и bins, первый будет содержать историю созданных нами объектов Bin, а второй будет некой куцей версией хранилища объектов Bin.
Теперь рассмотрим созданные методы.
Метод позволяющий получить объект Bin по его имени, в нём мы используем объект martini.Params (по сути просто map[string]string), через который можем доступиться к разобранным параметрам адреса.
Поэкспериментируем с нашим приложением. Для начала запустим его:
Добавим новый объект Bin:
Получим список доступных нам Bin объектов:
Запросим конкретный объект Bin, взяв значение name из предыдущего запроса:
Отлично, теперь мы научились создавать модели и отвечать на запросы, кажется теперь нас ничего не удержит от того, чтобы доделать всё остальное.
Теперь нам нужно научиться сохранять запросы, приходящие к нам, в нужный объект Bin.
Загрузим код для третьего шага
Для начала создадим модель, которая будет хранить в себе HTTP запрос.
Объяснять какое поле для чего нужно, полагаю смысла нет, но есть пара замечаний: для файлов мы будем хранить только их названия, а для данных формы — будем хранить уже готовый словарь значений.
По аналогии с созданием объекта Bin, напишем функцию создающую объект Request из HTTP запроса:
Функция получилась достаточно большой, но в целом, понятной, поясню только некоторые моменты. В объекте http.Request, тело запроса — Body это некий буффер, реализующий интерфейс io.ReadCloser, по этой причине после разбора формы (вызов метода ParseMultipartForm), мы уже никак не сможем получить сырые данные запроса. Поэтому для начала мы копируем Body в отдельную переменную и после заменим исходный буфер своим. Далее мы вызываем разбор входящих данных и собираем информацию о значениях форм и файлов.
Помимо объектов Bin, теперь нам нужно так же хранить и запросы, поэтому, пришло время добавить в наш проект возможность хранения данных. Опишем его интерфейс в файле storage.go:
Помимо этого создадим базовый объект storage, в котором будут вспомогательные поля, которые потребуются нам в каждой имплементации:
Теперь пришло время реализовать поведение нашего интерфейса хранилища. Для начала попробуем всё хранить в памяти, разграничивая параллельный доступ к данным мьютексами.
Создадим файл memory.go В основе нашего хранилища будет простая структура данных:
Она состоит из вложенных, анонимных полей BaseStorage и sync.RWMutex.
RWMutex нам нужен, чтобы блокировать одновременную работу со словарём binRecords, так как Go не гарантирует правильного поведения при параллельном изменении данных в словарях.
Сами данные будут хранится в поле binRecords, которой является словарём с ключами из поля name Bin объектов и данными вида BinRecord.
В этой структуре собраны все нужные данные. Ссылки на запросы хранятся в двух полях, в списке, где они идут по порядку добавления и в словаре, для более быстрого поиска по идентификатору.
Так же для объекта BinRecord реализован метод для обрезания лишних запросов, который просто удаляет ненужные элементы из requests и requestMap.
Все методы MemoryStorage имплементируют поведение интерфейса Storage, так же у нас есть вспомогательный метод getBinRecord, в котором мы можем прочитать нужную нам запись. В момент когда мы читаем запись, мы ставим блокировку на чтение и сразу же указываем отложенный вызов снятия блокировки в defer. Выражение defer позволяет нам указывать функцию, которая будет всегда выполнена по завершении работы функции, даже если функцию была прервана паникой. Подробнее почитать о defer можно в документации
Подробнее рассматривать каждый метод MemoryStorage смысла нет, там всё и так не сложно, вы можете заглянуть в код самостоятельно.
Теперь, когда у нас есть хранилище, можно приступать к описанию api. Посмотрим что у нас изменяется.
Во первых мы добавляем поддержку нашего нового хранилища.
Теперь в любом хендлере мы можем добавить параметр типа Storage и получить доступ к нашему хранилищу. Что мы и делаем, заменив во всех обработчиках запросов к Bin работу со словарём на вызовы к Storage.
Во вторых, добавили обработчики для объектов типа Request.
Попробуем запустить то, что у нас получилось и отправить несколько запросов.
Создадим контейнер Bin для наших HTTP запросов
Отправим запрос в наш контейнер
Проверим, сохранился ли наш запрос:
Кажется, всё работает как надо, но чтобы быть в этом точно уверенными нужно покрыть код тестами.
Продолжение статьи во второй части, где мы узнаем как писать тесты, реализуем одностраничный веб-интерфейс на основе AngularJS и Bootstrap, добавим немного приватности и внедрим поддержку Redis для хранения.
Часто, при разработке сервисов, нужно понимать какие данные отправляются в другой сервис, а возможность перехватить траффик есть не всегда. И как раз для того, чтобы отлавливать подобные запросы, существует проект requestb.in, позволяющий собирать запросы по определённому урлу и отображать их в веб-интерфейсе. Написанием подобного же приложения мы и займёмся. Чтобы немного упростить себе задачу, возьмём за основу какой-нибудь фреймворк, например Martini.
В конечном итоге, у нас должен будет получится вот такой вот сервис:
Подготовка
Эта статья будет разделена на шаги, каждый из которых будет содержать код, хранящийся в отдельной ветке репозитория на GitHub. Вы всегда сможете запустить и посмотреть результаты, а так же поиграться с кодом.
Для запуска приложения нужно иметь на своей машине компилятор Go. Я исхожу из предположения, что он у вас уже есть и настроен так, как вам удобно. Если же нет, то узнать как это сделать вы можете на странице проекта.
В качестве среды для разработки, вы можете использовать то, что вам удобнее, благо, плагины для Go есть почти под каждый редактор. Наиболее популярнен GoSublime. Но я бы посоветовал IntelijIdea + go-lang-ide-plugin, который последнее время очень активно развивается, например из последнего добавленного — дебаг приложения.
Попробовать уже готовый сервис в работе можно по ссылке skimmer.tulu.la.
Для начала работы нужно склонировать репозиторий к себе на машину в какую-нибудь директорию, например так:
git clone https://github.com/m0sth8/skimmer ./skimmer
Вы можете добавить проект в своё рабочее окружение (подробнее об этом можно прочитать на сайте проекта), либо организовывать код, как вам удобно. Я же для простоты изложения, использую goenv, позволяющий указывать версии компилятора go и создавать чистое рабочее окружение в директории проекта.
Теперь нам нужно зайти в склонированную директорию skimmer и установить нужные зависимости командой:
go get -d ./src/
После завершения установки зависимости, можно запустить проект:
go run ./src/main.go
У вас должен запуститься веб-сервис на порту 3000 (порт и хост можно указать через переменные окружения PORT и HOST соответственно). Теперь можно открыть его в браузере по адресу 127.0.0.1:3000 и попробовать уже готовый сервис в работе.
Впереди нас ждут следующие этапы:
- Шаг первый. Знакомство с Martini;
- Шаг второй. Создаём модель Bin и отвечаем на запросы;
- Шаг третий. Принимаем запросы и сохраняем их в хранилище;
- Шаг четвёртый. А как же тесты?
- Шаг пятый— украшательства и веб-интерфейс;
- Шаг шестой. Добавляем немного приватности;
- Шаг седьмой. Очищаем ненужное;
- Шаг восьмой. Используем Redis для хранения.
Особая благодарность kavu за коррекцию первой и второй части статьи.
Приступим к разработке.
Шаг первый. Знакомство с Martini.
Загрузим код первого шага:
git checkout step-1
Для начала попробуем просто вывести запрос, приходящий к нам. Точка входа в любое приложение на Go, это функция main пакета main. Создадим в директории src файл main.go. В Martini уже есть заготовка приложения, добавляющая логи, обработку ошибок, возможность восстановления и роутер; и дабы не повторяться, мы воспользуемся ей.
Сам по себе Martini достаточно прост:
// Martini represents the top level web application. inject.Injector methods can be invoked to map services on a global level.
type Martini struct {
inject.Injector
handlers []Handler
action Handler
logger *log.Logger
}
Он реализует интерфейс http.Handler, имплементируя метод ServeHTTP. Далее все приходящие запросы пропускаются через различные обработчики, хранящиеся в handlers и в конце выполняет Handler action.
Классический Martini:
// Classic creates a classic Martini with some basic default middleware - martini.Logger, martini.Recovery, and martini.Static.
func Classic() *ClassicMartini {
r := NewRouter()
m := New()
m.Use(Logger())
m.Use(Recovery())
m.Use(Static("public"))
m.Action(r.Handle)
return &ClassicMartini{m, r}
}
В этом конструкторе создаётся объект типа Martini и Router, в обработчики handler через метод martini.Use добавляется логирование запросов, перехват panic (подробнее об этом механизме), отдача статики, и последним действием устанавливается обработчик роутера.
Мы будем перехватывать любые HTTP запросы к нашему приложению, используя метод
Any
у роутера, перехватывающий любые урлы и методы. Интерфейс роутера описан в Martini вот так:type Router interface {
// Get adds a route for a HTTP GET request to the specified matching pattern.
Get(string, ...Handler) Route
// Patch adds a route for a HTTP PATCH request to the specified matching pattern.
Patch(string, ...Handler) Route
// Post adds a route for a HTTP POST request to the specified matching pattern.
Post(string, ...Handler) Route
// Put adds a route for a HTTP PUT request to the specified matching pattern.
Put(string, ...Handler) Route
// Delete adds a route for a HTTP DELETE request to the specified matching pattern.
Delete(string, ...Handler) Route
// Options adds a route for a HTTP OPTIONS request to the specified matching pattern.
Options(string, ...Handler) Route
// Any adds a route for any HTTP method request to the specified matching pattern.
Any(string, ...Handler) Route
// NotFound sets the handlers that are called when a no route matches a request. Throws a basic 404 by default.
NotFound(...Handler)
// Handle is the entry point for routing. This is used as a martini.Handler
Handle(http.ResponseWriter, *http.Request, Context)
}
Если очень хочется — можно реализовать свою имплементацию обработчика адресов, но мы воспользуемся той, что идет в Martini по умолчанию.
Первым параметром указывается локейшен. Локейшены в Martini поддерживают параметры через
":param"
, регулярные выражения, а так же glob. Второй параметр и последующие, принимают функцию, которая будет заниматься обработкой запроса. Так как Martini поддерживает цепочку обработчиков, сюда можно добавлять различные вспомогательные хендлеры, например проверку прав доступа. Нам пока это ни к чему, поэтому добавим только один обработчик c интерфейсом, обрабатываемым обычным веб обработчиком Go (пример разработки на нём можно посмотреть в документации). Вот код нашего обработчика:func main() {
api := martini.Classic()
api.Any("/", func(res http.ResponseWriter, req *http.Request,) {
if dumped, err := httputil.DumpRequest(req, true); err == nil {
res.WriteHeader(200)
res.Write(dumped)
} else {
res.WriteHeader(500)
fmt.Fprintf(res, "Error: %v", err)
}
})
api.Run()
}
Используя готовую функцию DumpRequest из пакета httputil мы сохраняем структуру запроса http.Request, и записываем его в ответ http.ResponseWriter. Так же не забываем обрабатывать возможные ошибки. Функция api.Run просто запускает встроенный сервер go из стандартной библиотеки, указывая порт и хост, которые она берёт из параметров окружения PORT(3000 по умолчанию) и HOST.
Запустим наше первое приложение:
go run ./src/main.go
Попробуем отправить запрос к серверу:
> curl -X POST -d "fizz=buzz" http://127.0.0.1:3000
POST / HTTP/1.1
Host: 127.0.0.1:3000
Accept: */*
Content-Type: application/x-www-form-urlencoded
User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8y zlib/1.2.5
fizz=buzz
Это была всего лишь проба сил, теперь приступим к написанию настоящего приложения.
Шаг второй. Создаём модель Bin и отвечаем на запросы.
Не забываем загрузить код:
git checkout step-2
Размещать код внутри пакета main не очень правильно, так как, например Google Application Engine создаёт свой пакет main, в котором уже подключаются ваши. Поэтому вынесем создание API в отдельный модуль, назовём его, например skimmer/api.go.
Теперь нам нужно создать сущность, в которой мы сможем хранить пойманные запросы, назовём её Bin, по аналогии с requestbin. Моделью у нас будет просто обычная структура данных Go.
Порядок полей в структуре достаточно важен, но мы не будем задумываться об этом, но те кто хотят узнать как порядок влияет на размер структуры в памяти, могут почитать вот эти статьи — www.goinggo.net/2013/07/understanding-type-in-go.html и www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing.
Итак, наша модель Bin будет содержать поля с названием, количеством пойманных запросов, и датами создания и изменения. Каждое поле у нас так же описывается тэгом.
Тэги это обычные строки, которые никак не влияют на программу в целом, но их можно прочитать используя пакет reflection во время работы программы (так называемая интроспекция), и исходя из этого изменять своё поведение (о том как работать тэгами через reflection). В нашем примере, пакет json при кодировании/раскодировании учитывает значение тэга, примерно так:
package main import ( "reflect" "fmt" ) type Bin struct { Name string `json:"name"` } func main() { bin := Bin{} bt := reflect.TypeOf(bin) field := bt.Field(0) fmt.Printf("Field's '%s' json name is '%s'", field.Name, field.Tag.Get("json")) }
Выведет
Field's 'Name' json name is 'name'
Пакет encoding/json поддерживает различные опции при формировании тэгов:
// Поле игнорируется Field int `json:"-"` // В json структуре поле интерпретируется как myName Field int `json:"myName"`
Вторым параметром может быть например, опция omitempty — если значение в json пропущено, то поле не заполняется. Так например, если поле будет ссылкой, мы сможем узнать, присутствует ли оно в json объекте, сравнив его с nil. Более подробно о json сериализации можно почитать в документации
Так же мы описываем вспомогательную функцию NewBin, в которой происходит инициализация значений объекта Bin (своего рода конструктор):
type Bin struct {
Name string `json:"name"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
RequestCount int `json:"requestCount"`
}
func NewBin() *Bin {
now := time.Now().Unix()
bin := Bin{
Created: now,
Updated: now,
Name: rs.Generate(6),
}
return &bin
}
Структуры в Go могут иницилизироваться двумя способами:
1) Обязательным перечислением всех полей по порядку:
Bin{rs.Generate(6), now, now, 0}
2) Указанием полей, для которых присваиваются значения:
Bin{ Created: now, Updated: now, Name: rs.Generate(6), }
Поля, которые не указаны, принимают значения по умолчанию. Например для целых чисел это будет 0, для строк — пустая строка "", для ссылок, каналов, массивов, слайсов и словарей — это будет nil. Подробнее в документации. Главное помнить, что смешивать эти два типа инициализации нельзя.
Теперь более подробно про генерацию строк через объект rs. Он инициализирован следующим образом:
var rs = NewRandomString("0123456789abcdefghijklmnopqrstuvwxyz")
Сам код находится в файле utils.go. В функцию мы передаём массив символов, из которых нужно генерировать строчку и создаём объект RandomString:
type RandomString struct {
pool string
rg *rand.Rand
}
func NewRandomString(pool string) *RandomString {
return &RandomString{
pool,
rand.New(rand.NewSource(time.Now().Unix())),
}
}
func (rs *RandomString) Generate(length int) (r string) {
if length < 1 {
return
}
b := make([]byte, length)
for i, _ := range b {
b[i] = rs.pool[rs.rg.Intn(len(rs.pool))]
}
r = string(b)
return
}
Здесь мы используем пакет math/rand, предоставляющий нам доступ к генерации случайных чисел. Самое главное, посеять генератор перед началом работы с ним, чтобы у нас не получилась одинаковая последовательность случайных чисел при каждом запуске.
В методе Generate мы создаём массив байтов, и каждый из байтов заполняем случайным символом из строки pool. Получившуюся в итоге строку возвращаем.
Перейдём, собственно, к описанию Api. Для начала нам нужно три метода для работы с объектами типа Bin, вывода списка объектов, создание и получение конкретного объекта.
Ранее я писал, что martini принимает в обработчик функцию с интерфейсом HandlerFunc, на самом деле, принимаемая функция в Martini описывается как interface{} — то есть это может быть абсолютно любая функция. Каким же образом в эту функцию вставляются аргументы? Делается это при помощи известного паттерна — Dependency injection (далее DI) при помощи небольшого пакета inject от автора martini. Не буду вдаваться в подробности относительно того, как это сделано, вы можете посмотреть в код самостоятельно, благо он не большой и там всё довольно просто. Но если двумя словами, то при помощи уже упомянутого пакета reflect, получаются типы аргументов функции и после этого подставляются нужные объекты этого типа. Например когда inject видит тип *http.Request, он подставляет объект req *http.Request в этот параметр.
Мы можем сами добавлять нужные объекты для рефлексии через методы объекта Map и MapTo глобально, либо через объект контекста запроса martini.Context для каждого запроса отдельно.
Объявим временные переменные history и bins, первый будет содержать историю созданных нами объектов Bin, а второй будет некой куцей версией хранилища объектов Bin.
Теперь рассмотрим созданные методы.
Создание объекта Bin
api.Post("/api/v1/bins/", func(r render.Render){
bin := NewBin()
bins[bin.Name] = bin
history = append(history, bin.Name)
r.JSON(http.StatusCreated, bin)
})
Получение списка объектов Bin
api.Get("/api/v1/bins/", func(r render.Render){
filteredBins := []*Bin{}
for _, name := range(history) {
if bin, ok := bins[name]; ok {
filteredBins = append(filteredBins, bin)
}
}
r.JSON(http.StatusOK, filteredBins)
})
Получение конкретного экземпляра
api.Get("/api/v1/bins/:bin", func(r render.Render, params martini.Params){
if bin, ok := bins[params["bin"]]; ok{
r.JSON(http.StatusOK, bin)
} else {
r.Error(http.StatusNotFound)
}
})
Метод позволяющий получить объект Bin по его имени, в нём мы используем объект martini.Params (по сути просто map[string]string), через который можем доступиться к разобранным параметрам адреса.
В языке Go мы можем обратиться к элементу словаря двумя способами:
- Запросив значение ключа
a := m[key]
, в этом случае вернётся либо значение ключа в словаре, если оно есть, либо дефолтное значение инициализации типа значения. Таким образом, например для чисел, сложно понять, содержит ли ключ 0 или просто значения этого ключа не существует. Поэтому в го предусмотрен второй вариант.- В этом способе, запросив по ключу и получить его значение первым параметром и индикатор существования этого ключа вторым параметром —
a, ok := m[key]
Поэкспериментируем с нашим приложением. Для начала запустим его:
go run ./src/main.go
Добавим новый объект Bin:
> curl -i -X POST "127.0.0.1:3000/api/v1/bins/"
HTTP/1.1 201 Created
Content-Type: application/json; charset=UTF-8
Date: Mon, 03 Mar 2014 04:10:38 GMT
Content-Length: 76
{"name":"7xpogf","created":1393819838,"updated":1393819838,"requestCount":0}
Получим список доступных нам Bin объектов:
> curl -i "127.0.0.1:3000/api/v1/bins/"
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Mon, 03 Mar 2014 04:11:18 GMT
Content-Length: 78
[{"name":"7xpogf","created":1393819838,"updated":1393819838,"requestCount":0}]
Запросим конкретный объект Bin, взяв значение name из предыдущего запроса:
curl -i "127.0.0.1:3000/api/v1/bins/7xpogf"
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Mon, 03 Mar 2014 04:12:13 GMT
Content-Length: 76
{"name":"7xpogf","created":1393819838,"updated":1393819838,"requestCount":0}
Отлично, теперь мы научились создавать модели и отвечать на запросы, кажется теперь нас ничего не удержит от того, чтобы доделать всё остальное.
Шаг третий. Принимаем запросы и сохраняем их в хранилище.
Теперь нам нужно научиться сохранять запросы, приходящие к нам, в нужный объект Bin.
Загрузим код для третьего шага
git checkout step-3
Модель Request
Для начала создадим модель, которая будет хранить в себе HTTP запрос.
type Request struct {
Id string `json:"id"`
Created int64 `json:"created"`
Method string `json:"method"` // GET, POST, PUT, etc.
Proto string `json:"proto"` // "HTTP/1.0"
Header http.Header `json:"header"`
ContentLength int64 `json:"contentLength"`
RemoteAddr string `json:"remoteAddr"`
Host string `json:"host"`
RequestURI string `json:"requestURI"`
Body string `json:"body"`
FormValue map[string][]string `json:"formValue"`
FormFile []string `json:"formFile"`
}
Объяснять какое поле для чего нужно, полагаю смысла нет, но есть пара замечаний: для файлов мы будем хранить только их названия, а для данных формы — будем хранить уже готовый словарь значений.
По аналогии с созданием объекта Bin, напишем функцию создающую объект Request из HTTP запроса:
func NewRequest(httpRequest *http.Request, maxBodySize int) *Request {
var (
bodyValue string
formValue map[string][]string
formFile []string
)
// Считываем тело приходящего запроса из буфера и подменяем исходный буфер на новый
if body, err := ioutil.ReadAll(httpRequest.Body); err == nil {
if len(body) > 0 && maxBodySize != 0 {
if maxBodySize == -1 || httpRequest.ContentLength < int64(maxBodySize) {
bodyValue = string(body)
} else {
bodyValue = fmt.Sprintf("%s\n<<<TRUNCATED , %d of %d", string(body[0:maxBodySize]),
maxBodySize, httpRequest.ContentLength)
}
}
httpRequest.Body = ioutil.NopCloser(bytes.NewBuffer(body))
defer httpRequest.Body.Close()
}
httpRequest.ParseMultipartForm(0)
if httpRequest.MultipartForm != nil {
formValue = httpRequest.MultipartForm.Value
for key := range httpRequest.MultipartForm.File {
formFile = append(formFile, key)
}
} else {
formValue = httpRequest.PostForm
}
request := Request{
Id: rs.Generate(12),
Created: time.Now().Unix(),
Method: httpRequest.Method,
Proto: httpRequest.Proto,
Host: httpRequest.Host,
Header: httpRequest.Header,
ContentLength: httpRequest.ContentLength,
RemoteAddr: httpRequest.RemoteAddr,
RequestURI: httpRequest.RequestURI,
FormValue: formValue,
FormFile: formFile,
Body: bodyValue,
}
return &request
}
Функция получилась достаточно большой, но в целом, понятной, поясню только некоторые моменты. В объекте http.Request, тело запроса — Body это некий буффер, реализующий интерфейс io.ReadCloser, по этой причине после разбора формы (вызов метода ParseMultipartForm), мы уже никак не сможем получить сырые данные запроса. Поэтому для начала мы копируем Body в отдельную переменную и после заменим исходный буфер своим. Далее мы вызываем разбор входящих данных и собираем информацию о значениях форм и файлов.
Помимо объектов Bin, теперь нам нужно так же хранить и запросы, поэтому, пришло время добавить в наш проект возможность хранения данных. Опишем его интерфейс в файле storage.go:
type Storage interface {
LookupBin(name string) (*Bin, error) // get one bin element by name
LookupBins(names []string) ([]*Bin, error) // get slice of bin elements
LookupRequest(binName, id string) (*Request, error) // get request from bin by id
LookupRequests(binName string, from, to int) ([]*Request, error) // get slice of requests from bin by position
CreateBin(bin *Bin) error // create bin in memory storage
UpdateBin(bin *Bin) error // save
CreateRequest(bin *Bin, req *Request) error
}
Интерфейсы в Go являются контрактом, связывающим ожидаемую функциональность и актуальную реализацию. В нашем случае, мы описали интерфейс storage, который будем использовать в дальнейшем в программе, но в зависимости от настроек, имплементация может быть совершенно разной (например это может быть Redis или Mongo). Подробнее об интерфейсах.
Помимо этого создадим базовый объект storage, в котором будут вспомогательные поля, которые потребуются нам в каждой имплементации:
type BaseStorage struct {
maxRequests int
}
Теперь пришло время реализовать поведение нашего интерфейса хранилища. Для начала попробуем всё хранить в памяти, разграничивая параллельный доступ к данным мьютексами.
Создадим файл memory.go В основе нашего хранилища будет простая структура данных:
type MemoryStorage struct {
BaseStorage
sync.RWMutex
binRecords map[string]*BinRecord
}
Она состоит из вложенных, анонимных полей BaseStorage и sync.RWMutex.
Анонимные поля дают нам возможность вызывать методы и поля анонимных структур напрямую. Например, если у нас есть переменная obj типа MemoryStorage, мы можем доступиться к полю maxRequests напрямую obj.BaseStorage.maxRequests, либо как будто они члены самого MemoryStorage obj.maxRequests. Подробнее об анонимных полях в структурах данных можно почитать в документации.
RWMutex нам нужен, чтобы блокировать одновременную работу со словарём binRecords, так как Go не гарантирует правильного поведения при параллельном изменении данных в словарях.
Сами данные будут хранится в поле binRecords, которой является словарём с ключами из поля name Bin объектов и данными вида BinRecord.
type BinRecord struct {
bin *Bin
requests []*Request
requestMap map[string]*Request
}
В этой структуре собраны все нужные данные. Ссылки на запросы хранятся в двух полях, в списке, где они идут по порядку добавления и в словаре, для более быстрого поиска по идентификатору.
Словари в Go в текущей реализации — это хеш таблицы, поэтому поиск элемента в словаре имеет константное значение. Подробнее о внутреннем устройстве можно ознакомиться в этой прекрасной статье.
Так же для объекта BinRecord реализован метод для обрезания лишних запросов, который просто удаляет ненужные элементы из requests и requestMap.
func (binRecord *BinRecord) ShrinkRequests(size int) {
if size > 0 && len(binRecord.requests) > size {
requests := binRecord.requests
lenDiff := len(requests) - size
removed := requests[:lenDiff]
for _, removedReq := range removed {
delete(binRecord.requestMap, removedReq.Id)
}
requests = requests[lenDiff:]
binRecord.requests = requests
}
}
Все методы MemoryStorage имплементируют поведение интерфейса Storage, так же у нас есть вспомогательный метод getBinRecord, в котором мы можем прочитать нужную нам запись. В момент когда мы читаем запись, мы ставим блокировку на чтение и сразу же указываем отложенный вызов снятия блокировки в defer. Выражение defer позволяет нам указывать функцию, которая будет всегда выполнена по завершении работы функции, даже если функцию была прервана паникой. Подробнее почитать о defer можно в документации
Подробнее рассматривать каждый метод MemoryStorage смысла нет, там всё и так не сложно, вы можете заглянуть в код самостоятельно.
Код MemoryStorage
package skimmer
import (
"errors"
"sync"
)
type MemoryStorage struct {
BaseStorage
sync.RWMutex
binRecords map[string]*BinRecord
}
type BinRecord struct {
bin *Bin
requests []*Request
requestMap map[string]*Request
}
func (binRecord *BinRecord) ShrinkRequests(size int) {
if size > 0 && len(binRecord.requests) > size {
requests := binRecord.requests
lenDiff := len(requests) - size
removed := requests[:lenDiff]
for _, removedReq := range removed {
delete(binRecord.requestMap, removedReq.Id)
}
requests = requests[lenDiff:]
binRecord.requests = requests
}
}
func NewMemoryStorage(maxRequests int) *MemoryStorage {
return &MemoryStorage{
BaseStorage{
maxRequests: maxRequests,
},
sync.RWMutex{},
map[string]*BinRecord{},
}
}
func (storage *MemoryStorage) getBinRecord(name string) (*BinRecord, error) {
storage.RLock()
defer storage.RUnlock()
if binRecord, ok := storage.binRecords[name]; ok {
return binRecord, nil
}
return nil, errors.New("Bin not found")
}
func (storage *MemoryStorage) LookupBin(name string) (*Bin, error) {
if binRecord, err := storage.getBinRecord(name); err == nil {
return binRecord.bin, nil
} else {
return nil, err
}
}
func (storage *MemoryStorage) LookupBins(names []string) ([]*Bin, error) {
bins := []*Bin{}
for _, name := range names {
if binRecord, err := storage.getBinRecord(name); err == nil {
bins = append(bins, binRecord.bin)
}
}
return bins, nil
}
func (storage *MemoryStorage) CreateBin(bin *Bin) error {
storage.Lock()
defer storage.Unlock()
binRec := BinRecord{bin, []*Request{}, map[string]*Request{}}
storage.binRecords[bin.Name] = &binRec
return nil
}
func (storage *MemoryStorage) UpdateBin(_ *Bin) error {
return nil
}
func (storage *MemoryStorage) LookupRequest(binName, id string) (*Request, error) {
if binRecord, err := storage.getBinRecord(binName); err == nil {
if request, ok := binRecord.requestMap[id]; ok {
return request, nil
} else {
return nil, errors.New("Request not found")
}
} else {
return nil, err
}
}
func (storage *MemoryStorage) LookupRequests(binName string, from int, to int) ([]*Request, error) {
if binRecord, err := storage.getBinRecord(binName); err == nil {
requestLen := len(binRecord.requests)
if to >= requestLen {
to = requestLen
}
if to < 0 {
to = 0
}
if from < 0 {
from = 0
}
if from > to {
from = to
}
reversedLen := to - from
reversed := make([]*Request, reversedLen)
for i, request := range binRecord.requests[from:to] {
reversed[reversedLen-i-1] = request
}
return reversed, nil
} else {
return nil, err
}
}
func (storage *MemoryStorage) CreateRequest(bin *Bin, req *Request) error {
if binRecord, err := storage.getBinRecord(bin.Name); err == nil {
storage.Lock()
defer storage.Unlock()
binRecord.requests = append(binRecord.requests, req)
binRecord.requestMap[req.Id] = req
binRecord.ShrinkRequests(storage.maxRequests)
binRecord.bin.RequestCount = len(binRecord.requests)
return nil
} else {
return err
}
}
Теперь, когда у нас есть хранилище, можно приступать к описанию api. Посмотрим что у нас изменяется.
Во первых мы добавляем поддержку нашего нового хранилища.
memoryStorage := NewMemoryStorage(MAX_REQUEST_COUNT)
api.MapTo(memoryStorage, (*Storage)(nil))
Теперь в любом хендлере мы можем добавить параметр типа Storage и получить доступ к нашему хранилищу. Что мы и делаем, заменив во всех обработчиках запросов к Bin работу со словарём на вызовы к Storage.
api.Post("/api/v1/bins/", func(r render.Render, storage Storage){
bin := NewBin()
if err := storage.CreateBin(bin); err == nil {
history = append(history, bin.Name)
r.JSON(http.StatusCreated, bin)
} else {
r.JSON(http.StatusInternalServerError, ErrorMsg{err.Error()})
}
})
api.Get("/api/v1/bins/", func(r render.Render, storage Storage){
if bins, err := storage.LookupBins(history); err == nil {
r.JSON(http.StatusOK, bins)
} else {
r.JSON(http.StatusInternalServerError, ErrorMsg{err.Error()})
}
})
api.Get("/api/v1/bins/:bin", func(r render.Render, params martini.Params, storage Storage){
if bin, err := storage.LookupBin(params["bin"]); err == nil{
r.JSON(http.StatusOK, bin)
} else {
r.JSON(http.StatusNotFound, ErrorMsg{err.Error()})
}
})
Во вторых, добавили обработчики для объектов типа Request.
// список всех реквестов
api.Get("/api/v1/bins/:bin/requests/", func(r render.Render, storage Storage, params martini.Params,
req *http.Request){
if bin, error := storage.LookupBin(params["bin"]); error == nil {
from := 0
to := 20
if fromVal, err := strconv.Atoi(req.FormValue("from")); err == nil {
from = fromVal
}
if toVal, err := strconv.Atoi(req.FormValue("to")); err == nil {
to = toVal
}
if requests, err := storage.LookupRequests(bin.Name, from, to); err == nil {
r.JSON(http.StatusOK, requests)
} else {
r.JSON(http.StatusInternalServerError, ErrorMsg{err.Error()})
}
} else {
r.Error(http.StatusNotFound)
}
})
// доступ к конкретному экземпляру Request
api.Get("/api/v1/bins/:bin/requests/:request", func(r render.Render, storage Storage, params martini.Params){
if request, err := storage.LookupRequest(params["bin"], params["request"]); err == nil {
r.JSON(http.StatusOK, request)
} else {
r.JSON(http.StatusNotFound, ErrorMsg{err.Error()})
}
})
// сохранение http запроса в объект Request контейнера Bin(name)
api.Any("/bins/:name", func(r render.Render, storage Storage, params martini.Params,
req *http.Request){
if bin, error := storage.LookupBin(params["name"]); error == nil {
request := NewRequest(req, REQUEST_BODY_SIZE)
if err := storage.CreateRequest(bin, request); err == nil {
r.JSON(http.StatusOK, request)
} else {
r.JSON(http.StatusInternalServerError, ErrorMsg{err.Error()})
}
} else {
r.Error(http.StatusNotFound)
}
})
Попробуем запустить то, что у нас получилось и отправить несколько запросов.
Создадим контейнер Bin для наших HTTP запросов
> curl -i -X POST "127.0.0.1:3000/api/v1/bins/"
HTTP/1.1 201 Created
Content-Type: application/json; charset=UTF-8
Date: Mon, 03 Mar 2014 12:19:28 GMT
Content-Length: 76
{"name":"ws87ui","created":1393849168,"updated":1393849168,"requestCount":0}
Отправим запрос в наш контейнер
> curl -X POST -d "fizz=buzz" http://127.0.0.1:3000/bins/ws87ui
{"id":"i0aigrrc1b40","created":1393849284,...}
Проверим, сохранился ли наш запрос:
> curl http://127.0.0.1:3000/api/v1/bins/ws87ui/requests/
[{"id":"i0aigrrc1b40","created":1393849284,...}]
Кажется, всё работает как надо, но чтобы быть в этом точно уверенными нужно покрыть код тестами.
Продолжение статьи во второй части, где мы узнаем как писать тесты, реализуем одностраничный веб-интерфейс на основе AngularJS и Bootstrap, добавим немного приватности и внедрим поддержку Redis для хранения.