package main import ( "bytes" "log" "main/cache" "main/config" "main/tools" "net/http" "os" "path/filepath" "strings" "text/template" "time" ) /* sudo systemctl stop server.service go build main.go sudo systemctl start server.service */ // App представляет основное состояние приложения type app struct { templates *template.Template // Шаблоны страниц config *config.Config // Конфиг startTime int64 // Время запуска cache *cache.Cache // Кэш (отрендеренные странички) } func main() { var app *app var err error // Инициализация приложения if app, err = appInit("config.json"); err != nil { log.Fatal(err) } // Настройка маршрутов и запуск if app.setupRouterAndRun() != nil { log.Fatal(err) } } // Запускает сервер на указанном IP и порту func runServer(ip, port string, router http.Handler) { addr := ip + port log.Println("Run on", addr) log.Fatal(http.ListenAndServe(addr, router)) } // Инициализирует приложение func appInit(configPath string) (*app, error) { a := &app{ startTime: time.Now().Unix(), config: config.Init(), cache: cache.Init(), } // Загрузка конфига if err := a.config.Load(configPath); err != nil { return nil, err } // Загрузка шаблонов if err := a.loadTemplates(a.config.TemplatesPath, a.config.TemplatesExt); err != nil { log.Fatal(err) } return a, nil } func (a *app) setupRouterAndRun() error { // Настройка маршрутов router := a.setupRouter() // Запуск сервера if ok, err := tools.IsIPInUse(a.config.ServerIP); err != nil { return err } else if ok { runServer(a.config.ServerIP, a.config.Port, router) } else { runServer(a.config.LocalIP, a.config.Port, router) } return nil } // Настраивает маршруты func (a *app) setupRouter() *http.ServeMux { router := http.NewServeMux() // Обработка статических файлов с кэшированием router.Handle(a.config.AssetsPath, a.staticHandler()) // Обработка главной страницы router.Handle("/", a.mainPageHandler()) return router } // Обработчик статических файлов с кэшированием func (a *app) staticHandler() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Cache-Control", "public, max-age=31536000, immutable") // Здесь используется встроенный файловый сервер Go (http.FileServer), который: // Реализует интерфейс http.Handler (и поэтому имеет метод ServeHTTP) // Автоматически обслуживает статические файлы из файловой системы // Сам обрабатывает HTTP-запросы, определяет MIME-типы, отправляет правильные заголовки и т.д. http.FileServer(http.Dir(".")).ServeHTTP(w, r) }) } // Обработчик главной страницы func (a *app) mainPageHandler() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var err error tmplName := "main" + a.config.TemplatesExt // Страничка рендерится только если ее нет в кэше pageData, ok := a.cache.Get(tmplName) if !ok { context := map[string]any{ "version": a.startTime, "renderingTimestamp": time.Now().Unix(), } pageData, err = a.renderPage(tmplName, context) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } a.cache.Set(tmplName, pageData) } a.sendPage(w, pageData.([]byte)) }) } // Загрузка шаблонов func (a *app) loadTemplates(templatesPath string, ext string) error { tmpls := template.New("") err := filepath.Walk(templatesPath, func(path string, f os.FileInfo, err error) error { if err != nil { return err } if !f.IsDir() && strings.HasSuffix(f.Name(), ext) { _, err = tmpls.ParseFiles(path) if err != nil { return err } } return nil }) if err != nil { return err } a.templates = tmpls return nil } // Рендерит шаблон в срез байт func (a *app) renderPage(tmplName string, context any) ([]byte, error) { var pageData bytes.Buffer if err := a.templates.ExecuteTemplate(&pageData, tmplName, context); err != nil { return nil, err } return pageData.Bytes(), nil } // Отправляет страницу func (a *app) sendPage(w http.ResponseWriter, data []byte) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write(data) }