Pull to refresh

Роутер на Golang

Reading time 8 min
Views 28K
image Добро пожаловать, или Посторонним вход воспрещён
(С) Э.Г.Климов 1964


Написанный на языке Go роутер (или как его ещё иногда называют — маршрутизатор), который оказался достаточно быстрым для того, чтобы его не стыдно было сравнить с лидерами go-роутинга: Bone, Httprouter, Gorilla, Zeus. Название роутеру дало простое русское слово «Вход», набранное английскими буквами в кодировке волапюк en.wikipedia.org/wiki/Volapuk_encoding


Никогда со мной такого не бывало: нет входа! Куда ни ткнись — везде сплошные окна.
(С) Аркадий и Борис Стругацкие. Хромая судьба


Bxog (английские буквы), читается как Бииксоуджи. В комментариях предлагаю называть роутер на выбор: Вход, Bxog, Бииксоуджи, Биксодж. Для модуля в приложении, которое первым встречает запрос пользователя, название, с моей конечно точки зрения, вполне подходящее. По аналогии с Windows хотелось бы назвать свой роутер Door, но боюсь быть неверно понятым ;)

Ссылки на исходники:
github.com/claygod/Bxog
github.com/claygod/BxogTest

Зачем

И нафига мне эти перья?
(С) Из анекдота


Скажу сразу, во всём виноват кризис: зарплаты не хватает, с Адсенса на старых сайтах ничего уже не капает, а жить надо. Нужен дополнительный заработок, а на чём его делать? На пыхе надоело, а на другом ничего не умею, ну не идти же в самом деле, в верстальщики… Фронтэнд, это как-то не моё.

Изначально я приглядывался к С/С++, притом скорее склонялся к Си (тут каждому своё). Но учитывая указатели, указатели на указатели, указатели на функции и указатели на функции, которые принимают на вход указатели на функции, мой неокрепший мозг потребовал вернуться от безалкогольного пива к классическому, чему как-то воспротивился организм (единство и борьба противоположностей).

После приятного и интересного Lua под руку попался новый (для меня) Golang: простой, быстро осваеваемый, похожий на Си. Поставил LiteIDE, и можно работать. Важный плюс — на рабочий компьютер под виндой всё можно поставить даже с правами пользователя (ну строгие корпоративные правила у меня на работе, это факт).

Осталось определиться с тем, что писать в процессе изучения языка (я считаю, что по другому язык нормально никак не изучить). Фреймворк, это как-то многовато… А если изучать даже всего по одному языку в год, это ж сколько фреймворков придётся наклепать? Боюсь, планета такого не выдержит, я ведь на ней не один :) Решил написать роутер, и на этом, пожалуй, лирическое отступление закончу.

Разработка

Поскольку языка Go на момент начало разработки я ещё не знал, то и структура роутера, и принципы его хранения и поиска маршрутов менялись несколько раз (кардинально, раза четыре). Первая итерация была совершенно ознакомительной. Посмотрев на неё (со слезами, а как же), я уяснил основы Golang и сделал вторую версию, в которой даже на код глядеть было не слишком страшно. Однако даже простое ab тестирование показало мне, что моя поделка довольно тормознутая.

Поменяв алгоритмы и внеся ещё некоторые правки, я решил ознакомиться с другими Go-роутерами. Из списка мне понравился Bone, не удивлюсь, если во многом из-за названия и прикольной картинки. Посмотрев на этот роутер я подумал, что эх, мне бы так смочь… Количество звёзд под тысячу у этого роутера тоже как-то воодушевляли.

У Bone есть бенч, который толкнул-таки меня на третью итерацию разработки своего творения. Написав себе похожий бенч, я опять поменял структуру хранения и алгоритм поиска. Теперь, о чудо, мой Биксоджей смог обогнать Gorilla Pat (Gorilla Mux более медленный, и с ним тягаться не сложно). Одновременно я понял, что любимец Bone совсем не заточен под большие динамические урлы, хотя неплохо справляется с короткими статическими, и его бенч как раз под это и написан.

Король умер, да здравствует король! Теперь я буду сражаться с HttpRouter. По ходу пьесы я уяснил, что мапы в Go мягко говоря не блещут скоростью по сравнению с массивами. Очередной виток разработки начал выводить роутер на финишную прямую, где бенч лидера стал близок, можно сказать соседским. Отстранёно взглянув на свой код, я решил, что он слишком многословен, и кроме того, требует тонкой настройки.

Отшлифовав роутер на столько, на сколько хватило терпения и мозгов, я перешёл к написанию тестов (каюсь, до этого просто тестировал вручную и эпизодически). Тесты показали, что и терпения и мозгов у меня не так уж и много, а заодно вскрыли пару крупных и неявных ошибок, которые пришлось устранять.

Поскольку strings.Split оказалась тоже не из быстрых, при колеблющемся свете свечи пришлось написать аналог, удовлетворяющий моим требованиям. Да скажу честно, иногда писать быстрый код означает писать не очень красивый и хардкорный код. Лес рубят — щепки летят. В конечном итоге я смог в бенчах добиться превосходства над королём. Это немного польстило моему самолюбию, и хотя и не ставилось явной целью.

Быстрый старт

В принципе, я постарался организовать код так, чтобы работа роутера была проста и понятна, т. е. надеюсь, код говорит сам за себя. Ну а ниже даю комплексный пример использования роутера, демонстрирующий все его методы
комплексный пример использования роутера
package main

import (
	"io"
	"net/http"
	"github.com/claygod/Bxog"
)

// Handlers
func IHandler(w http.ResponseWriter, req *http.Request, r *bxog.Router) {
	io.WriteString(w, "Welcome to Bxog!")
}
func THandler(w http.ResponseWriter, req *http.Request, r *bxog.Router) {
	params := r.Params(req, "/abc/:par")
	io.WriteString(w, "Params:\n")
	io.WriteString(w, " 'par' -> "+params["par"]+"\n")
}
func PHandler(w http.ResponseWriter, req *http.Request, r *bxog.Router) {
	// Получить параметры из урла
	params := r.Params(req, "country")
	io.WriteString(w, "Country:\n")
	io.WriteString(w, " 'name' -> "+params["name"]+"\n")
	io.WriteString(w, " 'capital' -> "+params["city"]+"\n")
	io.WriteString(w, " 'valuta' -> "+params["money"]+"\n")
	// Сгенерировать строку урла
	io.WriteString(w, "Creating a URL from route:\n")
	io.WriteString(w, r.Create("country", map[string]string{"name": "Russia", "capital": "Moscow", "money": "rouble"}))
}

// Main
func main() {
	m := bxog.New()
	m.Add("/", IHandler)
	m.Add("/abc/:par", THandler)
	m.Add("/country/:name/capital/:city/valuta/:money", PHandler).
		Id("country"). // Для удобства короткий ID
		Method("GET") // Тут GET можно было не указывать, это для примера
	m.Start(":80")
}



После запуска приведённого примера перейдите по ссылкам:


Конфигурирование

Настраивать роутер нужно через редактирование файла конфигурации github.com/claygod/bxog/config.go В файле обозначены константы, которые можно изменять.

HTTP_METHOD_DEFAULT
Метод, используемый роутером по умолчанию (GET, POST etc.). Эта опция позволяет писать меньше кода и по сути просто сахар.

HTTP_SECTION_COUNT
Максимальное количество секций в урле. Под секцией подразумевается набор символов между двух слэшей. Устанавливать эту цифру вплотную к количеству секций в самом большом роуте не обязательно. Если 32 маловато, то пишите 64, чтобы не заморачиваться, ну и т.д.

HTTP_PATTERN_COUNT
Максимальная длина урла. Тут понятно без объяснений.

READ_TIME_OUT
Максимальное время ожидания при чтении

WRITE_TIME_OUT
Максимальное время ожидания при записи

FILE_PREF
Локальный путь от корня сайта в урле для файлов, доступных для скачивания.

FILE_PATH
Полный путь к каталогу, из которого файлы будут доступны для скачивания.

Бенчмарк

Роутер получился достаточно быстрым для того, чтобы было не стыдно испытать его в бенчмарке совместно с такими популярными роутерами, написанными на Go, как Bone, Httprouter, Gorilla, Zeus.Код бенчмарка и тесты роутера лежит тут — github.com/claygod/bxogtest

Результат бенчмарка для указанных роутеров в сравнении с Bxog со 150 сгенерированными 6-секционными роутами (конфигурацию компьютера не указываю, ибо тут скорее важны не абсолютные цифры, а их сравнение):

  • BenchmarkBxogMux-4 5000000 330 ns/op
  • BenchmarkHttpRouterMux-4 3000000 395 ns/op
  • BenchmarkZeusMux-4 100000 23772 ns/op
  • BenchmarkGorillaMux-4 50000 30223 ns/op
  • BenchmarkGorillaPatMux-4 1000000 1253 ns/op
  • BenchmarkBoneMux2-4 20000 63656 ns/op

Очень порадовал Gorilla Pat, который демонстрирует хорошую, хотя и не лучшую производительность. Если вас устраивают программы от Gorilla, то для поиска быстрого решения Gorilla Pat очень хорошее решение.
Если же говорить о выборе роутера вообще, то тут немаловажным будет то, насколько он подходит вам изнутри (это моё мнение, кто-то считает, что кроме API ничего глядеть и не надо). Но то, что есть выбор, это уже хорошо. На картинке ниже видно, что у тройки лидеров в зависимости от количества маршрутов скорость сильно не меняется.

image

Тесты

Возможно, тестов маловато, или какой-то момент ими охвачен недостаточно, буду рад получить подсказки в комментариях. В общем, DISCLAIMER, в косяках, которые наверняка есть, не вините меня, я тут просто, мимо проходил.

API

API у роутера простое, как апельсин. В качестве небольшой сладости (если апельсин кислый), добавлена возможность не только обращаться к параметрам из урла в хэндлере, но и генерировать новый урл. Это может оказаться удобным при разработке сайта на Go (или фреймворка под такие задачи).

Методы:
  • New — создать мультиплексор
  • Add — добавить роут. По умолчанию метод GET, и ID роута в виде строки его правила, соответственно, это не обязательные опции. Пример с указанием метода и ID: m.Add("/abc/:param", YourHandler).Method(«POST»).Id(«abc»)
  • Start — запустить сервер с указанием порта, который нужно слушать
  • Params — получить из URL параметры
  • Create — создать URL из параметров роута
  • Test — аналог Start (только для тестирования)

Простой пример:
	m := bxog.New()
	m.Add("/", IndexHandler)
	m.Start(":80")


Секции

Как уже упоминал выше, урл слэшами делится на секции. Секция может быть статическая (неизменяемая) и динамическая (аргумент). Динамическая секция начинается с двоеточия (это общеупотребительная практика роутеров). Последняя секция не должна заканчиваться на слэш.

Статические файлы

С помощью констант FILE_PREF и FILE_PATH необходимо указать роутеру, по какому пути будут доступны статические файлы в браузере и какой путь к этим файлам на жёстком диске.

Golang

Споры о Go не утихают. Одни его критикуют, другие хвалят. И то, и другое, возможно, обосновано. Мне понравилось принудительное форматирование кода: чтение чужих исходников теперь не вызывает никаких проблем, всё однородно и читаемо. Иногда мешают напоминания о созданных и не использованных переменных, но в конечном итоге, просто меняешь свой стиль программирования, чтобы компиляция сразу прошла успешно. Буду ли я в дальнейшем использовать этот язык? Да. Посоветую ли его другим? Да. Нужно ли его делать главным и единственным языком в своей работе? Нет, и не потому, что он плохой, просто свой кругозор надо расширять.

Подытоживая

Вот все пишут «Запасной выход». И хоть бы одна зараза написала «запасной вход».
(С) Дмитрий Емец. Мефодий Буслаев. Месть валькирий


Начиная писать свой Биксодж, я не задавался целью утереть нос лидеру go-роутеров *Httprouter* в скорости обработки запроса. То, что мой код оказался быстрым, это скорее приятный бонус. На мой взгляд, у приложения, использующего роутер, этот самый роутер вряд-ли будет узким горлышком. Но если задаваться целью написать действительно быстрое приложение, то… другое дело. Скорей всего это будет маленькое приложение, сервис или ещё что-то в таком духе. Для больших и сложных приложений скорость перестаёт быть единственным и самым важным критерием.

А как же работа?

Работу я не нашёл, поскольку ещё не искал. Очень бы хотелось в комментариях прочитать про реальные ситуации рынка Go-программистов, а то меня тут разобрали сомнения, может сразу покупать новый учебник по языку ХХХ :)

github.com/claygod/Bxog
github.com/claygod/BxogTest

UPD: 17/01/2017


Сделал клон роутера под Fast HTTP server:
github.com/claygod/door

UPD: 28/02/2017


Недавно обратил внимание на то, что скорость работы главного соперника HttpRouter была немного увеличена его автором. Также, к моему прискорбию, коммиты, сделавшие работу с параметрами командной строки через нативный Context существенно замедлили мой любимый роутер. Соответственно пришлось потратить пару вечеров и увеличить перфоманс своего детища. На сегодня бенчмарк для сотни шестисекционных роутов выглядит так:

  • BenchmarkBxogMux-4 5000000 277 ns/op
  • BenchmarkHttpRouterMux-4 3000000 307 ns/op
  • BenchmarkZeusMux-4 100000 18631 ns/op
  • BenchmarkGorillaMux-4 50000 25302 ns/op
  • BenchmarkGorillaPatMux-4 1000000 1253 ns/op
  • BenchmarkBoneMux2-4 20000 63656 ns/op

Для GorillaPat и Bone ситуация не изменилась, поэтому я оставил старые цифры. Также отмечу, что для роутов с меньшим количеством секций ситуация ещё лучше, но раз уж решил мерить в шестисекционных, то так тому и быть.
Tags:
Hubs:
+16
Comments 28
Comments Comments 28

Articles