Хотите узнать, как использовать HTML-шаблоны, CSS, JavaScript, картинки и конфиги прямо внутри вашего Go-приложения? Давайте разберёмся, как это сделать с помощью пакета embed.
Одна из самых крутых фишек Go — это возможность собрать всё приложение в один-единственный бинарный файл. И начиная с версии 1.16, пакет embed делает процесс встраивания статических файлов простым и интуитивным. Забудьте о ситуациях, когда в продакшене вдруг не хватает какого-то файла или ресурса: теперь всё, что нужно, будет всегда под рукой — прямо внутри исполняемого файла.
Пакет embed
Пакет embed в Go позволяет включать файлы на этапе компиляции:
package main
import (
"embed"
"fmt"
)
var content string
func main() {
fmt.Println(content)
}
Директива //go:embed указывает компилятору включить файл hello.txt в бинарный файл.
Шаблоны встраивания
Один файл как строка
package main
import (
_ "embed"
"fmt"
)
var configJSON string
func main() {
fmt.Println(configJSON)
}
Один файл как байты
package main
import (
_ "embed"
)
var imageData []byte
func main() {
fmt.Printf("Размер изображения: %d байт\n", len(imageData))
}
Каталог как embed.FS
package main
import (
"embed"
"io/fs"
"log"
)
var templates embed.FS
func main() {
entries, err := fs.ReadDir(templates, "templates")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Println(entry.Name())
}
}
Типичные сценарии использования
Встраивание HTML-шаблонов
package main
import (
"embed"
"html/template"
"net/http"
)
var templateFS embed.FS
var templates *template.Template
func init() {
templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))
}
type PageData struct {
Title string
Message string
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := PageData{
Title: "Welcome",
Message: "Hello from embedded templates!",
}
templates.ExecuteTemplate(w, "index.html", data)
})
http.ListenAndServe(":8080", nil)
}
Создайте файл templates/index.html:
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Message}}</h1>
</body>
</html>
Встраивание статических веб-ресурсов
package main
import (
"embed"
"io/fs"
"net/http"
)
//go:embed static/*
var staticFS embed.FS
func main() {
// Убираем префикс "static" из путей
staticContent, _ := fs.Sub(staticFS, "static")
// Обслуживание файлов по адресу /static/
http.Handle("/static/", http.StripPrefix("/static/",
http.FileServer(http.FS(staticContent))))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h1>Hello!</h1>
<script src="/static/app.js"></script>
</body>
</html>
`))
})
http.ListenAndServe(":8080", nil)
}
Встраивание конфигурационных файлов
package main
import (
_ "embed"
"encoding/json"
"fmt"
)
var defaultConfigJSON []byte
type Config struct {
AppName string `json:"appName"`
Port int `json:"port"`
Debug bool `json:"debug"`
AllowedHosts []string `json:"allowedHosts"`
}
func loadConfig() (*Config, error) {
var config Config
if err := json.Unmarshal(defaultConfigJSON, &config); err != nil {
return nil, err
}
return &config, nil
}
func main() {
config, err := loadConfig()
if err != nil {
panic(err)
}
fmt.Printf("Приложение: %s, Порт: %d\n", config.AppName, config.Port)
}
Встраивание файлов миграций SQL
package main
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"sort"
"strings"
)
var migrationsFS embed.FS
func runMigrations(db *sql.DB) error {
entries, err := fs.ReadDir(migrationsFS, "migrations")
if err != nil {
return err
}
var filenames []string
for _, entry := range entries {
if strings.HasSuffix(entry.Name(), ".sql") {
filenames = append(filenames, entry.Name())
}
}
sort.Strings(filenames)
for _, filename := range filenames {
content, err := fs.ReadFile(migrationsFS, "migrations/"+filename)
if err != nil {
return err
}
fmt.Printf("Выполнение миграции: %s\n", filename)
if _, err := db.Exec(string(content)); err != nil {
return fmt.Errorf("миграция %s не выполнена: %w", filename, err)
}
}
return nil
}
func main() {
fmt.Println("Миграции успешно встроены")
entries, _ := fs.ReadDir(migrationsFS, "migrations")
for _, e := range entries {
fmt.Println(" -", e.Name())
}
}
Несколько директив embed
Вы можете встраивать несколько шаблонов:
package main
import (
"embed"
)
var assets embed.FS
var multiAssets embed.FS
Рекурсивное встраивание
Используйте префикс all: для включения файлов, начинающихся с . или _:
package main
import (
"embed"
)
var publicAssets embed.FS
var allAssets embed.FS
Сопоставление с образцом (Pattern Matching)
Embed поддерживает glob-шаблоны:
package main
import (
"embed"
)
var sourceFiles embed.FS
var configFiles embed.FS
var rootFiles embed.FS
Шаблон для разработки и продакшена
Используйте теги сборки для переключения между встроенными и «живыми» файлами:
package main
import (
"embed"
"io/fs"
)
var staticFS embed.FS
func getStaticFS() fs.FS {
sub, _ := fs.Sub(staticFS, "static")
return sub
}
package main
import (
"io/fs"
"os"
)
func getStaticFS() fs.FS {
return os.DirFS("static")
}
Сборка для продакшена:
go build -o myapp
Сборка для разработки:
go build -tags dev -o myapp
Встраивание информации о версии
package main
import (
_ "embed"
"fmt"
"strings"
)
var version string
func main() {
fmt.Printf("Версия: %s\n", strings.TrimSpace(version))
}
Полный пример веб-сервера
package main
import (
"embed"
"encoding/json"
"html/template"
"io/fs"
"log"
"net/http"
)
var templateFS embed.FS
var staticFS embed.FS
var configJSON []byte
type Config struct {
Port string `json:"port"`
AppName string `json:"appName"`
}
type Server struct {
config *Config
templates *template.Template
}
func NewServer() (*Server, error) {
var config Config
if err := json.Unmarshal(configJSON, &config); err != nil {
return nil, err
}
templates, err := template.ParseFS(templateFS, "web/templates/*.html")
if err != nil {
return nil, err
}
return &Server{
config: &config,
templates: templates,
}, nil
}
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
data := map[string]string{
"AppName": s.config.AppName,
}
s.templates.ExecuteTemplate(w, "home.html", data)
}
func main() {
server, err := NewServer()
if err != nil {
log.Fatal(err)
}
staticContent, _ := fs.Sub(staticFS, "web/static")
http.Handle("/static/", http.StripPrefix("/static/",
http.FileServer(http.FS(staticContent))))
http.HandleFunc("/", server.handleHome)
log.Printf("Запуск %s на порту %s", server.config.AppName, server.config.Port)
log.Fatal(http.ListenAndServe(":"+server.config.Port, nil))
}
Проверка встроенных файлов во время выполнения
package main
import (
"embed"
"fmt"
"io/fs"
)
var assets embed.FS
func listEmbeddedFiles() {
fs.WalkDir(assets, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
info, _ := d.Info()
fmt.Printf("%s (%d байт)\n", path, info.Size())
}
return nil
})
}
func main() {
fmt.Println("Встроенные файлы:")
listEmbeddedFiles()
}
Ограничения и важные моменты
- Только на этапе компиляции: изменения требуют повторной компиляции
- Использование памяти: встроенные файлы хранятся в оперативной памяти
- Отсутствие поддержки символьных ссылок: символические ссылки не обрабатываются
- Разделители путей: всегда используйте прямые слеши
/ - Относительные пути: пути указываются относительно исходного файла Go
// НЕВЕРНО — абсолютные пути не работают
//go:embed /etc/config.json
// НЕВЕРНО — родительские каталоги не работают
//go:embed ../shared/config.json
// ВЕРНО — путь относительно текущего исходного файла
//go:embed config.json
Резюме
Пакет embed предоставляет несколько способов встраивания файлов:
| Тип переменной | Назначение |
|---|
string | Один текстовый файл |
[]byte | Один бинарный файл |
embed.FS | Несколько файлов или каталогов |
Рекомендации по использованию:
- Используйте
embed.FS для группы связанных файлов - Применяйте
fs.Sub() для создания чистых префиксов путей - Рассмотрите использование тегов сборки для режима разработки
- Встраивайте конфигурационные значения по умолчанию, переопределяя их во время выполнения
- Учитывайте размер бинарного файла при работе с крупными ресурсами