Создаём статический сайт
Недавно я перенес сайт, который вы сейчас читаете, с приложения Ghost/Node.js на статический сайт, обслуживаемый Go. Пока это свежо в моей памяти, вот объяснение принципов создания и обслуживания статических сайтов с помощью Go.
Начнём с простого, но реального примера: обслуживание файлов HTML и CSS из определённого места на диске.
Начните с создания каталога для хранения проекта:
$ mkdir static-site
$ cd static-site
Затем добавьте файл main.go
для хранения нашего кода, а также несколько простых файлов HTML и CSS в static
каталог.
$ touch main.go
$ mkdir -p static/css
$ touch static/example.html static/css/main.css
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Статичная страница</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<h1>Это статичная страница</h1>
</body>
</html>
body {color: #9f0712}
После создания этих файлов код, необходимый для запуска и работы, становится удивительно компактным:
1package main
2
3import (
4 "log"
5 "net/http"
6)
7
8func main() {
9 fs := http.FileServer(http.Dir("./static"))
10 http.Handle("/", fs)
11
12 log.Print("Listening on :3000...")
13 err := http.ListenAndServe(":3000", nil)
14 if err != nil {
15 log.Fatal(err)
16 }
17}
Давайте разберемся в написанном коде.
Сначала мы используем функцию http.FileServer()
для создания обработчика, который отвечает на все HTTP-запросы содержимым заданной файловой системы. Для нашей файловой системы мы используем каталог static
, относительный в нашем приложении, но вы можете использовать любой другой каталог на вашем компьютере (или любой объект, который реализует интерфейс http.FileSystem
). Затем мы используем функцию http.Handle()
, чтобы зарегистрировать файловый сервер в качестве обработчика для всех запросов, и запускаем сервер, прослушивающий порт 3000.
Стоит отметить, что в Go шаблон "/"
соответствует всем путям запросов, а не только пустому пути.
Давайте запустим приложение:
$ go run main.go
2025/08/18 10:43:33 Listening on :3000...
Откройте в браузере адрес http://localhost:3000/example.html. Вы должны увидеть созданную нами HTML-страницу с большим красным заголовком.
Практически статические сайты
Если вы вручную создаете много статических HTML-файлов, повторение шаблонного контента может быть утомительным. Давайте рассмотрим использование пакета html/template
языка Go для размещения общей разметки в файле макета.
В настоящее время все запросы обрабатываются нашим файловым сервером. Давайте внесем небольшое изменение в наше приложение, чтобы файловый сервер обрабатывал только те пути запросов, которые начинаются с шаблона /static/
.
8func main() {
9 fs := http.FileServer(http.Dir("./static"))
10 http.Handle("/static/", http.StripPrefix("/static/", fs))
11
12 log.Print("Listening on :3000...")
13 err := http.ListenAndServe(":3000", nil)
14 if err != nil {
15 log.Fatal(err)
16 }
17}
Обратите внимание, что поскольку наш каталог static
установлен в качестве корня файловой системы, нам необходимо удалить префикс /static/
из пути запроса перед поиском данного файла в файловой системе. Для этого мы используем функцию http.StripPrefix()
.
Если вы перезапустите приложение, вы должны найти CSS-файл, который мы создали ранее, по адресу http://localhost:3000/static/css/main.css.
$ mkdir templates
$ touch templates/layout.tmpl templates/example.tmpl
{{define "layout"}}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{template "title"}}</title>
<link rel="stylesheet" href="/static/stylesheets/main.css">
</head>
<body>
{{template "body"}}
<footer>Сделано на Go</footer>
</body>
</html>
{{end}}
{{define "title"}}Шаблонная страница{{end}}
{{define "body"}}
<h1>Это шаблонная страница</h1>
{{end}}
Если вы ранее использовали шаблоны в других веб-фреймворках или языках, то, надеюсь, это будет вам знакомо.
Шаблоны Go — в том виде, в котором мы их используем здесь — по сути представляют собой просто именованные текстовые блоки, окруженные тегами {{define}}
и {{end}}
. Шаблоны можно вставлять друг в друга с помощью тега {{template}}
, как мы делаем выше, где шаблон макета вставляет шаблоны заголовка и тела.
Давайте обновим код приложения, чтобы использовать их:
1package main
2
3import (
4 "html/template"
5 "log"
6 "net/http"
7 "path/filepath"
8 "strings"
9)
10
11func main() {
12 fs := http.FileServer(http.Dir("./static"))
13 http.Handle("/static/", http.StripPrefix("/static/", fs))
14
15 http.HandleFunc("/", serveTemplate)
16
17 log.Print("Listening on :3000...")
18 err := http.ListenAndServe(":3000", nil)
19 if err != nil {
20 log.Fatal(err)
21 }
22}
23
24func serveTemplate(w http.ResponseWriter, r *http.Request) {
25 lp := filepath.Join("templates", "layout.tmpl")
26
27 // Преобразуем путь: заменяем .html на .tmpl
28 urlPath := r.URL.Path
29 if strings.HasSuffix(urlPath, ".html") {
30 // Заменяем .html на .tmpl
31 urlPath = strings.TrimSuffix(urlPath, ".html") + ".tmpl"
32 } else if urlPath == "/" {
33 // Для корневого пути можно задать файл по умолчанию
34 urlPath = "/index.tmpl"
35 } else {
36 // Если путь не содержит .html, добавляем .tmpl
37 urlPath = urlPath + ".tmpl"
38 }
39
40 fp := filepath.Join("templates", filepath.Clean(urlPath))
41
42 tmpl, _ := template.ParseFiles(lp, fp)
43 tmpl.ExecuteTemplate(w, "layout", nil)
44}
Так что же здесь изменилось?
Сначала мы добавили пакеты html/template
и path
в оператор import
.
Затем мы указали, что все запросы, не обработанные сервером статических файлов, должны обрабатываться с помощью новой функции serveTemplate()
(если вам интересно, Go сопоставляет шаблоны на основе длины, причем более длинные шаблоны имеют приоритет над более короткими).
В функции serveTemplate()
мы создаем пути к файлу макета и файлу шаблона, соответствующим запросу. Вместо ручного объединения мы используем filepath.Join()
, который имеет преимущество в том, что объединяет пути с использованием правильного разделителя для вашей ОС. Также функция заменяет суфикс .html
на .tmpl
, для использования шаблонизатором правильных имён файлов, а для посетителей остаются привычные расширения HTML файлов.
Важно отметить, что поскольку URL-адрес является недоверенным пользовательским вводом, мы используем filepath.Clean()
для очистки URL-адреса перед его использованием.
Обратите внимание
Даже несмотря на то, что filepath.Join()
автоматически пропускает соединенный путь через filepath.Clean()
, для предотвращения атак с перебором каталогов вам необходимо вручную очистить все ненадежные входные данные перед их объединением.
Затем мы используем функцию template.ParseFiles()
, чтобы объединить запрошенный шаблон и макет в набор шаблонов. Наконец, мы используем функцию template.ExecuteTemplate()
, чтобы отобразить именованный шаблон в наборе, в нашем случае макет шаблона.
Перезапустите приложение:
$ go run main.go
2025/08/18 11:13:43 Listening on :3000...
Откройте в браузере файл http://localhost:3000/example.html. Вы должны увидеть разметку всех шаблонов, объединенную следующим образом:
Если вы используете инструменты веб-разработчика для проверки HTTP-ответа, вы также увидите, что Go автоматически устанавливает для нас правильные заголовки Content-Type
и Content-Length
.
Наконец, давайте сделаем код немного более надежным. Мы должны:
- Отправить ответ
404
, если запрошенный шаблон не существует. - Отправить ответ
404
, если путь к запрошенному шаблону является каталогом. - Отправить ответ
500
, если функцииtemplate.ParseFiles()
илиtemplate.ExecuteTemplate()
вызывают ошибку, и зарегистрировать подробное сообщение об ошибке в журнале.
1package main
2
3import (
4 "html/template"
5 "log"
6 "net/http"
7 "os"
8 "path/filepath"
9 "strings"
10)
11
12func main() {
13 fs := http.FileServer(http.Dir("./static"))
14 http.Handle("/static/", http.StripPrefix("/static/", fs))
15 http.HandleFunc("/", serveTemplate)
16
17 log.Print("Listening on :3000...")
18 err := http.ListenAndServe(":3000", nil)
19 if err != nil {
20 log.Fatal(err)
21 }
22}
23
24func serveTemplate(w http.ResponseWriter, r *http.Request) {
25 lp := filepath.Join("templates", "layout.tmpl")
26
27 urlPath := r.URL.Path
28 if strings.HasSuffix(urlPath, ".html") {
29 urlPath = strings.TrimSuffix(urlPath, ".html") + ".tmpl"
30 } else if urlPath == "/" {
31 urlPath = "/index.tmpl"
32 } else {
33 urlPath = urlPath + ".tmpl"
34 }
35
36 fp := filepath.Join("templates", filepath.Clean(urlPath))
37
38 // Возвращаем 404 если шаблон не найден
39 info, err := os.Stat(fp)
40 if err != nil {
41 if os.IsNotExist(err) {
42 http.NotFound(w, r)
43 return
44 }
45 }
46
47 // Возвращаем 404 если запро ведёт к каталогу
48 if info.IsDir() {
49 http.NotFound(w, r)
50 return
51 }
52
53 tmpl, err := template.ParseFiles(lp, fp)
54 if err != nil {
55 // Выводим в лог детали ошибки
56 log.Print(err.Error())
57 // Возвращаем общее сообщение "Internal Server Error" ("Внутреняя ошибка сервера")
58 http.Error(w, http.StatusText(500), 500)
59 return
60 }
61
62 err = tmpl.ExecuteTemplate(w, "layout", nil)
63 if err != nil {
64 log.Print(err.Error())
65 http.Error(w, http.StatusText(500), 500)
66 }
67}
Комментарии