最近在學習Go語言,看到了Embed,我突然覺得把web資源放到Go編譯好的二進制文件中去。所有就讓AI給我寫了下面4個程序。

一、先準備vue3+vite的程序

vue3+vite的編寫的前端代碼完成後編譯dist文件夾,如下圖:

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_json

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_json_02

通過Nginx部署後效果如下:

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_json_03


二、四個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編譯後程序大小如下圖:

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_靜態文件_04

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_靜態文件_05

(一)、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))
}

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_靜態文件_06

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_html_07


(二)、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))
}

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_靜態文件_08

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_json_09


(三)、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("?? 服務器已關閉")
}

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_html_10

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_json_11



(四)、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("👋 應用已關閉")
}

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_html_12

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_靜態文件_13

Go語言Embed把vue3編寫的前端內嵌到Go的程序中去_靜態文件_14