- PVSM.RU - https://www.pvsm.ru -
В этой статье, я хотел бы рассказать вам, как можно достаточно быстро и легко написать небольшое веб-приложение на языке Go, который, не смотря на юнный возраст, успел завоевать расположение у многих разработчиков. Обычно, для подобных статей пишут искусственные приложения, вроде TODO листа. Мы же попробуем написать что-то полезное, что уже существует и используется.
Часто, при разработке сервисов, нужно понимать какие данные отправляются в другой сервис, а возможность перехватить траффик есть не всегда. И как раз для того, чтобы отлавливать подобные запросы, существует проект requestb.in/ [1], позволяющий собирать запросы по определённому урлу и отображать их в веб-интерфейсе. Написанием подобного же приложения мы и займёмся. Чтобы немного упростить себе задачу, возьмём за основу какой-нибудь фреймворк, например Martini [2].
В конечном итоге, у нас должен будет получится вот такой вот сервис:
Эта статья будет разделена на шаги, каждый из которых будет содержать код, хранящийся в отдельной ветке репозитория на GitHub. Вы всегда сможете запустить и посмотреть результаты, а так же поиграться с кодом.
Для запуска приложения нужно иметь на своей машине компилятор Go. Я исхожу из предположения, что он у вас уже есть и настроен так, как вам удобно. Если же нет, то узнать как это сделать вы можете на странице проекта [3].
В качестве среды для разработки, вы можете использовать то, что вам удобнее, благо, плагины для Go есть почти под каждый редактор. Наиболее популярнен GoSublime [4]. Но я бы посоветовал IntelijIdea + go-lang-ide-plugin [5], который последнее время очень активно развивается, например из последнего добавленного — дебаг приложения.
Попробовать уже готовый сервис в работе можно по ссылке skimmer.tulu.la/ [6].
Для начала работы нужно склонировать репозиторий к себе на машину в какую-нибудь директорию, например так:
git clone https://github.com/m0sth8/skimmer ./skimmer
Вы можете добавить проект в своё рабочее окружение (подробнее об этом можно прочитать на сайте проекта [7]), либо организовывать код, как вам удобно. Я же для простоты изложения, использую goenv [8], позволяющий указывать версии компилятора go и создавать чистое рабочее окружение в директории проекта.
Теперь нам нужно зайти в склонированную директорию skimmer и установить нужные зависимости командой:
go get -d ./src/
После завершения установки зависимости, можно запустить проект:
go run ./src/main.go
У вас должен запуститься веб-сервис на порту 3000 (порт и хост можно указать через переменные окружения PORT и HOST соответственно). Теперь можно открыть его в браузере по адресу 127.0.0.1:3000 и попробовать уже готовый сервис в работе.
Впереди нас ждут следующие этапы:
Особая благодарность kavu [9] за коррекцию первой и второй части статьи.
Приступим к разработке.
Загрузим код первого шага:
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 [10], имплементируя метод 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 (подробнее [11] об этом механизме), отдача статики, и последним действием устанавливается обработчик роутера.
Мы будем перехватывать любые 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 [12]. Второй параметр и последующие, принимают функцию, которая будет заниматься обработкой запроса. Так как Martini поддерживает цепочку обработчиков, сюда можно добавлять различные вспомогательные хендлеры, например проверку прав доступа. Нам пока это ни к чему, поэтому добавим только один обработчик c интерфейсом, обрабатываемым обычным веб обработчиком Go (пример разработки на нём можно посмотреть в документации [13]). Вот код нашего обработчика:
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 [14] из пакета httputil [15] мы сохраняем структуру запроса 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
Это была всего лишь проба сил, теперь приступим к написанию настоящего приложения.
Не забываем загрузить код:
git checkout step-2
Размещать код внутри пакета main не очень правильно, так как, например Google Application Engine [16] создаёт свой пакет main, в котором уже подключаются ваши. Поэтому вынесем создание API в отдельный модуль, назовём его, например skimmer/api.go.
Теперь нам нужно создать сущность, в которой мы сможем хранить пойманные запросы, назовём её Bin, по аналогии с requestbin. Моделью у нас будет просто обычная структура данных Go.
Порядок полей в структуре достаточно важен, но мы не будем задумываться об этом, но те кто хотят узнать как порядок влияет на размер структуры в памяти, могут почитать вот эти статьи — www.goinggo.net/2013/07/understanding-type-in-go.html [17] и www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/ [18].
Итак, наша модель Bin будет содержать поля с названием, количеством пойманных запросов, и датами создания и изменения. Каждое поле у нас так же описывается тэгом.
Тэги это обычные строки, которые никак не влияют на программу в целом, но их можно прочитать используя пакет reflection во время работы программы (так называемая интроспекция), и исходя из этого изменять своё поведение (о том как работать тэгами через reflection [19]). В нашем примере, пакет 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 сериализации можно почитать в документации [20]
Так же мы описываем вспомогательную функцию 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. Подробнее в документации [21]. Главное помнить, что смешивать эти два типа инициализации нельзя.
Теперь более подробно про генерацию строк через объект 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 [22], предоставляющий нам доступ к генерации случайных чисел. Самое главное, посеять генератор перед началом работы с ним, чтобы у нас не получилась одинаковая последовательность случайных чисел при каждом запуске.
В методе Generate мы создаём массив байтов, и каждый из байтов заполняем случайным символом из строки pool. Получившуюся в итоге строку возвращаем.
Перейдём, собственно, к описанию Api. Для начала нам нужно три метода для работы с объектами типа Bin, вывода списка объектов, создание и получение конкретного объекта.
Ранее я писал, что martini принимает в обработчик функцию с интерфейсом HandlerFunc, на самом деле, принимаемая функция в Martini описывается как interface{} — то есть это может быть абсолютно любая функция. Каким же образом в эту функцию вставляются аргументы? Делается это при помощи известного паттерна — Dependency injection [23] (далее DI) при помощи небольшого пакета inject [24] от автора martini. Не буду вдаваться в подробности относительно того, как это сделано, вы можете посмотреть в код самостоятельно, благо он не большой и там всё довольно просто. Но если двумя словами, то при помощи уже упомянутого пакета reflect, получаются типы аргументов функции и после этого подставляются нужные объекты этого типа. Например когда inject видит тип *http.Request, он подставляет объект req *http.Request в этот параметр.
Мы можем сами добавлять нужные объекты для рефлексии через методы объекта Map и MapTo глобально, либо через объект контекста запроса martini.Context для каждого запроса отдельно.
Объявим временные переменные history и bins, первый будет содержать историю созданных нами объектов 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)
})
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
Для начала создадим модель, которая будет хранить в себе 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("%sn<<<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 [25], тело запроса — Body это некий буффер, реализующий интерфейс io.ReadCloser [26], по этой причине после разбора формы (вызов метода 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). Подробнее об интерфейсах [27].
Помимо этого создадим базовый объект storage, в котором будут вспомогательные поля, которые потребуются нам в каждой имплементации:
type BaseStorage struct {
maxRequests int
}
Теперь пришло время реализовать поведение нашего интерфейса хранилища. Для начала попробуем всё хранить в памяти, разграничивая параллельный доступ к данным мьютексами [28].
Создадим файл memory.go В основе нашего хранилища будет простая структура данных:
type MemoryStorage struct {
BaseStorage
sync.RWMutex
binRecords map[string]*BinRecord
}
Она состоит из вложенных, анонимных полей BaseStorage и sync.RWMutex.
Анонимные поля дают нам возможность вызывать методы и поля анонимных структур напрямую. Например, если у нас есть переменная obj типа MemoryStorage, мы можем доступиться к полю maxRequests напрямую obj.BaseStorage.maxRequests, либо как будто они члены самого MemoryStorage obj.maxRequests. Подробнее об анонимных полях в структурах данных можно почитать в документации [29].
RWMutex [30] нам нужен, чтобы блокировать одновременную работу со словарём binRecords, так как Go не гарантирует правильного поведения при параллельном изменении данных в словарях.
Сами данные будут хранится в поле binRecords, которой является словарём с ключами из поля name Bin объектов и данными вида BinRecord.
type BinRecord struct {
bin *Bin
requests []*Request
requestMap map[string]*Request
}
В этой структуре собраны все нужные данные. Ссылки на запросы хранятся в двух полях, в списке, где они идут по порядку добавления и в словаре, для более быстрого поиска по идентификатору.
Словари в Go в текущей реализации — это хеш таблицы, поэтому поиск элемента в словаре имеет константное значение. Подробнее о внутреннем устройстве можно ознакомиться в этой прекрасной статье [31].
Так же для объекта 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 можно в документации [11]
Подробнее рассматривать каждый метод 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,...}]
Кажется, всё работает как надо, но чтобы быть в этом точно уверенными нужно покрыть код тестами.
Продолжение статьи во второй части [32], где мы узнаем как писать тесты, реализуем одностраничный веб-интерфейс на основе AngularJS и Bootstrap, добавим немного приватности и внедрим поддержку Redis для хранения.
Автор: M0sTH8
Источник [33]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/56803
Ссылки в тексте:
[1] requestb.in/: http://requestb.in/
[2] Martini: http://martini.codegangsta.io/
[3] на странице проекта: http://golang.org/doc/install
[4] GoSublime: https://github.com/DisposaBoy/GoSublime
[5] go-lang-ide-plugin: http://plugins.jetbrains.com/plugin/5047
[6] skimmer.tulu.la/: http://skimmer.tulu.la/
[7] сайте проекта: http://golang.org/doc/code.html#Workspaces
[8] goenv: https://github.com/pwoolcoc/goenv
[9] kavu: http://habrahabr.ru/users/kavu/
[10] http.Handler: http://golang.org/pkg/net/http/#Handler
[11] подробнее: http://blog.golang.org/defer-panic-and-recover
[12] glob: http://en.wikipedia.org/wiki/Glob_(programming)
[13] в документации: http://golang.org/doc/articles/wiki/
[14] DumpRequest: http://golang.org/pkg/net/http/httputil/#DumpRequest
[15] httputil: http://golang.org/pkg/net/http/httputil/
[16] Google Application Engine: https://developers.google.com/appengine/docs/go/
[17] www.goinggo.net/2013/07/understanding-type-in-go.html: http://www.goinggo.net/2013/07/understanding-type-in-go.html
[18] www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/: http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/
[19] reflection: http://golang.org/pkg/reflect/#StructTag
[20] документации : http://golang.org/pkg/encoding/json/
[21] документации: http://golang.org/ref/spec#The_zero_value
[22] math/rand: http://golang.org/pkg/math/rand
[23] Dependency injection: http://en.wikipedia.org/wiki/Dependency_injection
[24] inject: https://github.com/codegangsta/inject/
[25] http.Request: http://golang.org/pkg/net/http/#Request
[26] io.ReadCloser: http://golang.org/pkg/io/#ReadCloser
[27] интерфейсах: http://golangtutorials.blogspot.com/2011/06/interfaces-in-go.html
[28] мьютексами: http://ru.wikipedia.org/wiki/Мьютекс
[29] документации: http://golangtutorials.blogspot.com/2011/06/anonymous-fields-in-structs-like-object.html
[30] RWMutex: http://golang.org/pkg/sync/#RWMutex
[31] прекрасной статье: http://www.goinggo.net/2013/12/macro-view-of-map-internals-in-go.html
[32] второй части: http://habrahabr.ru/post/214425/
[33] Источник: http://habrahabr.ru/post/208680/
Нажмите здесь для печати.