1.10.3 嵌入文件系统 #
Go 1.16 引入了 embed
包,这是一个革命性的特性,允许开发者将静态文件直接嵌入到可执行文件中。这个特性解决了长期以来 Go 应用部署时需要携带额外静态资源文件的问题,使得应用的分发和部署变得更加简单。
embed 包的使用方法 #
基本概念 #
embed
包提供了在编译时将文件和目录嵌入到 Go 程序中的能力。嵌入的文件会成为可执行文件的一部分,无需在运行时访问外部文件系统。
基本语法 #
使用 //go:embed
指令来嵌入文件:
package main
import (
_ "embed"
"fmt"
)
// 嵌入单个文件
//go:embed hello.txt
var helloContent string
// 嵌入二进制文件
//go:embed logo.png
var logoData []byte
func main() {
fmt.Println("Hello content:", helloContent)
fmt.Printf("Logo size: %d bytes\n", len(logoData))
}
支持的数据类型 #
embed
支持三种数据类型:
- string - 用于文本文件
- []byte - 用于二进制文件
- embed.FS - 用于文件系统
package main
import (
"embed"
"fmt"
"io/fs"
"log"
)
// 嵌入单个文件为字符串
//go:embed config.json
var configJSON string
// 嵌入单个文件为字节数组
//go:embed assets/logo.png
var logoBytes []byte
// 嵌入整个目录
//go:embed assets/*
var assetsFS embed.FS
// 嵌入多个文件和目录
//go:embed templates/*.html static/css/*.css static/js/*.js
var webFS embed.FS
func main() {
fmt.Println("Config JSON:", configJSON)
fmt.Printf("Logo size: %d bytes\n", len(logoBytes))
// 遍历嵌入的文件系统
err := fs.WalkDir(assetsFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
fmt.Printf("Found: %s (dir: %t)\n", path, d.IsDir())
return nil
})
if err != nil {
log.Fatal(err)
}
}
静态资源的嵌入技巧 #
Web 应用的静态资源 #
让我们创建一个完整的 Web 应用示例,展示如何嵌入各种静态资源:
// main.go
package main
import (
"embed"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"log"
"net/http"
"path/filepath"
"strings"
"time"
)
// 嵌入配置文件
//go:embed config.json
var configData string
// 嵌入模板文件
//go:embed templates/*.html
var templatesFS embed.FS
// 嵌入静态资源
//go:embed static/*
var staticFS embed.FS
// 嵌入数据文件
//go:embed data/*.json
var dataFS embed.FS
// 应用配置
type Config struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
} `json:"server"`
App struct {
Name string `json:"name"`
Version string `json:"version"`
Debug bool `json:"debug"`
} `json:"app"`
}
// 用户数据结构
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
}
// Web 服务器
type WebServer struct {
config *Config
templates *template.Template
users []User
}
func main() {
// 解析配置
var config Config
if err := json.Unmarshal([]byte(configData), &config); err != nil {
log.Fatalf("Failed to parse config: %v", err)
}
// 创建服务器实例
server, err := NewWebServer(&config)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
// 启动服务器
addr := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)
fmt.Printf("Starting %s v%s on %s\n",
config.App.Name, config.App.Version, addr)
log.Fatal(http.ListenAndServe(addr, server))
}
func NewWebServer(config *Config) (*WebServer, error) {
// 解析模板
templates, err := template.ParseFS(templatesFS, "templates/*.html")
if err != nil {
return nil, fmt.Errorf("failed to parse templates: %w", err)
}
// 加载用户数据
usersData, err := dataFS.ReadFile("data/users.json")
if err != nil {
return nil, fmt.Errorf("failed to read users data: %w", err)
}
var users []User
if err := json.Unmarshal(usersData, &users); err != nil {
return nil, fmt.Errorf("failed to parse users data: %w", err)
}
return &WebServer{
config: config,
templates: templates,
users: users,
}, nil
}
func (ws *WebServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 处理静态文件
if strings.HasPrefix(r.URL.Path, "/static/") {
ws.handleStatic(w, r)
return
}
// 处理 API 路由
if strings.HasPrefix(r.URL.Path, "/api/") {
ws.handleAPI(w, r)
return
}
// 处理页面路由
switch r.URL.Path {
case "/":
ws.handleHome(w, r)
case "/users":
ws.handleUsers(w, r)
case "/about":
ws.handleAbout(w, r)
default:
ws.handle404(w, r)
}
}
func (ws *WebServer) handleStatic(w http.ResponseWriter, r *http.Request) {
// 从嵌入的文件系统提供静态文件
staticServer := http.FileServer(http.FS(staticFS))
staticServer.ServeHTTP(w, r)
}
func (ws *WebServer) handleHome(w http.ResponseWriter, r *http.Request) {
data := struct {
Config *Config
Users []User
Time string
}{
Config: ws.config,
Users: ws.users,
Time: time.Now().Format("2006-01-02 15:04:05"),
}
if err := ws.templates.ExecuteTemplate(w, "home.html", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (ws *WebServer) handleUsers(w http.ResponseWriter, r *http.Request) {
data := struct {
Users []User
Count int
}{
Users: ws.users,
Count: len(ws.users),
}
if err := ws.templates.ExecuteTemplate(w, "users.html", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (ws *WebServer) handleAbout(w http.ResponseWriter, r *http.Request) {
data := struct {
Config *Config
}{
Config: ws.config,
}
if err := ws.templates.ExecuteTemplate(w, "about.html", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (ws *WebServer) handleAPI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/api/users":
json.NewEncoder(w).Encode(ws.users)
case "/api/config":
json.NewEncoder(w).Encode(ws.config)
case "/api/stats":
stats := map[string]interface{}{
"users_count": len(ws.users),
"app_name": ws.config.App.Name,
"version": ws.config.App.Version,
"uptime": time.Since(time.Now()).String(),
}
json.NewEncoder(w).Encode(stats)
default:
http.Error(w, "API endpoint not found", http.StatusNotFound)
}
}
func (ws *WebServer) handle404(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
data := struct {
Path string
}{
Path: r.URL.Path,
}
if err := ws.templates.ExecuteTemplate(w, "404.html", data); err != nil {
http.Error(w, "Page not found", http.StatusNotFound)
}
}
创建项目文件结构 #
# 创建项目目录结构
mkdir -p webapp/{templates,static/{css,js,images},data}
# 创建配置文件
cat > webapp/config.json << 'EOF'
{
"server": {
"host": "localhost",
"port": 8080
},
"app": {
"name": "Embedded Web App",
"version": "1.0.0",
"debug": true
}
}
EOF
# 创建用户数据
cat > webapp/data/users.json << 'EOF'
[
{
"id": 1,
"name": "Alice Johnson",
"email": "[email protected]",
"role": "admin"
},
{
"id": 2,
"name": "Bob Smith",
"email": "[email protected]",
"role": "user"
},
{
"id": 3,
"name": "Charlie Brown",
"email": "[email protected]",
"role": "user"
}
]
EOF
HTML 模板文件 #
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.Config.App.Name}}</title>
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<nav class="navbar">
<div class="nav-brand">{{.Config.App.Name}}</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/users">Users</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<main class="container">{{template "content" .}}</main>
<footer class="footer">
<p>© 2024 {{.Config.App.Name}} v{{.Config.App.Version}}</p>
</footer>
<script src="/static/js/app.js"></script>
</body>
</html>
<!-- templates/home.html -->
{{define "content"}}
<div class="hero">
<h1>Welcome to {{.Config.App.Name}}</h1>
<p>Current time: {{.Time}}</p>
</div>
<div class="stats">
<div class="stat-card">
<h3>Total Users</h3>
<p class="stat-number">{{len .Users}}</p>
</div>
<div class="stat-card">
<h3>App Version</h3>
<p class="stat-number">{{.Config.App.Version}}</p>
</div>
</div>
<div class="recent-users">
<h2>Recent Users</h2>
<div class="user-grid">
{{range .Users}}
<div class="user-card">
<h4>{{.Name}}</h4>
<p>{{.Email}}</p>
<span class="role {{.Role}}">{{.Role}}</span>
</div>
{{end}}
</div>
</div>
{{end}}
<!-- templates/users.html -->
{{define "content"}}
<div class="page-header">
<h1>Users ({{.Count}})</h1>
<button onclick="loadUsers()" class="btn btn-primary">Refresh</button>
</div>
<div class="users-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody id="users-tbody">
{{range .Users}}
<tr>
<td>{{.ID}}</td>
<td>{{.Name}}</td>
<td>{{.Email}}</td>
<td><span class="role {{.Role}}">{{.Role}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<!-- templates/about.html -->
{{define "content"}}
<div class="about-page">
<h1>About {{.Config.App.Name}}</h1>
<div class="info-grid">
<div class="info-card">
<h3>Application Info</h3>
<ul>
<li><strong>Name:</strong> {{.Config.App.Name}}</li>
<li><strong>Version:</strong> {{.Config.App.Version}}</li>
<li><strong>Debug Mode:</strong> {{.Config.App.Debug}}</li>
</ul>
</div>
<div class="info-card">
<h3>Server Info</h3>
<ul>
<li><strong>Host:</strong> {{.Config.Server.Host}}</li>
<li><strong>Port:</strong> {{.Config.Server.Port}}</li>
</ul>
</div>
</div>
<div class="features">
<h2>Features</h2>
<ul>
<li>Embedded file system for static resources</li>
<li>Template-based rendering</li>
<li>JSON API endpoints</li>
<li>Responsive design</li>
</ul>
</div>
</div>
{{end}}
<!-- templates/404.html -->
{{define "content"}}
<div class="error-page">
<h1>404 - Page Not Found</h1>
<p>The page <code>{{.Path}}</code> could not be found.</p>
<a href="/" class="btn btn-primary">Go Home</a>
</div>
{{end}}
CSS 样式文件 #
/* static/css/style.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.navbar {
background: #2c3e50;
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-brand {
font-size: 1.5rem;
font-weight: bold;
}
.nav-links {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-links a {
color: white;
text-decoration: none;
transition: color 0.3s;
}
.nav-links a:hover {
color: #3498db;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
min-height: calc(100vh - 120px);
}
.hero {
text-align: center;
padding: 3rem 0;
background: white;
border-radius: 8px;
margin-bottom: 2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.hero h1 {
color: #2c3e50;
margin-bottom: 1rem;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #3498db;
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.user-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.user-card h4 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.role {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
text-transform: uppercase;
}
.role.admin {
background: #e74c3c;
color: white;
}
.role.user {
background: #3498db;
color: white;
}
.users-table {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.users-table table {
width: 100%;
border-collapse: collapse;
}
.users-table th,
.users-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
.users-table th {
background: #f8f9fa;
font-weight: bold;
color: #2c3e50;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 1rem;
transition: background-color 0.3s;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 2rem;
}
.info-card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.info-card h3 {
color: #2c3e50;
margin-bottom: 1rem;
}
.info-card ul {
list-style: none;
}
.info-card li {
padding: 0.5rem 0;
border-bottom: 1px solid #eee;
}
.features {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.features h2 {
color: #2c3e50;
margin-bottom: 1rem;
}
.features ul {
list-style-type: disc;
margin-left: 2rem;
}
.features li {
padding: 0.5rem 0;
}
.error-page {
text-align: center;
padding: 3rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.error-page h1 {
color: #e74c3c;
margin-bottom: 1rem;
}
.error-page code {
background: #f8f9fa;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: "Courier New", monospace;
}
.footer {
background: #2c3e50;
color: white;
text-align: center;
padding: 1rem;
margin-top: auto;
}
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
}
.nav-links {
gap: 1rem;
}
.container {
padding: 1rem;
}
.page-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
}
JavaScript 文件 #
// static/js/app.js
document.addEventListener("DOMContentLoaded", function () {
console.log("Embedded Web App loaded");
// 添加导航高亮
highlightCurrentNav();
// 添加表格交互
addTableInteractions();
// 添加统计卡片动画
animateStatCards();
});
function highlightCurrentNav() {
const currentPath = window.location.pathname;
const navLinks = document.querySelectorAll(".nav-links a");
navLinks.forEach((link) => {
if (link.getAttribute("href") === currentPath) {
link.style.color = "#3498db";
link.style.fontWeight = "bold";
}
});
}
function addTableInteractions() {
const tableRows = document.querySelectorAll(".users-table tbody tr");
tableRows.forEach((row) => {
row.addEventListener("mouseenter", function () {
this.style.backgroundColor = "#f8f9fa";
});
row.addEventListener("mouseleave", function () {
this.style.backgroundColor = "";
});
row.addEventListener("click", function () {
const userId = this.cells[0].textContent;
showUserDetails(userId);
});
});
}
function animateStatCards() {
const statCards = document.querySelectorAll(".stat-card");
statCards.forEach((card, index) => {
card.style.opacity = "0";
card.style.transform = "translateY(20px)";
setTimeout(() => {
card.style.transition = "opacity 0.5s, transform 0.5s";
card.style.opacity = "1";
card.style.transform = "translateY(0)";
}, index * 100);
});
}
function showUserDetails(userId) {
alert(
`User ID: ${userId}\nClick OK to view details (feature not implemented)`
);
}
async function loadUsers() {
try {
const response = await fetch("/api/users");
const users = await response.json();
const tbody = document.getElementById("users-tbody");
if (tbody) {
tbody.innerHTML = users
.map(
(user) => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
<td><span class="role ${user.role}">${user.role}</span></td>
</tr>
`
)
.join("");
// 重新添加表格交互
addTableInteractions();
}
showNotification("Users refreshed successfully", "success");
} catch (error) {
console.error("Failed to load users:", error);
showNotification("Failed to refresh users", "error");
}
}
function showNotification(message, type) {
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 2rem;
border-radius: 4px;
color: white;
font-weight: bold;
z-index: 1000;
transition: opacity 0.3s;
${type === "success" ? "background: #27ae60;" : "background: #e74c3c;"}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = "0";
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// API 工具函数
async function fetchStats() {
try {
const response = await fetch("/api/stats");
return await response.json();
} catch (error) {
console.error("Failed to fetch stats:", error);
return null;
}
}
async function fetchConfig() {
try {
const response = await fetch("/api/config");
return await response.json();
} catch (error) {
console.error("Failed to fetch config:", error);
return null;
}
}
// 导出函数供全局使用
window.loadUsers = loadUsers;
window.fetchStats = fetchStats;
window.fetchConfig = fetchConfig;
嵌入文件系统的性能考虑 #
内存使用优化 #
package main
import (
"embed"
"io"
"io/fs"
"net/http"
"strings"
)
// 大文件应该使用 embed.FS 而不是 []byte
//go:embed large-files/*
var largeFilesFS embed.FS
// 小文件可以直接嵌入为字节数组
//go:embed small-config.json
var smallConfig []byte
// 优化的文件服务器
type OptimizedFileServer struct {
fs embed.FS
}
func (ofs *OptimizedFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// 检查文件是否存在
file, err := ofs.fs.Open(path)
if err != nil {
http.NotFound(w, r)
return
}
defer file.Close()
// 获取文件信息
stat, err := file.Stat()
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// 设置适当的 Content-Type
if strings.HasSuffix(path, ".css") {
w.Header().Set("Content-Type", "text/css")
} else if strings.HasSuffix(path, ".js") {
w.Header().Set("Content-Type", "application/javascript")
} else if strings.HasSuffix(path, ".html") {
w.Header().Set("Content-Type", "text/html")
}
// 设置缓存头
w.Header().Set("Cache-Control", "public, max-age=3600")
// 使用 ServeContent 支持范围请求和条件请求
http.ServeContent(w, r, stat.Name(), stat.ModTime(), file.(io.ReadSeeker))
}
条件编译优化 #
// +build !embed
package assets
import "net/http"
// 开发模式:从文件系统读取
func GetFileSystem() http.FileSystem {
return http.Dir("./static")
}
// +build embed
package assets
import (
"embed"
"net/http"
)
//go:embed static/*
var staticFS embed.FS
// 生产模式:使用嵌入文件系统
func GetFileSystem() http.FileSystem {
return http.FS(staticFS)
}
压缩优化 #
package main
import (
"compress/gzip"
"embed"
"io"
"net/http"
"strings"
)
//go:embed compressed/*.gz
var compressedFS embed.FS
type CompressedFileServer struct {
fs embed.FS
}
func (cfs *CompressedFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
// 检查客户端是否支持 gzip
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
gzipPath := "compressed/" + path + ".gz"
if file, err := cfs.fs.Open(gzipPath); err == nil {
defer file.Close()
w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Type", getContentType(path))
io.Copy(w, file)
return
}
}
// 回退到未压缩版本
http.FileServer(http.FS(cfs.fs)).ServeHTTP(w, r)
}
func getContentType(path string) string {
switch {
case strings.HasSuffix(path, ".css"):
return "text/css"
case strings.HasSuffix(path, ".js"):
return "application/javascript"
case strings.HasSuffix(path, ".html"):
return "text/html"
case strings.HasSuffix(path, ".json"):
return "application/json"
default:
return "application/octet-stream"
}
}
实际项目中的应用场景 #
CLI 工具的资源嵌入 #
package main
import (
"embed"
"fmt"
"text/template"
)
//go:embed templates/*.tmpl
var templateFS embed.FS
//go:embed help/*.txt
var helpFS embed.FS
type CLITool struct {
templates *template.Template
}
func NewCLITool() (*CLITool, error) {
templates, err := template.ParseFS(templateFS, "templates/*.tmpl")
if err != nil {
return nil, err
}
return &CLITool{templates: templates}, nil
}
func (cli *CLITool) GenerateConfig(name string) error {
data := struct {
ProjectName string
Version string
}{
ProjectName: name,
Version: "1.0.0",
}
return cli.templates.ExecuteTemplate(os.Stdout, "config.tmpl", data)
}
func (cli *CLITool) ShowHelp(topic string) error {
helpFile := fmt.Sprintf("help/%s.txt", topic)
content, err := helpFS.ReadFile(helpFile)
if err != nil {
return fmt.Errorf("help topic %q not found", topic)
}
fmt.Print(string(content))
return nil
}
数据库迁移脚本嵌入 #
package migration
import (
"embed"
"fmt"
"path/filepath"
"sort"
"strconv"
"strings"
)
//go:embed migrations/*.sql
var migrationFS embed.FS
type Migration struct {
Version int
Name string
Content string
}
func LoadMigrations() ([]Migration, error) {
entries, err := migrationFS.ReadDir("migrations")
if err != nil {
return nil, err
}
var migrations []Migration
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
continue
}
// 解析文件名:001_create_users.sql
parts := strings.SplitN(entry.Name(), "_", 2)
if len(parts) != 2 {
continue
}
version, err := strconv.Atoi(parts[0])
if err != nil {
continue
}
name := strings.TrimSuffix(parts[1], ".sql")
content, err := migrationFS.ReadFile(filepath.Join("migrations", entry.Name()))
if err != nil {
return nil, err
}
migrations = append(migrations, Migration{
Version: version,
Name: name,
Content: string(content),
})
}
// 按版本号排序
sort.Slice(migrations, func(i, j int) bool {
return migrations[i].Version < migrations[j].Version
})
return migrations, nil
}
func (m Migration) String() string {
return fmt.Sprintf("Migration %03d: %s", m.Version, m.Name)
}
多语言支持 #
package i18n
import (
"embed"
"encoding/json"
"fmt"
"path/filepath"
)
//go:embed locales/*.json
var localesFS embed.FS
type Localizer struct {
messages map[string]map[string]string
}
func NewLocalizer() (*Localizer, error) {
l := &Localizer{
messages: make(map[string]map[string]string),
}
entries, err := localesFS.ReadDir("locales")
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
locale := strings.TrimSuffix(entry.Name(), ".json")
content, err := localesFS.ReadFile(filepath.Join("locales", entry.Name()))
if err != nil {
return nil, err
}
var messages map[string]string
if err := json.Unmarshal(content, &messages); err != nil {
return nil, err
}
l.messages[locale] = messages
}
return l, nil
}
func (l *Localizer) Get(locale, key string, args ...interface{}) string {
if messages, ok := l.messages[locale]; ok {
if message, ok := messages[key]; ok {
return fmt.Sprintf(message, args...)
}
}
// 回退到英语
if locale != "en" {
return l.Get("en", key, args...)
}
return key // 最后回退到键名
}
func (l *Localizer) GetAvailableLocales() []string {
var locales []string
for locale := range l.messages {
locales = append(locales, locale)
}
return locales
}
构建和部署优化 #
#!/bin/bash
# build.sh
set -e
echo "Building embedded web application..."
# 压缩静态资源
echo "Compressing static files..."
mkdir -p static/compressed
find static -name "*.css" -o -name "*.js" -o -name "*.html" | while read file; do
gzip -c "$file" > "static/compressed/$(basename "$file").gz"
done
# 构建应用
echo "Building Go application..."
go build -ldflags="-s -w" -o webapp main.go
echo "Build completed!"
echo "Executable size: $(du -h webapp | cut -f1)"
通过本节的学习,您应该已经掌握了 Go 嵌入文件系统的核心概念和实际应用。嵌入文件系统是现代 Go 应用开发的重要特性,能够简化部署流程并提高应用的可移植性。在下一节中,我们将学习 Go 编译与构建优化的相关技巧。