- PVSM.RU - https://www.pvsm.ru -

Пишем веб сервис на Go (часть первая)

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

Часто, при разработке сервисов, нужно понимать какие данные отправляются в другой сервис, а возможность перехватить траффик есть не всегда. И как раз для того, чтобы отлавливать подобные запросы, существует проект requestb.in/ [1], позволяющий собирать запросы по определённому урлу и отображать их в веб-интерфейсе. Написанием подобного же приложения мы и займёмся. Чтобы немного упростить себе задачу, возьмём за основу какой-нибудь фреймворк, например Martini [2].

В конечном итоге, у нас должен будет получится вот такой вот сервис:

Пишем веб сервис на Go (часть первая)

Подготовка

Эта статья будет разделена на шаги, каждый из которых будет содержать код, хранящийся в отдельной ветке репозитория на 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 и попробовать уже готовый сервис в работе.

Впереди нас ждут следующие этапы:

  1. Шаг первый. Знакомство с Martini;
  2. Шаг второй. Создаём модель Bin и отвечаем на запросы;
  3. Шаг третий. Принимаем запросы и сохраняем их в хранилище;
  4. Шаг четвёртый. А как же тесты?
  5. Шаг пятый— украшательства и веб-интерфейс;
  6. Шаг шестой. Добавляем немного приватности;
  7. Шаг седьмой. Очищаем ненужное;
  8. Шаг восьмой. Используем Redis для хранения.

Особая благодарность kavu [9] за коррекцию первой и второй части статьи.

Приступим к разработке.

Шаг первый. Знакомство с 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 [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

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

Шаг второй. Создаём модель Bin и отвечаем на запросы.

Не забываем загрузить код:

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.
Теперь рассмотрим созданные методы.

Создание объекта 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 мы можем обратиться к элементу словаря двумя способами:

  1. Запросив значение ключа a := m[key], в этом случае вернётся либо значение ключа в словаре, если оно есть, либо дефолтное значение инициализации типа значения. Таким образом, например для чисел, сложно понять, содержит ли ключ 0 или просто значения этого ключа не существует. Поэтому в го предусмотрен второй вариант.
  2. В этом способе, запросив по ключу и получить его значение первым параметром и индикатор существования этого ключа вторым параметром — 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("%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 смысла нет, там всё и так не сложно, вы можете заглянуть в код самостоятельно.

Код 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/