Skip to content

Instantly share code, notes, and snippets.

@m0sth8
Created March 17, 2014 01:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m0sth8/9592678 to your computer and use it in GitHub Desktop.
Save m0sth8/9592678 to your computer and use it in GitHub Desktop.
How to build web application on Go (Part 1)
В этой статье, я хотел бы рассказать вам, как можно достаточно быстро и легко написать небольшое веб-приложение на языке Go, который, не смотря на юный возраст, успел завоевать расположение у многих разработчиков. Обычно, для подобных статей пишут искусственные приложения, вроде TODO листа. Мы же попробуем написать что-то полезное, что уже существует и используется.
Часто, при разработке сервисов, нужно понимать какие данные отправляются в другой сервис, а возможность перехватить траффик есть не всегда. И как раз для того, чтобы отлавливать подобные запросы, существует проект http://requestb.in/, позволяющий собирать запросы по определённому урлу и отображать их в веб-интерфейсе. Написанием подобного же приложения мы и займёмся. Чтобы немного упростить себе задачу, возьмём за основу какой-нибудь фреймворк, например <a href="http://martini.codegangsta.io/">Martini</a>.
В конечном итоге, у нас должен будет получится вот такой вот сервис:
<img align="left" src="http://habrastorage.org/getpro/habr/post_images/a18/13a/db9/a1813adb9fcbbd5f30549ad4d0429b8f.png"/>
<habracut text="Приступим к разработке" />
<h4>Подготовка</h4>
Эта статья будет разделена на шаги, каждый из которых будет содержать код, хранящийся в отдельной ветке репозитория на GitHub. Вы всегда сможете запустить и посмотреть результаты, а так же поиграться с кодом.
Для запуска приложения нужно иметь на своей машине компилятор Go. Я исхожу из предположения, что он у вас уже есть и настроен так, как вам удобно. Если же нет, то узнать как это сделать вы можете <a href="http://golang.org/doc/install">на странице проекта</a>.
В качестве среды для разработки, вы можете использовать то, что вам удобнее, благо, плагины для Go есть почти под каждый редактор. Наиболее популярнен <a href="https://github.com/DisposaBoy/GoSublime">GoSublime</a>. Но я бы посоветовал IntelijIdea + <a href="http://plugins.jetbrains.com/plugin/5047">go-lang-ide-plugin</a>, который последнее время очень активно развивается, например из последнего добавленного - дебаг приложения.
Попробовать уже готовый сервис в работе можно по ссылке http://skimmer.tulu.la/.
Для начала работы нужно склонировать репозиторий к себе на машину в какую-нибудь директорию, например так:
<source lang="bash">
git clone https://github.com/m0sth8/skimmer ./skimmer
</source>
Вы можете добавить проект в своё рабочее окружение (подробнее об этом можно прочитать на <a href="http://golang.org/doc/code.html#Workspaces">сайте проекта</a>) , либо организовывать код, как вам удобно. Я же для простоты изложения, использую <a href="https://github.com/pwoolcoc/goenv">goenv</a>, позволяющий указывать версии компилятора go и создавать чистое рабочее окружение в директории проекта.
Теперь нам нужно зайти в склонированную директорию skimmer и установить нужные зависимости командой:
<source lang="bash">
go get -d ./src/
</source>
После завершения установки зависимости, можно запустить проект:
<source lang="bash">
go run ./src/main.go
</source>
У вас должен запуститься веб-сервис на порту 3000 (порт и хост можно указать через переменные окружения PORT и HOST соответственно). Теперь можно открыть его в браузере по адресу 127.0.0.1:3000 и попробовать уже готовый сервис в работе.
Впереди нас ждут следующие этапы:
<ol>
<li>Шаг первый. Знакомство с Martini;</li>
<li>Шаг второй. Создаём модель Bin и отвечаем на запросы;</li>
<li>Шаг третий. Принимаем запросы и сохраняем их в хранилище;</li>
<li>Шаг четвёртый. А как же тесты?</li>
<li>Шаг пятый— украшательства и веб-интерфейс;</li>
<li>Шаг шестой. Добавляем немного приватности;</li>
<li>Шаг седьмой. Очищаем ненужное;</li>
<li>Шаг восьмой. Используем Redis для хранения.</li>
</ol>
Особая благодарность <hh user="kavu"/> за коррекцию первой и второй части статьи.
Приступим к разработке.
<h4>Шаг первый. Знакомство с Martini.</h4>
Загрузим код первого шага:
<source code="bash">
git checkout step-1
</source>
Для начала попробуем просто вывести запрос, приходящий к нам. Точка входа в любое приложение на Go, это функция main пакета main. Создадим в директории src файл main.go. В Martini уже есть заготовка приложения, добавляющая логи, обработку ошибок, возможность восстановления и роутер; и дабы не повторяться, мы воспользуемся ей.
Сам по себе Martini достаточно прост:
<source lang="go">
// 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
}
</source>
Он реализует интерфейс <a href="http://golang.org/pkg/net/http/#Handler">http.Handler</a>, имплементируя метод ServeHTTP. Далее все приходящие запросы пропускаются через различные обработчики, хранящиеся в handlers и в конце выполняет Handler action.
Классический Martini:
<source lang="go">
// 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}
}
</source>
В этом конструкторе создаётся объект типа Martini и Router, в обработчики handler через метод martini.Use добавляется логирование запросов, перехват panic (<a href="http://blog.golang.org/defer-panic-and-recover">подробнее</a> об этом механизме), отдача статики, и последним действием устанавливается обработчик роутера.
Мы будем перехватывать любые HTTP запросы к нашему приложению, используя метод <code>Any</code> у роутера, перехватывающий любые урлы и методы. Интерфейс роутера описан в Martini вот так:
<source lang="go">
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)
}
</source>
Если очень хочется - можно реализовать свою имплементацию обработчика адресов, но мы воспользуемся той, что идет в Martini по умолчанию.
Первым параметром указывается локейшен. Локейшены в Martini поддерживают параметры через <code>":param"</code>, регулярные выражения, а так же <a href="http://en.wikipedia.org/wiki/Glob_(programming)">glob</a>. Второй параметр и последующие, принимают функцию, которая будет заниматься обработкой запроса. Так как Martini поддерживает цепочку обработчиков, сюда можно добавлять различные вспомогательные хендлеры, например проверку прав доступа. Нам пока это ни к чему, поэтому добавим только один обработчик c интерфейсом, обрабатываемым обычным веб обработчиком Go (пример разработки на нём можно посмотреть<a href="http://golang.org/doc/articles/wiki/"> в документации</a>). Вот код нашего обработчика:
<source lang="go">
func main() {
api := martini.Classic()
api.Any("/", func(res http.ResponseWriter, req *http.Request,) {
if dumped, err := httputil.DumpRequest(req, true); err == nil {
res.WriteHeader(200)
res.Write(dumped)
} else {
res.WriteHeader(500)
fmt.Fprintf(res, "Error: %v", err)
}
})
api.Run()
}
</source>
Используя готовую функцию <a href="http://golang.org/pkg/net/http/httputil/#DumpRequest">DumpRequest</a> из пакета <a href="http://golang.org/pkg/net/http/httputil/">httputil</a> мы сохраняем структуру запроса http.Request, и записываем его в ответ http.ResponseWriter. Так же не забываем обрабатывать возможные ошибки. Функция api.Run просто запускает встроенный сервер go из стандартной библиотеки, указывая порт и хост, которые она берёт из параметров окружения PORT(3000 по умолчанию) и HOST.
Запустим наше первое приложение:
<source lang="bash">
go run ./src/main.go
</source>
Попробуем отправить запрос к серверу:
<source lang="bash">
> 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
</source>
Это была всего лишь проба сил, теперь приступим к написанию настоящего приложения.
<h4>Шаг второй. Создаём модель Bin и отвечаем на запросы.</h4>
Не забываем загрузить код:
<source code="bash">
git checkout step-2
</source>
Размещать код внутри пакета main не очень правильно, так как, например <a href="https://developers.google.com/appengine/docs/go/">Google Application Engine</a> создаёт свой пакет main, в котором уже подключаются ваши. Поэтому вынесем создание API в отдельный модуль, назовём его, например skimmer/api.go.
Теперь нам нужно создать сущность, в которой мы сможем хранить пойманные запросы, назовём её Bin, по аналогии с requestbin. Моделью у нас будет просто обычная структура данных Go. <blockquote>Порядок полей в структуре достаточно важен, но мы не будем задумываться об этом, но те кто хотят узнать как порядок влияет на размер структуры в памяти, могут почитать вот эти статьи - http://www.goinggo.net/2013/07/understanding-type-in-go.html и http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/.</blockquote>
Итак, наша модель Bin будет содержать поля с названием, количеством пойманных запросов, и датами создания и изменения. Каждое поле у нас так же описывается тэгом.
<blockquote>Тэги это обычные строки, которые никак не влияют на программу в целом, но их можно прочитать используя пакет reflection во время работы программы (так называемая интроспекция), и исходя из этого изменять своё поведение (о том как работать тэгами через <a href="http://golang.org/pkg/reflect/#StructTag">reflection</a>). В нашем примере, пакет json при кодировании/раскодировании учитывает значение тэга, примерно так:
<source lang="go">
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"))
}
</source>
Выведет
<code>Field's 'Name' json name is 'name' </code>
Пакет encoding/json поддерживает различные опции при формировании тэгов:
<source lang="go">
// Поле игнорируется
Field int `json:"-"`
// В json структуре поле интерпретируется как myName
Field int `json:"myName"`
</source>
Вторым параметром может быть например, опция omitempty - если значение в json пропущено, то поле не заполняется. Так например, если поле будет ссылкой, мы сможем узнать, присутствует ли оно в json объекте, сравнив его с nil. Более подробно о json сериализации можно почитать в <a href="http://golang.org/pkg/encoding/json/ ">документации </a></blockquote>
Так же мы описываем вспомогательную функцию NewBin, в которой происходит инициализация значений объекта Bin (своего рода конструктор):
<source lang="go">
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
}
</source>
<blockquote>Структуры в Go могут иницилизироваться двумя способами:
1) Обязательным перечислением всех полей по порядку:
<source lang="go">
Bin{rs.Generate(6), now, now, 0}
</source>
2) Указанием полей, для которых присваиваются значения:
<source lang="go">
Bin{
Created: now,
Updated: now,
Name: rs.Generate(6),
}
</source>
Поля, которые не указаны, принимают значения по умолчанию. Например для целых чисел это будет 0, для строк - пустая строка "", для ссылок, каналов, массивов, слайсов и словарей - это будет nil. Подробнее в <a href="http://golang.org/ref/spec#The_zero_value">документации</a>. Главное помнить, что смешивать эти два типа инициализации нельзя.</blockquote>
Теперь более подробно про генерацию строк через объект rs. Он инициализирован следующим образом:
<source lang="go">
var rs = NewRandomString("0123456789abcdefghijklmnopqrstuvwxyz")
</source>
Сам код находится в файле utils.go. В функцию мы передаём массив символов, из которых нужно генерировать строчку и создаём объект RandomString:
<source lang="go">
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
}
</source>
Здесь мы используем пакет <a href="http://golang.org/pkg/math/rand">math/rand</a>, предоставляющий нам доступ к генерации случайных чисел. Самое главное, посеять генератор перед началом работы с ним, чтобы у нас не получилась одинаковая последовательность случайных чисел при каждом запуске.
В методе Generate мы создаём массив байтов, и каждый из байтов заполняем случайным символом из строки pool. Получившуюся в итоге строку возвращаем.
Перейдём, собственно, к описанию Api. Для начала нам нужно три метода для работы с объектами типа Bin, вывода списка объектов, создание и получение конкретного объекта.
Ранее я писал, что martini принимает в обработчик функцию с интерфейсом HandlerFunc, на самом деле, принимаемая функция в Martini описывается как interface{} - то есть это может быть абсолютно любая функция. Каким же образом в эту функцию вставляются аргументы? Делается это при помощи известного паттерна - <a href="http://en.wikipedia.org/wiki/Dependency_injection">Dependency injection</a> (далее DI) при помощи небольшого пакета <a href="https://github.com/codegangsta/inject/ ">inject</a> от автора martini. Не буду вдаваться в подробности относительно того, как это сделано, вы можете посмотреть в код самостоятельно, благо он не большой и там всё довольно просто. Но если двумя словами, то при помощи уже упомянутого пакета reflect, получаются типы аргументов функции и после этого подставляются нужные объекты этого типа. Например когда inject видит тип *http.Request, он подставляет объект req *http.Request в этот параметр.
Мы можем сами добавлять нужные объекты для рефлексии через методы объекта Map и MapTo глобально, либо через объект контекста запроса martini.Context для каждого запроса отдельно.
Объявим временные переменные history и bins, первый будет содержать историю созданных нами объектов Bin, а второй будет некой куцей версией хранилища объектов Bin.
Теперь рассмотрим созданные методы.
<h5>Создание объекта Bin</h5>
<source lang="go">
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)
})
</source>
<h5>Получение списка объектов Bin</h5>
<source lang="go">
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)
})
</source>
<h5>Получение конкретного экземпляра</h5>
<source lang="go">
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)
}
})
</source>
Метод позволяющий получить объект Bin по его имени, в нём мы используем объект martini.Params (по сути просто map[string]string), через который можем доступиться к разобранным параметрам адреса.
<blockquote>В языке Go мы можем обратиться к элементу словаря двумя способами:
<ol>
<li>Запросив значение ключа <code>a := m[key]</code>, в этом случае вернётся либо значение ключа в словаре, если оно есть, либо дефолтное значение инициализации типа значения. Таким образом, например для чисел, сложно понять, содержит ли ключ 0 или просто значения этого ключа не существует. Поэтому в го предусмотрен второй вариант.</li>
<li>В этом способе, запросив по ключу и получить его значение первым параметром и индикатор существования этого ключа вторым параметром — <code>a, ok := m[key]</code></li>
</ol>
</blockquote>
Поэкспериментируем с нашим приложением. Для начала запустим его:
<source code="bash">
go run ./src/main.go
</source>
Добавим новый объект Bin:
<source code="bash">
> 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}
</source>
Получим список доступных нам Bin объектов:
<source code="bash">
> 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}]
</source>
Запросим конкретный объект Bin, взяв значение name из предыдущего запроса:
<source code="bash">
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}
</source>
Отлично, теперь мы научились создавать модели и отвечать на запросы, кажется теперь нас ничего не удержит от того, чтобы доделать всё остальное.
<h4>Шаг третий. Принимаем запросы и сохраняем их в хранилище.</h4>
Теперь нам нужно научиться сохранять запросы, приходящие к нам, в нужный объект Bin.
Загрузим код для третьего шага
<source code="bash">
git checkout step-3
</source>
<h5>Модель Request</h5>
Для начала создадим модель, которая будет хранить в себе HTTP запрос.
<source code="go">
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"`
}
</source>
Объяснять какое поле для чего нужно, полагаю смысла нет, но есть пара замечаний: для файлов мы будем хранить только их названия, а для данных формы — будем хранить уже готовый словарь значений.
По аналогии с созданием объекта Bin, напишем функцию создающую объект Request из HTTP запроса:
<source code="go">
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
}
</source>
Функция получилась достаточно большой, но в целом, понятной, поясню только некоторые моменты. В объекте <a href="http://golang.org/pkg/net/http/#Request">http.Request</a>, тело запроса - Body это некий буффер, реализующий интерфейс <a href="http://golang.org/pkg/io/#ReadCloser">io.ReadCloser</a>, по этой причине после разбора формы (вызов метода ParseMultipartForm), мы уже никак не сможем получить сырые данные запроса. Поэтому для начала мы копируем Body в отдельную переменную и после заменим исходный буфер своим. Далее мы вызываем разбор входящих данных и собираем информацию о значениях форм и файлов.
Помимо объектов Bin, теперь нам нужно так же хранить и запросы, поэтому, пришло время добавить в наш проект возможность хранения данных. Опишем его интерфейс в файле storage.go:
<source code="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
}
</source>
<blockquote>Интерфейсы в Go являются контрактом, связывающим ожидаемую функциональность и актуальную реализацию. В нашем случае, мы описали интерфейс storage, который будем использовать в дальнейшем в программе, но в зависимости от настроек, имплементация может быть совершенно разной (например это может быть Redis или Mongo). Подробнее об <a href="http://golangtutorials.blogspot.com/2011/06/interfaces-in-go.html ">интерфейсах</a>.</blockquote>
Помимо этого создадим базовый объект storage, в котором будут вспомогательные поля, которые потребуются нам в каждой имплементации:
<source code="go">
type BaseStorage struct {
maxRequests int
}
</source>
Теперь пришло время реализовать поведение нашего интерфейса хранилища. Для начала попробуем всё хранить в памяти, разграничивая параллельный доступ к данным <a href="http://ru.wikipedia.org/wiki/Мьютекс">мьютексами</a>.
Создадим файл memory.go В основе нашего хранилища будет простая структура данных:
<source code="go">
type MemoryStorage struct {
BaseStorage
sync.RWMutex
binRecords map[string]*BinRecord
}
</source>
Она состоит из вложенных, анонимных полей BaseStorage и sync.RWMutex. <blockquote>Анонимные поля дают нам возможность вызывать методы и поля анонимных структур напрямую. Например, если у нас есть переменная obj типа MemoryStorage, мы можем доступиться к полю maxRequests напрямую obj.BaseStorage.maxRequests, либо как будто они члены самого MemoryStorage obj.maxRequests. Подробнее об анонимных полях в структурах данных можно почитать в <a href="http://golangtutorials.blogspot.com/2011/06/anonymous-fields-in-structs-like-object.html">документации</a>.</blockquote>
<a href="http://golang.org/pkg/sync/#RWMutex">RWMutex</a> нам нужен, чтобы блокировать одновременную работу со словарём binRecords, так как Go не гарантирует правильного поведения при параллельном изменении данных в словарях.
Сами данные будут хранится в поле binRecords, которой является словарём с ключами из поля name Bin объектов и данными вида BinRecord.
<source code="go">
type BinRecord struct {
bin *Bin
requests []*Request
requestMap map[string]*Request
}
</source>
В этой структуре собраны все нужные данные. Ссылки на запросы хранятся в двух полях, в списке, где они идут по порядку добавления и в словаре, для более быстрого поиска по идентификатору.
<blockquote>Словари в Go в текущей реализации - это хеш таблицы, поэтому поиск элемента в словаре имеет константное значение. Подробнее о внутреннем устройстве можно ознакомиться в этой <a href="http://www.goinggo.net/2013/12/macro-view-of-map-internals-in-go.html">прекрасной статье</a>.</blockquote>
Так же для объекта BinRecord реализован метод для обрезания лишних запросов, который просто удаляет ненужные элементы из requests и requestMap.
<source code="go">
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
}
}
</source>
Все методы MemoryStorage имплементируют поведение интерфейса Storage, так же у нас есть вспомогательный метод getBinRecord, в котором мы можем прочитать нужную нам запись. В момент когда мы читаем запись, мы ставим блокировку на чтение и сразу же указываем отложенный вызов снятия блокировки в defer. Выражение defer позволяет нам указывать функцию, которая будет всегда выполнена по завершении работы функции, даже если функцию была прервана паникой. Подробнее почитать о defer можно в <a href="http://blog.golang.org/defer-panic-and-recover">документации</a>
Подробнее рассматривать каждый метод MemoryStorage смысла нет, там всё и так не сложно, вы можете заглянуть в код самостоятельно.
<spoiler title="Код MemoryStorage">
<source code="go">
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
}
}
</source>
</spoiler>
Теперь, когда у нас есть хранилище, можно приступать к описанию api. Посмотрим что у нас изменяется.
Во первых мы добавляем поддержку нашего нового хранилища.
<source code="go">
memoryStorage := NewMemoryStorage(MAX_REQUEST_COUNT)
api.MapTo(memoryStorage, (*Storage)(nil))
</source>
Теперь в любом хендлере мы можем добавить параметр типа Storage и получить доступ к нашему хранилищу. Что мы и делаем, заменив во всех обработчиках запросов к Bin работу со словарём на вызовы к Storage.
<source code="go">
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()})
}
})
</source>
Во вторых, добавили обработчики для объектов типа Request.
<source code="go">
// список всех реквестов
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)
}
})
</source>
Попробуем запустить то, что у нас получилось и отправить несколько запросов.
Создадим контейнер Bin для наших HTTP запросов
<source code="bash">
> 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}
</source>
Отправим запрос в наш контейнер
<source code="bash">
> curl -X POST -d "fizz=buzz" http://127.0.0.1:3000/bins/ws87ui
{"id":"i0aigrrc1b40","created":1393849284,...}
</source>
Проверим, сохранился ли наш запрос:
<source code="bash">
> curl http://127.0.0.1:3000/api/v1/bins/ws87ui/requests/
[{"id":"i0aigrrc1b40","created":1393849284,...}]
</source>
Кажется, всё работает как надо, но чтобы быть в этом точно уверенными нужно покрыть код тестами.
Продолжение статьи во <a href="http://habrahabr.ru/post/214425/">второй части</a>, где мы узнаем как писать тесты, реализуем одностраничный веб-интерфейс на основе AngularJS и Bootstrap, добавим немного приватности и внедрим поддержку Redis для хранения.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment