最近在學習Go語言,看到了Embed,我突然覺得把web資源放到Go編譯好的二進制文件中去。所有就讓AI給我寫了下面4個程序。
一、先準備vue3+vite的程序
vue3+vite的編寫的前端代碼完成後編譯dist文件夾,如下圖:
通過Nginx部署後效果如下:
二、四個Go語言程序內嵌人dist目錄
|--g01.go
|--g02.go
|--g03.go
|--g04.go
|--dist/index.html
|--dist/vite.svg
|--dist/assets/
|--dist/assets/401-BTxIa4pz.css
|--dist/assets/401-BTxIa4pz.js
|--dist/assets/a*.css
|--dist/assets/a*.js
|--dist/assets/a*.*
我這g01,g02,g03都是在Linux下測試。g04我是在windows下測試。go編譯後程序大小如下圖:
(一)、g01.go
package main
import (
"embed"
"fmt"
"io/fs"
"log"
"net/http"
"os"
)
// 嵌入Vue3構建的dist目錄
//go:embed dist/*
var distFS embed.FS
func main() {
// 獲取嵌入的文件系統
staticFS, err := fs.Sub(distFS, "dist")
if err != nil {
log.Fatal("獲取靜態文件系統失敗:", err)
}
// 創建文件服務器
fileServer := http.FileServer(http.FS(staticFS))
// 設置路由
http.Handle("/", fileServer)
// 啓動服務器
port := "6081"
if p := os.Getenv("PORT"); p != "" {
port = p
}
fmt.Printf("Vue3應用已啓動,訪問地址: http://localhost:%s\n", port)
fmt.Println("按 Ctrl+C 退出")
log.Fatal(http.ListenAndServe(":"+port, nil))
}
(二)、g02.go
package main
import (
"embed"
"fmt"
"io"
"io/fs"
"log"
"mime"
"net/http"
"os"
"path"
"path/filepath"
"strings"
)
//go:embed dist/*
var distFS embed.FS
// SPAHandler 處理單頁應用路由
type SPAHandler struct {
staticFS fs.FS
indexBytes []byte
}
func NewSPAHandler() (*SPAHandler, error) {
// 獲取靜態文件系統
staticFS, err := fs.Sub(distFS, "dist")
if err != nil {
return nil, fmt.Errorf("獲取靜態文件系統失敗: %v", err)
}
// 讀取index.html內容
indexFile, err := staticFS.Open("index.html")
if err != nil {
return nil, fmt.Errorf("打開index.html失敗: %v", err)
}
defer indexFile.Close()
indexBytes, err := io.ReadAll(indexFile)
if err != nil {
return nil, fmt.Errorf("讀取index.html失敗: %v", err)
}
return &SPAHandler{
staticFS: staticFS,
indexBytes: indexBytes,
}, nil
}
func (h *SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 清理路徑
cleanPath := path.Clean(r.URL.Path)
if cleanPath == "/" {
cleanPath = "/index.html"
}
// 嘗試打開文件
file, err := h.staticFS.Open(strings.TrimPrefix(cleanPath, "/"))
if err != nil {
// 文件不存在,返回index.html (SPA路由)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(h.indexBytes)
return
}
defer file.Close()
// 獲取文件信息
stat, err := file.Stat()
if err != nil {
http.Error(w, "獲取文件信息失敗", http.StatusInternalServerError)
return
}
// 如果是目錄,返回index.html
if stat.IsDir() {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(h.indexBytes)
return
}
// 設置Content-Type
ext := filepath.Ext(cleanPath)
if contentType := mime.TypeByExtension(ext); contentType != "" {
w.Header().Set("Content-Type", contentType)
}
// 設置緩存頭
if strings.HasPrefix(cleanPath, "/assets/") ||
strings.HasPrefix(cleanPath, "/static/") {
w.Header().Set("Cache-Control", "public, max-age=31536000") // 1年
} else {
w.Header().Set("Cache-Control", "no-cache")
}
// 返回文件內容
http.ServeContent(w, r, cleanPath, stat.ModTime(), file.(io.ReadSeeker))
}
func main() {
handler, err := NewSPAHandler()
if err != nil {
log.Fatal("創建SPA處理器失敗:", err)
}
// 設置路由
http.Handle("/", handler)
// 啓動服務器
port := "6082"
if p := os.Getenv("PORT"); p != "" {
port = p
}
fmt.Printf("Vue3 SPA應用已啓動,訪問地址: http://localhost:%s\n", port)
fmt.Println("支持前端路由,按 Ctrl+C 退出")
log.Fatal(http.ListenAndServe(":"+port, nil))
}
(三)、g03.go
package main
import (
"context"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"path"
"path/filepath"
"strings"
"syscall"
"time"
)
//go:embed dist/*
var distFS embed.FS
// WebApp Web應用結構
type WebApp struct {
server *http.Server
staticFS fs.FS
indexHTML []byte
}
// APIResponse API響應結構
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func NewWebApp(port string) (*WebApp, error) {
// 獲取靜態文件系統
staticFS, err := fs.Sub(distFS, "dist")
if err != nil {
return nil, fmt.Errorf("獲取靜態文件系統失敗: %v", err)
}
// 讀取index.html
indexFile, err := staticFS.Open("index.html")
if err != nil {
return nil, fmt.Errorf("打開index.html失敗: %v", err)
}
defer indexFile.Close()
indexHTML, err := io.ReadAll(indexFile)
if err != nil {
return nil, fmt.Errorf("讀取index.html失敗: %v", err)
}
app := &WebApp{
staticFS: staticFS,
indexHTML: indexHTML,
}
// 創建HTTP服務器
mux := http.NewServeMux()
// API路由
mux.HandleFunc("/api/", app.handleAPI)
// 靜態文件路由
mux.HandleFunc("/", app.handleStatic)
app.server = &http.Server{
Addr: ":" + port,
Handler: app.corsMiddleware(app.loggingMiddleware(mux)),
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
return app, nil
}
// CORS中間件
func (app *WebApp) corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// 日誌中間件
func (app *WebApp) loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 創建響應記錄器
rr := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rr, r)
duration := time.Since(start)
log.Printf("%s %s %d %v %s",
r.Method,
r.URL.Path,
rr.statusCode,
duration,
r.UserAgent())
})
}
// 響應記錄器
type responseRecorder struct {
http.ResponseWriter
statusCode int
}
func (rr *responseRecorder) WriteHeader(code int) {
rr.statusCode = code
rr.ResponseWriter.WriteHeader(code)
}
// 處理API請求
func (app *WebApp) handleAPI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// 去掉/api前綴
apiPath := strings.TrimPrefix(r.URL.Path, "/api")
switch {
case apiPath == "/health":
app.handleHealth(w, r)
case apiPath == "/info":
app.handleInfo(w, r)
case strings.HasPrefix(apiPath, "/data"):
app.handleData(w, r)
default:
app.sendJSONResponse(w, http.StatusNotFound, APIResponse{
Success: false,
Message: "API端點不存在",
})
}
}
// 健康檢查
func (app *WebApp) handleHealth(w http.ResponseWriter, r *http.Request) {
app.sendJSONResponse(w, http.StatusOK, APIResponse{
Success: true,
Message: "服務正常運行",
Data: map[string]interface{}{
"timestamp": time.Now().Unix(),
"uptime": time.Since(startTime).String(),
},
})
}
// 應用信息
func (app *WebApp) handleInfo(w http.ResponseWriter, r *http.Request) {
app.sendJSONResponse(w, http.StatusOK, APIResponse{
Success: true,
Message: "應用信息",
Data: map[string]interface{}{
"name": "Vue3 + Go 嵌入式應用",
"version": "1.0.0",
"author": "Your Name",
"build": time.Now().Format("2006-01-02 15:04:05"),
},
})
}
// 數據處理
func (app *WebApp) handleData(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
// 返回示例數據
data := []map[string]interface{}{
{"id": 1, "name": "項目1", "status": "進行中"},
{"id": 2, "name": "項目2", "status": "已完成"},
{"id": 3, "name": "項目3", "status": "計劃中"},
}
app.sendJSONResponse(w, http.StatusOK, APIResponse{
Success: true,
Message: "獲取數據成功",
Data: data,
})
case "POST":
// 處理POST請求
var requestData map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
app.sendJSONResponse(w, http.StatusBadRequest, APIResponse{
Success: false,
Message: "請求數據格式錯誤",
})
return
}
app.sendJSONResponse(w, http.StatusOK, APIResponse{
Success: true,
Message: "數據處理成功",
Data: requestData,
})
default:
app.sendJSONResponse(w, http.StatusMethodNotAllowed, APIResponse{
Success: false,
Message: "不支持的請求方法",
})
}
}
// 發送JSON響應
func (app *WebApp) sendJSONResponse(w http.ResponseWriter, statusCode int, response APIResponse) {
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(response)
}
// 處理靜態文件
func (app *WebApp) handleStatic(w http.ResponseWriter, r *http.Request) {
// 清理路徑
cleanPath := path.Clean(r.URL.Path)
if cleanPath == "/" {
cleanPath = "/index.html"
}
// 嘗試打開文件
filePath := strings.TrimPrefix(cleanPath, "/")
file, err := app.staticFS.Open(filePath)
if err != nil {
// 文件不存在,返回index.html (SPA路由)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(app.indexHTML)
return
}
defer file.Close()
// 獲取文件信息
stat, err := file.Stat()
if err != nil {
http.Error(w, "獲取文件信息失敗", http.StatusInternalServerError)
return
}
// 如果是目錄,返回index.html
if stat.IsDir() {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(app.indexHTML)
return
}
// 設置Content-Type和緩存
app.setHeaders(w, cleanPath)
// 返回文件內容
http.ServeContent(w, r, cleanPath, stat.ModTime(), file.(io.ReadSeeker))
}
// 設置HTTP頭
func (app *WebApp) setHeaders(w http.ResponseWriter, filePath string) {
ext := filepath.Ext(filePath)
// 設置Content-Type
switch ext {
case ".html":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
case ".css":
w.Header().Set("Content-Type", "text/css; charset=utf-8")
case ".js":
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
case ".json":
w.Header().Set("Content-Type", "application/json; charset=utf-8")
case ".png":
w.Header().Set("Content-Type", "image/png")
case ".jpg", ".jpeg":
w.Header().Set("Content-Type", "image/jpeg")
case ".gif":
w.Header().Set("Content-Type", "image/gif")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".ico":
w.Header().Set("Content-Type", "image/x-icon")
case ".woff":
w.Header().Set("Content-Type", "font/woff")
case ".woff2":
w.Header().Set("Content-Type", "font/woff2")
case ".ttf":
w.Header().Set("Content-Type", "font/ttf")
}
// 設置緩存策略
if strings.HasPrefix(filePath, "/assets/") ||
strings.HasPrefix(filePath, "/static/") ||
ext == ".css" || ext == ".js" {
// 靜態資源長期緩存
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("Expires", time.Now().AddDate(1, 0, 0).Format(http.TimeFormat))
} else if ext == ".html" {
// HTML文件不緩存
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
} else {
// 其他文件短期緩存
w.Header().Set("Cache-Control", "public, max-age=3600")
}
}
// 啓動應用
func (app *WebApp) Start() error {
log.Printf("啓動Web服務器,地址: http://localhost%s", app.server.Addr)
return app.server.ListenAndServe()
}
// 優雅關閉
func (app *WebApp) Shutdown(ctx context.Context) error {
log.Println("正在關閉Web服務器...")
return app.server.Shutdown(ctx)
}
var startTime = time.Now()
func main() {
// 獲取端口
port := "6083"
if p := os.Getenv("PORT"); p != "" {
port = p
}
// 創建應用
app, err := NewWebApp(port)
if err != nil {
log.Fatal("創建Web應用失敗:", err)
}
// 設置信號處理
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 啓動服務器
go func() {
if err := app.Start(); err != nil && err != http.ErrServerClosed {
log.Fatal("啓動服務器失敗:", err)
}
}()
fmt.Printf("?? Vue3應用已啓動!\n")
fmt.Printf("?? 訪問地址: http://localhost:%s\n", port)
fmt.Printf("?? API端點: http://localhost:%s/api/\n", port)
fmt.Printf("?? 按 Ctrl+C 退出\n\n")
// 等待退出信號
<-quit
// 優雅關閉
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := app.Shutdown(ctx); err != nil {
log.Fatal("關閉服務器失敗:", err)
}
fmt.Println("?? 服務器已關閉")
}
(四)、g04.go
package main
import (
"context"
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
"time"
)
//go:embed dist/*
var distFS embed.FS
// DesktopApp 桌面應用
type DesktopApp struct {
server *http.Server
port string
autoOpen bool
}
func NewDesktopApp() (*DesktopApp, error) {
// 查找可用端口
port, err := findAvailablePort()
if err != nil {
return nil, fmt.Errorf("查找可用端口失敗: %v", err)
}
app := &DesktopApp{
port: port,
autoOpen: true,
}
// 設置路由
mux := http.NewServeMux()
// 靜態文件處理
staticFS, err := fs.Sub(distFS, "dist")
if err != nil {
return nil, fmt.Errorf("獲取靜態文件系統失敗: %v", err)
}
// 創建SPA處理器
spaHandler := &SPAHandler{staticFS: staticFS}
if err := spaHandler.loadIndex(); err != nil {
return nil, err
}
mux.Handle("/", spaHandler)
// API路由
mux.HandleFunc("/api/quit", app.handleQuit)
mux.HandleFunc("/api/minimize", app.handleMinimize)
mux.HandleFunc("/api/system", app.handleSystemInfo)
app.server = &http.Server{
Addr: "127.0.0.1:" + port,
Handler: mux,
}
return app, nil
}
// SPA處理器
type SPAHandler struct {
staticFS fs.FS
indexHTML []byte
}
func (h *SPAHandler) loadIndex() error {
indexFile, err := h.staticFS.Open("index.html")
if err != nil {
return fmt.Errorf("打開index.html失敗: %v", err)
}
defer indexFile.Close()
indexBytes, err := io.ReadAll(indexFile)
if err != nil {
return fmt.Errorf("讀取index.html失敗: %v", err)
}
h.indexHTML = indexBytes
return nil
}
func (h *SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 處理靜態文件
filePath := r.URL.Path
if filePath == "/" {
filePath = "/index.html"
}
// 嘗試打開文件
file, err := h.staticFS.Open(filePath[1:])
if err != nil {
// 文件不存在,返回index.html
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(h.indexHTML)
return
}
defer file.Close()
// 獲取文件信息
stat, err := file.Stat()
if err != nil || stat.IsDir() {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(h.indexHTML)
return
}
// 返回文件
http.ServeContent(w, r, filePath, stat.ModTime(), file.(io.ReadSeeker))
}
// 查找可用端口
func findAvailablePort() (string, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
defer listener.Close()
addr := listener.Addr().(*net.TCPAddr)
return fmt.Sprintf("%d", addr.Port), nil
}
// 打開瀏覽器
func (app *DesktopApp) openBrowser() error {
url := fmt.Sprintf("http://127.0.0.1:%s", app.port)
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
case "darwin":
cmd = exec.Command("open", url)
case "linux":
cmd = exec.Command("xdg-open", url)
default:
return fmt.Errorf("不支持的操作系統: %s", runtime.GOOS)
}
return cmd.Start()
}
// 處理退出請求
func (app *DesktopApp) handleQuit(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"success": true, "message": "應用即將退出"}`))
// 延遲退出,讓響應返回
go func() {
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
}
// 處理最小化請求
func (app *DesktopApp) handleMinimize(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"success": true, "message": "最小化功能需要桌面集成"}`))
}
// 處理系統信息請求
func (app *DesktopApp) handleSystemInfo(w http.ResponseWriter, r *http.Request) {
info := map[string]interface{}{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
"version": runtime.Version(),
"numCPU": runtime.NumCPU(),
"hostname": getHostname(),
"port": app.port,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
"data": info,
})
}
func getHostname() string {
hostname, err := os.Hostname()
if err != nil {
return "unknown"
}
return hostname
}
// 啓動應用
func (app *DesktopApp) Start() error {
// 啓動HTTP服務器
go func() {
log.Printf("啓動本地服務器: http://127.0.0.1:%s", app.port)
if err := app.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal("服務器啓動失敗:", err)
}
}()
// 等待服務器啓動
time.Sleep(500 * time.Millisecond)
// 自動打開瀏覽器
if app.autoOpen {
if err := app.openBrowser(); err != nil {
log.Printf("自動打開瀏覽器失敗: %v", err)
fmt.Printf("請手動訪問: http://127.0.0.1:%s\n", app.port)
} else {
fmt.Printf("應用已在瀏覽器中打開: http://127.0.0.1:%s\n", app.port)
}
}
return nil
}
// 優雅關閉
func (app *DesktopApp) Shutdown(ctx context.Context) error {
return app.server.Shutdown(ctx)
}
func main() {
fmt.Println("🚀 啓動Vue3桌面應用...")
// 創建應用
app, err := NewDesktopApp()
if err != nil {
log.Fatal("創建應用失敗:", err)
}
// 設置信號處理
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 啓動應用
if err := app.Start(); err != nil {
log.Fatal("啓動應用失敗:", err)
}
fmt.Println("✅ 應用啓動成功!")
fmt.Println("💡 按 Ctrl+C 退出應用")
// 等待退出信號
<-quit
// 優雅關閉
fmt.Println("\n🔄 正在關閉應用...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.Shutdown(ctx); err != nil {
log.Printf("關閉應用失敗: %v", err)
}
fmt.Println("👋 應用已關閉")
}