У некоторых читателей могут возникнуть вопросы о «велосипедостроении» — это всё плоды любопытства и живого интереса при ознакомлении с языком Golang.
Администрирование системы и разработка проекта
Лишь мельком обозначу этот пункт, чтобы по кусочкам иметь представление о единой системе. В конечном счете CI-сервер собирает проект из git-репозитория и формирует полноценный rpm-пакет для нужной архитектуры, который устанавливается в систему как systemd-сервис.
[Unit]
Description=Description
After=network.target
Requires=mysqld.service
[Service]
Type=simple
User=nginx
Group=nginx
WorkingDirectory=/usr/share/project_name
StandardOutput=journal
StandardError=journal
ExecStart=/usr/share/project_name/project_name
Restart=always
[Install]
WantedBy=multi-user.target
Системный менеджер systemd занимается:
- Установлением зависимостей запуска веб-сервиса (как в вышеуказанном примере от mysqld);
- Respawn-ом на случай падения приложения;
- Благодаря опциям StandardOutput и StandardError, логированием службы. Чтобы из приложения писать в системный лог, достаточно вызвать:
log.Println("Server is preparing to start")
Впереди устанавливается http-сервер для отдачи статики, например, nginx.
Установка, обновление и откат веб-приложения целиком ложатся на пакетный менеджер linux-системы (yum/dnf/rpm), в результате чего эта иногда нетривиальная задача становиться простой и надежной.
Основная логика
Для некоторых задач мы будем пользоваться готовым тулкитом Gorilla toolkit и на его основе, по сути, сделаем свой несколько расширенный тулкит.
Инициализация приложения
Приложение имеет объекты, которые изменяются лишь однажды при старте — это структуры конфигурации, роутеров, объекты доступа к базе данных и шаблонам. Для консолидации и удобного их применения, создадим структуру Application:
type MapRoutes map[string]Controller
type Application struct {
Doc AbstractPage
Config Config
DB SQL
routes MapRoutes
}
// Routes устанавливает обработчики запросов в соответствии с URL'ами
func (app *Application) Routes(r MapRoutes) {
app.routes = r
}
func (app *Application) Run() {
r := mux.NewRouter()
r.StrictSlash(true)
for url, ctrl := range app.routes {
r.HandleFunc(url, obs(ctrl))
}
http.Handle("/", r)
listen := fmt.Sprintf("%s:%d", app.Config.Net.Listen_host, app.Config.Net.Listen_port)
log.Println("Server is started on", listen)
if err := http.ListenAndServe(listen, nil); err != nil {
log.Println(err)
}
}
Объект Application в приложении конечно же должен быть один:
var appInstance *Application
// GetApplication возвращает экземпляр Application
func GetApplication() *Application {
if appInstance == nil {
appInstance = new(Application)
// Init code
appInstance.Config = loadConfig("config.ini")
appInstance.Doc = make(AbstractPage)
appInstance.routes = make(MapRoutes)
// ...
}
return appInstance
}
Таким образом, использование нашего Application будет достаточно простым:
package main
import (
"interfaces/app"
"interfaces/handlers"
"log"
)
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}
func main() {
log.Println("Server is preparing to start")
Application := app.GetApplication()
if Application.Config.Site.Disabled {
log.Println("Site is disabled")
Application.Routes(app.MapRoutes{"/": handlers.HandleDisabled{}})
} else {
Application.Routes(app.MapRoutes{
"/": handlers.HandleHome{},
"/v1/ajax/": handlers.HandleAjax{},
// другие контроллеры
"/{url:.*}": handlers.Handle404{},
})
}
Application.Run()
log.Println("Exit")
}
httpHandler с контекстом *Context
Самое интересное здесь именно установление роутеров:
for url, ctrl := range app.routes {
r.HandleFunc(url, obs(ctrl))
}
Дело в том, что в Router из тулкита Gorilla ровно как и в стандартной библиотеке «net/http» работа обработчика (контроллера) сводится к функции типа func(http.ResponseWriter, *http.Request). Нам же интересен другой вид контроллера, чтобы не дублировать код из контроллера в контроллер тривиальными операциями:
func ProductHandler(ctx *Context) {
// ...
}
где *Context — удобный инструмент работы с куками, сессией и другими контекстно-зависимыми структурами. Если говорить более детально, то нас интересует не только контекст реквеста в контроллере, но и доступ к БД, к конфигурации, т.е. и к объекту Application. Для этого вводим функцию обертку obs(handler Controller) func(http.ResponseWriter, *http.Request), которая на вход получает нужный нам вид контроллера — интерфейс Controller, а возвращает нужный для r.HandleFunc() вид функции и при этом выполняет все надстроечные действия перед выполнением контроллера — создание *ContextApplication объекта.
type Controller interface {
GET(app *ContextApplication)
POST(app *ContextApplication)
PUT(app *ContextApplication)
DELETE(app *ContextApplication)
PATCH(app *ContextApplication)
OPTIONS(app *ContextApplication)
HEAD(app *ContextApplication)
TRACE(app *ContextApplication)
CONNECT(app *ContextApplication)
}
// obs инициализирует контекст для заданного клиента и вызывает контроллер
func obs(handler Controller) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
ctx := context.New(w, req)
app := GetApplication()
doc := app.Doc.Clone("")
doc["Ctx"] = ctx
doc["User"] = ctx.User()
contextApp := &ContextApplication{ctx, doc, app.Config, app.DB}
switch ctx.Input.Method() {
case "GET": handler.GET(contextApp);
case "POST": handler.POST(contextApp);
case "PUT": handler.PUT(contextApp);
case "DELETE": handler.DELETE(contextApp);
case "PATCH": handler.PATCH(contextApp);
case "OPTIONS": handler.OPTIONS(contextApp);
case "HEAD": handler.HEAD(contextApp);
case "TRACE": handler.TRACE(contextApp);
case "CONNECT": handler.CONNECT(contextApp);
default: http.Error(ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
}
}
// HTTPController объект для встраивания в контроллеры, содержащие стандартные методы для контроллера
// Задача контроллеров переписать необходимые методы.
type HTTPController struct {}
func (h HTTPController) GET(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) POST(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) PUT(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) DELETE(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) PATCH(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) OPTIONS(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) HEAD(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) TRACE(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
func (h HTTPController) CONNECT(app *ContextApplication) {
http.Error(app.Ctx.Response(), "Method not allowed", http.StatusMethodNotAllowed)
}
type ContextApplication struct {
Ctx *context.Context
Doc AbstractPage
Config Config
DB SQL
}
Создание контроллера
Теперь все готово для создание контроллера:
import (
"interfaces/app"
)
type HandleCustom struct {
app.HTTPController
}
func (h HandleCustom) GET(app *app.ContextApplication) {
app.Ctx.SendHTML("html data here")
}
func (h HandleCustom) POST(app *app.ContextApplication) {
// and so on...
}
Процесс создания нового контроллера заключается в переписывании методов встроенного app.HTTPController объекта (GET, POST и т.п.). Если не переписать метод, то вызовется встроенный, который возвращает клиенту «Method not allowed» (это поведение можно изменить на любое другое).
Контекст
Context по сути состоит из набора методов для упрощения работы с контекстно-зависимыми переменными. Не буду писать реализацию, вкратце перечислю некоторые методы, чтобы было ясно о чем идет речь:
func (c *Context) NotFound() // NotFound sends page with 404 http code from template tpls/404.tpl
func (c *Context) Redirect(url string) // Redirect sends http redirect with 301 code
func (c *Context) Redirect303(url string) // Redirect303 sends http redirect with 303 code
func (c *Context) SendJSON(data string) int // SendJSON sends json-content (data)
func (c *Context) SendXML(data string) // SendXML sends xml-content (data)
func (c *Context) GetCookie(key string) string // GetCookie return cookie from request by a given key.
func (c *Context) SetCookie(name string, value string, others ...interface{}) // SetCookie set cookie for response.
func (c *Context) CheckXsrfToken() bool // CheckXsrfToken проверяет token
func (c *Context) User() User // User возвращает текущего пользователя
func (c *Context) Session(name string) (*Session, error) // Session открывает сессию
func (s *Session) Clear() // Clear очищает открытую сессию
// и т.д.
Шаблонизатор
В составе стандартной библиотеки есть замечательный пакет «html/template». Его и будем использовать, немного расширив его функционал.
// loadTemplate load template from tpls/%s.tpl
func loadTemplate(Name string) *html.Template {
funcMap := html.FuncMap{
"html": func(val string) html.HTML {
return html.HTML(val)
},
"typo": func(val string) string {
return typo.Typo(val)
},
"mod": func(args ...interface{}) interface{} {
if len(args) == 0 {
return ""
}
name := args[0].(string)
ctx := new(context.Context)
if len(args) > 1 {
ctx = args[1].(*context.Context)
}
modules := reflect.ValueOf(modules.Get())
mod := modules.MethodByName(name)
if (mod == reflect.Value{}) {
return ""
}
inputs := make([]reflect.Value, 0)
inputs = append(inputs, reflect.ValueOf(ctx))
ret := mod.Call(inputs)
return ret[0].Interface()
},
}
return html.Must(html.New("*").Funcs(funcMap).Delims("{{%", "%}}").ParseFiles("tpls/" + Name + ".tpl"))
}
Для совместимости с AngularJS меняем разделители с "{{ }}" на "{{% %}}", хотя, признаюсь, не совсем удобно.
Более подробно о 3-х вышеуказанных pipeline-функций:
- html — меняет тип входного параметра на HTML, чтобы шаблон не экранировал HTML-строки. Иногда бывает полезно. Пример использования в шаблоне:
<div>{{% .htmlString | html %}}</div>
- typo — обработка текста по некоторым типографическим правилам. Пример использования в шаблоне:
<h1>{{% .title | typo %}}</h1>
- mod — запуск модулей прямо из тела шаблона. Пример использования:
<div>{{% mod "InformMenu" %}}</div>
type AbstractPage map[string]interface{}
AbstractPage является контейнером входных данных для использования их в template'ах. Приведу пример:
func (h HandleCustom) GET(app *app.ContextApplication) {
doc := app.Doc.Clone("custom") // Создается новый AbstractPage, который будет использовать custom.tpl
doc["V1"] = "V1"
doc["V2"] = 555
result := doc.Compile()
app.Ctx.SendHTML(result)
}
{{%define "*"%}}
<ul>
<li>{{% .V1 %}}</li>
<li>{{% .V2 %}}</li>
</ul>
{{%end%}}
AbstractPage имеет 2 метода:
- Метод Clone()
// Clone возвращает новый экземпляр AbstractPage c наследованными полями и значениями func (page AbstractPage) Clone(tplName string) AbstractPage { doc := make(AbstractPage) for k, v := range page { doc[k] = v } doc["__tpl"] = tplName return doc }
Создает новый контейнер AbstractPage, копируя все значения. Смысл этой операции заключается в наследовании значений с вышестоящих уровней AbstractPage. - Метод Compile()
// Compile return page formatted with template from tpls/%d.tpl func (page AbstractPage) Compile() string { var data bytes.Buffer for k, v := range page { switch val := v.(type) { case AbstractPage: { page[k] = html.HTML(val.Compile()) } case func()string: { page[k] = val() } } } // Директива загрузки модулей динамичная (ctx записан в doc["Ctx"]) getTpl(page["__tpl"].(string)).Execute(&data, page) return data.String() }
Выполняет прогон шаблона и формирует результирующий HTML-код.
Резюме
На мой взгляд, получилось гибко и довольно просто. Остальная разработка связана с реализацией конкретных контроллеров и модулей, которые по своей природе независимы друг от друга.
Хотелось бы отметить, что Go не оставил меня равнодушным, также как и многих.
Ссылки
1. github.com/dblokhin/typo — golang package для обработки текста по некоторым типографическим правилам.
Комментарии (28)
taliban
17.06.2015 19:50+4Сгусток сумбура, эта статья явно не для новичков. Эта статья кто уже как минимум знаком с языком, и кто писал на нем программы и запускал их, иначе будет миллиард вопросов про компиляцию, импорт пакетов, файлы итд.
ZoomLS
18.06.2015 00:03>>Впереди устанавливается http-сервер для отдачи статики, например, nginx
А совсем недавно статья была, там говорилось что Go ненужны никакие nginx и прочие апачи, якобы он настолько самостоятельный, что это всё сам обрабатывать может легко. И где правду искать? Круто конечно, если действительно он настолько самодостаточный.frol
18.06.2015 01:25Атак на web-серверы превеликое множество, я бы не рисковал выставлять Go в интернет. Несмотря на то, что он скорее всего будет справляться с определённой нагрузкой, его вероятнее всего не составит труда свадить банальной slowloris. Это не говоря о том, что реализовывать HTTPS самостоятельно — ещё более плохая идея. Подводя итог, использовать Go как самостоятельный веб-сервер хоть и можно, но не рекомендуется уж точно.
neolink
18.06.2015 01:43+3что значит https самостоятельно? в go в стандартной библиотеке есть реализация tls.
вот slowloris написанный на go для nginx github.com/valyala/goloris
веб сервер на go дает ~ 20000-25000 rps на ядро, это примерно на 20-30% медленее nginx на тойже машине в задаче отдачи статических файлов
подводя итог ваши аргументы достаточно странные как и рекомендации.
если не нужен reverse-proxy то смысла его использовать нет — получится двойной парсинг запроса, ещё больше занятых сокетов и т.п, что в итоге даст меньшую производительность.frol
18.06.2015 09:24+1How quickly it can take down unprotected nginx with default settings?
Это ещё раз подтверждает, что не стоит делать свой велосипед, если даже специально заточенный проект борется с этими атаками годами. Ну и, как видно, в Nginx 1.6, который вышел больше года назад, уже приняты меры, но даже без обновления до 1.6 можно было залатать уязвимость при помощи конфигурирования.
In a few minutes with default config options.
Which versions of nginx are vulnerable?
All up to 1.5.9 if unprotected as described below (i.e. with default config).
Если вы хотите велосипед — мне вас не остановить. Не доверяете граблям, на которые наступили другие, — наступите на них сами.
dblokhin Автор
18.06.2015 01:54+3Дело не просто в отдачи статики. Nginx имеет целый багаж полезных фич и настроек — кэш, сжатие, limit_rate, limit_req, геомодуль, все не перечесть — вы и так все знаете. Не делать же этот функционал заново средствами Go.
Alexeyco
18.06.2015 11:02+3Не все понимают, что между фразами «go умеет самостоятельно» и «лучше над go ставить веб-сервер» нет противоречия.
Crandel
18.06.2015 09:44+3Скиньте пожалуйста все это в гитхаб, хочется всю структуру проекта оценить
dblokhin Автор
18.06.2015 12:16dblokhin Автор
18.06.2015 12:17Задавайте свои вопросы и/или оставляйте свои рекомендации.
Crandel
18.06.2015 16:42не очень понятно зачем в каждом темплейте
{{%define "*"%}}dblokhin Автор
19.06.2015 03:53Это именование шаблона, требование системы шаблонов «text/template» (и соответственно «html/template»), т.к. шаблоны могут быть множественно представлены в одном файле: golang.org/pkg/text/template/#hdr-Nested_template_definitions
Т.е. определять имя шаблона необходимо в любом случае, даже если вы не обращаете на это внимание. В моем примере все шаблоны именуются одинаково "*". Можно дать любое другое именование.
xlin
20.06.2015 14:49Статья явно не для новичков. Не понял ничего. Хотя не много (совсем не много) пытаюсь писать мини программки на go.
x88
Пример реализации простых приложений это может и хорошо, но вот мы не нашли паттерна реализации или фреймворка для крупных веб приложений. У меня довольно большой опыт web разработки (с применением Yii, laravel, kohana, symphony), и буквально неделю назад начали писать под свой проект (с дальнейшей opensource публикацией) вменяемый fullstack фреймворк со всеми плюшками ORM, валидаторами, i18n и т. д. Если есть желающие присоединиться к разработке, пишите в лс.
neolink
только он symfony
shoomyst
Symfony — для малоопытных, symphony — для многоопытных
vsb
Revel это не оно?
denis_g
Может быть именно в этом и состоит дзен Go: вместо больших фреймворков использовать небольшие, но модульные решения (один инструмент выполняет одну задачу). И, черт побери, после жутко монолитной Джанги, мне это как бальзам на душу.
Pryada
Микросервисы? REST Microservices in Go with Gin
denis_g
Типа того :)
VasilioRuzanni
А нужно ли оно вообще, свой фреймворк-то, в котором все-все-все?
Связка Gin + Gorm (или beego + beego ORM) + govalidator + go-i18n + whatever не подходит по объективным причинам или обладает фатальным недостатком?
alehano
+1 можно легко собрать свой фреймворк и заменять детали на ходу.
VasilioRuzanni
Именно. Мне вот это в node и Go сообществах и нравится — понимание, что не нужны большие монолитные фреймворки, где есть от и до, а под задачу удобно собирать набор заменяемых при случае компонентов, каждый из которых делает что-то конкретное.
Alexeyco
beego.me не понравился? martini — для приложений поменьше (за отсутствие ссылок прошу прощения — не позволяет карма)
alehano
Вместо Martini лучше на Gin посмотреть.