Пишем веб сервис на Go (часть первая)
Часто, при разработке сервисов, нужно понимать какие данные отправляются в другой сервис, а возможность перехватить траффик есть не всегда. И как раз для того, чтобы отлавливать подобные запросы, существует проект requestb.in/, позволяющий собирать запросы по определённому урлу и отображать их в веб-интерфейсе. Написанием подобного же приложения мы и займёмся. Чтобы немного упростить себе задачу, возьмём за основу какой-нибудь фреймворк, например Martini.
В конечном итоге, у нас должен будет получится вот такой вот сервис:
Подготовка
Эта статья будет разделена на шаги, каждый из которых будет содержать код, хранящийся в отдельной ветке репозитория на GitHub. Вы всегда сможете запустить и посмотреть результаты, а так же поиграться с кодом.
Для запуска приложения нужно иметь на своей машине компилятор Go. Я исхожу из предположения, что он у вас уже есть и настроен так, как вам удобно. Если же нет, то узнать как это сделать вы можете на странице проекта.
В качестве среды для разработки, вы можете использовать то, что вам удобнее, благо, плагины для Go есть почти под каждый редактор. Наиболее популярнен GoSublime. Но я бы посоветовал IntelijIdea + go-lang-ide-plugin, который последнее время очень активно развивается, например из последнего добавленного — дебаг приложения.
Попробовать уже готовый сервис в работе можно по ссылке skimmer.tulu.la/.
Для начала работы нужно склонировать репозиторий к себе на машину в какую-нибудь директорию, например так:
1 2 |
git clone https://github.com/m0sth8/skimmer ./skimmer |
Вы можете добавить проект в своё рабочее окружение (подробнее об этом можно прочитать на сайте проекта), либо организовывать код, как вам удобно. Я же для простоты изложения, использую goenv, позволяющий указывать версии компилятора go и создавать чистое рабочее окружение в директории проекта.
Теперь нам нужно зайти в склонированную директорию skimmer и установить нужные зависимости командой:
1 2 |
go get -d ./src/ |
После завершения установки зависимости, можно запустить проект:
1 2 |
go run ./src/main.go |
У вас должен запуститься веб-сервис на порту 3000 (порт и хост можно указать через переменные окружения PORT и HOST соответственно). Теперь можно открыть его в браузере по адресу 127.0.0.1:3000 и попробовать уже готовый сервис в работе.
Впереди нас ждут следующие этапы:
- Шаг первый. Знакомство с Martini;
- Шаг второй. Создаём модель Bin и отвечаем на запросы;
- Шаг третий. Принимаем запросы и сохраняем их в хранилище;
- Шаг четвёртый. А как же тесты?
- Шаг пятый— украшательства и веб-интерфейс;
- Шаг шестой. Добавляем немного приватности;
- Шаг седьмой. Очищаем ненужное;
- Шаг восьмой. Используем Redis для хранения.
Особая благодарность kavu за коррекцию первой и второй части статьи.
Приступим к разработке.
Шаг первый. Знакомство с Martini.
Загрузим код первого шага:
1 2 |
git checkout step-1 |
Сам по себе Martini достаточно прост:
1 2 3 4 5 6 7 8 |
// 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:
1 2 3 4 5 6 7 8 9 10 11 |
// 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 вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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 (пример разработки на нём можно посмотреть в документации). Вот код нашего обработчика:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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.
Запустим наше первое приложение:
1 2 |
go run ./src/main.go |
Попробуем отправить запрос к серверу:
1 2 3 4 5 6 7 8 9 |
> 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 и отвечаем на запросы.
Не забываем загрузить код:
1 2 |
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 при кодировании/раскодировании учитывает значение тэга, примерно так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 поддерживает различные опции при формировании тэгов:
1 2 3 4 5 6 |
// Поле игнорируется Field int `json:"-"` // В json структуре поле интерпретируется как myName Field int `json:"myName"` |
Вторым параметром может быть например, опция omitempty — если значение в json пропущено, то поле не заполняется. Так например, если поле будет ссылкой, мы сможем узнать, присутствует ли оно в json объекте, сравнив его с nil. Более подробно о json сериализации можно почитать в документации
Так же мы описываем вспомогательную функцию NewBin, в которой происходит инициализация значений объекта Bin (своего рода конструктор):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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) Обязательным перечислением всех полей по порядку:
1 2 |
Bin{rs.Generate(6), now, now, 0} |
2) Указанием полей, для которых присваиваются значения:
1 2 3 4 5 6 |
Bin{ Created: now, Updated: now, Name: rs.Generate(6), } |
Поля, которые не указаны, принимают значения по умолчанию. Например для целых чисел это будет 0, для строк — пустая строка “”, для ссылок, каналов, массивов, слайсов и словарей — это будет nil. Подробнее в документации. Главное помнить, что смешивать эти два типа инициализации нельзя.
Теперь более подробно про генерацию строк через объект rs. Он инициализирован следующим образом:
1 2 |
var rs = NewRandomString("0123456789abcdefghijklmnopqrstuvwxyz") |
Сам код находится в файле utils.go. В функцию мы передаём массив символов, из которых нужно генерировать строчку и создаём объект RandomString:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
1 2 3 4 5 6 7 |
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
1 2 3 4 5 6 7 8 9 10 |
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) }) |
Получение конкретного экземпляра
1 2 3 4 5 6 7 8 |
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) } }) |
- Запросив значение ключа
a := m[key]
, в этом случае вернётся либо значение ключа в словаре, если оно есть, либо дефолтное значение инициализации типа значения. Таким образом, например для чисел, сложно понять, содержит ли ключ 0 или просто значения этого ключа не существует. Поэтому в го предусмотрен второй вариант. - В этом способе, запросив по ключу и получить его значение первым параметром и индикатор существования этого ключа вторым параметром —
a, ok := m[key]
1 2 |
go run ./src/main.go |
1 2 3 4 5 6 7 8 |
> 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} |
1 2 3 4 5 6 7 8 |
> 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}] |
1 2 3 4 5 6 7 8 |
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} |
Шаг третий. Принимаем запросы и сохраняем их в хранилище.
1 2 |
git checkout step-3 |
Модель Request
Для начала создадим модель, которая будет хранить в себе HTTP запрос.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 запроса:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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:
1 2 3 4 5 6 7 8 9 10 |
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 } |
Помимо этого создадим базовый объект storage, в котором будут вспомогательные поля, которые потребуются нам в каждой имплементации:
1 2 3 4 |
type BaseStorage struct { maxRequests int } |
Теперь пришло время реализовать поведение нашего интерфейса хранилища. Для начала попробуем всё хранить в памяти, разграничивая параллельный доступ к данным мьютексами.
Создадим файл memory.go В основе нашего хранилища будет простая структура данных:
1 2 3 4 5 6 |
type MemoryStorage struct { BaseStorage sync.RWMutex binRecords map[string]*BinRecord } |
Она состоит из вложенных, анонимных полей BaseStorage и sync.RWMutex.
RWMutex нам нужен, чтобы блокировать одновременную работу со словарём binRecords, так как Go не гарантирует правильного поведения при параллельном изменении данных в словарях.
Сами данные будут хранится в поле binRecords, которой является словарём с ключами из поля name Bin объектов и данными вида BinRecord.
1 2 3 4 5 6 |
type BinRecord struct { bin *Bin requests []*Request requestMap map[string]*Request } |
В этой структуре собраны все нужные данные. Ссылки на запросы хранятся в двух полях, в списке, где они идут по порядку добавления и в словаре, для более быстрого поиска по идентификатору.
Так же для объекта BinRecord реализован метод для обрезания лишних запросов, который просто удаляет ненужные элементы из requests и requestMap.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 смысла нет, там всё и так не сложно, вы можете заглянуть в код самостоятельно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
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()}) } }) |
Теперь, когда у нас есть хранилище, можно приступать к описанию api. Посмотрим что у нас изменяется.
Во первых мы добавляем поддержку нашего нового хранилища.
1 2 3 |
memoryStorage := NewMemoryStorage(MAX_REQUEST_COUNT) api.MapTo(memoryStorage, (*Storage)(nil)) |
Теперь в любом хендлере мы можем добавить параметр типа Storage и получить доступ к нашему хранилищу. Что мы и делаем, заменив во всех обработчиках запросов к Bin работу со словарём на вызовы к Storage.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// список всех реквестов 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 запросов
1 2 3 4 5 6 7 8 |
> 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} |
Отправим запрос в наш контейнер
1 2 3 |
> curl -X POST -d "fizz=buzz" http://127.0.0.1:3000/bins/ws87ui {"id":"i0aigrrc1b40","created":1393849284,...} |
Проверим, сохранился ли наш запрос:
1 2 3 |
> curl http://127.0.0.1:3000/api/v1/bins/ws87ui/requests/ [{"id":"i0aigrrc1b40","created":1393849284,...}] |
Кажется, всё работает как надо, но чтобы быть в этом точно уверенными нужно покрыть код тестами.
Продолжение статьи во второй части, где мы узнаем как писать тесты, реализуем одностраничный веб-интерфейс на основе AngularJS и Bootstrap, добавим немного приватности и внедрим поддержку Redis для хранения.