Статический сайт на Go

Создаём статический сайт

Недавно я перенес сайт, который вы сейчас читаете, с приложения Ghost/Node.js на статический сайт, обслуживаемый Go. Пока это свежо в моей памяти, вот объяснение принципов создания и обслуживания статических сайтов с помощью Go.

Начнём с простого, но реального примера: обслуживание файлов HTML и CSS из определённого места на диске.

Начните с создания каталога для хранения проекта:

CONSOLE
$ mkdir static-site
$ cd static-site
Нажмите, чтобы развернуть и увидеть больше

Затем добавьте файл main.go для хранения нашего кода, а также несколько простых файлов HTML и CSS в static каталог.

CONSOLE
$ touch main.go
$ mkdir -p static/css
$ touch static/example.html static/css/main.css
Нажмите, чтобы развернуть и увидеть больше
static/example.html
<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<title>Статичная страница</title>
	<link rel="stylesheet" href="/css/main.css">
</head>
<body>
	<h1>Это статичная страница</h1>
</body>
</html>
Нажмите, чтобы развернуть и увидеть больше
static/css/main.css
body {color: #9f0712}
Нажмите, чтобы развернуть и увидеть больше

После создания этих файлов код, необходимый для запуска и работы, становится удивительно компактным:

main.go
 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 шаблон "/" соответствует всем путям запросов, а не только пустому пути.

Давайте запустим приложение:

CONSOLE
$ go run main.go
2025/08/18 10:43:33 Listening on :3000...
Нажмите, чтобы развернуть и увидеть больше

Откройте в браузере адрес http://localhost:3000/example.html. Вы должны увидеть созданную нами HTML-страницу с большим красным заголовком.

Практически статические сайты

Если вы вручную создаете много статических HTML-файлов, повторение шаблонного контента может быть утомительным. Давайте рассмотрим использование пакета html/template языка Go для размещения общей разметки в файле макета.

В настоящее время все запросы обрабатываются нашим файловым сервером. Давайте внесем небольшое изменение в наше приложение, чтобы файловый сервер обрабатывал только те пути запросов, которые начинаются с шаблона /static/.

main.go
 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.

CONSOLE
$ mkdir templates
$ touch templates/layout.tmpl templates/example.tmpl
Нажмите, чтобы развернуть и увидеть больше
templates/layout.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}}
Нажмите, чтобы развернуть и увидеть больше
templates/example.tmpl
{{define "title"}}Шаблонная страница{{end}}

{{define "body"}}
<h1>Это шаблонная страница</h1>
{{end}}
Нажмите, чтобы развернуть и увидеть больше

Если вы ранее использовали шаблоны в других веб-фреймворках или языках, то, надеюсь, это будет вам знакомо.

Шаблоны Go — в том виде, в котором мы их используем здесь — по сути представляют собой просто именованные текстовые блоки, окруженные тегами {{define}} и {{end}}. Шаблоны можно вставлять друг в друга с помощью тега {{template}}, как мы делаем выше, где шаблон макета вставляет шаблоны заголовка и тела.

Давайте обновим код приложения, чтобы использовать их:

main.go
 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-адреса перед его использованием.

Затем мы используем функцию template.ParseFiles(), чтобы объединить запрошенный шаблон и макет в набор шаблонов. Наконец, мы используем функцию template.ExecuteTemplate(), чтобы отобразить именованный шаблон в наборе, в нашем случае макет шаблона.

Перезапустите приложение:

CONSOLE
$ go run main.go
2025/08/18 11:13:43 Listening on :3000...
Нажмите, чтобы развернуть и увидеть больше

Откройте в браузере файл http://localhost:3000/example.html. Вы должны увидеть разметку всех шаблонов, объединенную следующим образом:

Если вы используете инструменты веб-разработчика для проверки HTTP-ответа, вы также увидите, что Go автоматически устанавливает для нас правильные заголовки Content-Type и Content-Length.

Наконец, давайте сделаем код немного более надежным. Мы должны:

main.go
 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}
Нажмите, чтобы развернуть и увидеть больше

Авторское право

Автор: Арсений Соколов

Ссылка: https://cdn.arsen.pw/posts/serving-static-sites-with-go/

Лицензия: CC BY-NC-SA 4.0

Эта работа лицензирована в соответствии с международной лицензией Creative Commons Attribution-NonCommercial-ShareAlike 4.0. Пожалуйста, указывайте источник, используйте в некоммерческих целях и сохраняйте ту же лицензию.

Комментарии

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша