歡迎來到 Jack’s Blog 👋

分享技術學習、生活體驗與家庭時光的個人空間。

探索 技術筆記生活隨筆家庭記錄

Go 語言系列(十八):Kubernetes 部署

這是 Go 語言從零到 Web 應用系列的第十八篇,也是本系列的最終篇。我們將把前面建構的 Go 應用部署到 Kubernetes 上,學習容器編排的核心概念。 Kubernetes 基礎概念 核心元件 ┌─────────────────────────────────────────┐ │ Kubernetes Cluster │ │ │ │ ┌────────────────────────────────────┐ │ │ │ Namespace: default │ │ │ │ │ │ │ │ ┌──────────┐ ┌──────────┐ │ │ │ │ │Deployment│ │ Service │ │ │ │ │ │ │ │(ClusterIP│ │ │ │ │ │ ┌──────┐ │ │ or LB) │ │ │ │ │ │ │ Pod │ │←──│ │ │ │ │ │ │ │┌────┐│ │ └──────────┘ │ │ │ │ │ ││容器││ │ │ │ │ │ │ │└────┘│ │ │ │ │ │ │ └──────┘ │ │ │ │ │ └──────────┘ │ │ │ └────────────────────────────────────┘ │ └─────────────────────────────────────────┘ 元件 說明 Pod 最小部署單位,一個或多個容器的組合 Deployment 管理 Pod 的副本數、更新策略 Service 為 Pod 提供穩定的網路端點 Namespace 資源的邏輯隔離 ConfigMap 配置資料(非機密) Secret 機密資料(密碼、token) 本地環境設定 使用 minikube # 安裝 brew install minikube # 啟動叢集 minikube start # 確認狀態 kubectl cluster-info kubectl get nodes 使用 kind(Kubernetes in Docker) # 安裝 brew install kind # 建立叢集 kind create cluster --name bookstore # 確認 kubectl cluster-info --context kind-bookstore kind 更輕量,適合 CI/CD 和快速測試。 ...

2026年2月24日 · 5 min · 971 words · Jack

Go 語言系列(十七):微服務架構

這是 Go 語言從零到 Web 應用系列的第十七篇。前面我們學了 REST API 和 gRPC 這兩種通訊方式,現在來學如何用微服務架構組織大型系統。 單體 vs 微服務 單體架構(Monolith) 所有功能在一個程式中: ┌─────────────────────────┐ │ 單體應用 │ │ ┌─────┐ ┌─────┐ ┌───┐ │ │ │用戶 │ │訂單 │ │商品│ │ │ └──┬──┘ └──┬──┘ └─┬─┘ │ │ └───────┼──────┘ │ │ ┌──┴──┐ │ │ │ DB │ │ │ └─────┘ │ └─────────────────────────┘ 微服務架構 每個功能是獨立的服務: ┌──────┐ ┌──────┐ ┌──────┐ │用戶 │ │訂單 │ │商品 │ │服務 │ │服務 │ │服務 │ └──┬───┘ └──┬───┘ └──┬───┘ │ │ │ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ │Users│ │Orders│ │Products│ │ DB │ │ DB │ │ DB │ └─────┘ └─────┘ └──────┘ 對比 面向 單體 微服務 開發速度(初期) 快 慢(基礎設施多) 部署 整包部署 獨立部署 擴展 整體擴展 按需擴展個別服務 技術選型 統一 每個服務可用不同技術 團隊組織 單一團隊 各服務獨立團隊 複雜度 程式碼內部 分散式系統 故障影響 全域 局部(做好隔離時) 什麼時候該拆微服務? 不要一開始就用微服務。 先用單體,當出現以下訊號時再考慮拆分: ...

2026年2月24日 · 4 min · 778 words · Jack

Go 語言系列(十六):gRPC 入門

這是 Go 語言從零到 Web 應用系列的第十六篇。前面我們用 REST API 建構服務,現在來學 Go 的另一個強項——gRPC,了解為什麼它在微服務架構中如此受歡迎。 gRPC vs REST 面向 REST gRPC 協議 HTTP/1.1(通常) HTTP/2 資料格式 JSON(文字) Protocol Buffers(二進位) 型別安全 弱(需要手動解析) 強(自動產生型別) 串流 不原生支持 原生支持雙向串流 效能 較慢(JSON 解析) 快 3-10 倍 瀏覽器支持 原生支持 需要 gRPC-Web 適用場景 公開 API、前端 微服務間通訊 什麼時候用 gRPC: 微服務之間的內部通訊 需要高效能和低延遲 需要串流(streaming) 強型別的服務契約 Protocol Buffers 安裝 # 安裝 protoc 編譯器 brew install protobuf # macOS # 安裝 Go 的 protoc plugin go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 確保 $GOPATH/bin 在你的 PATH 中。 ...

2026年2月24日 · 5 min · 917 words · Jack

Go 語言系列(十五):PostgreSQL 進階

這是 Go 語言從零到 Web 應用系列的第十五篇。前面我們用 SQLite 作為開發資料庫,現在升級到生產環境常用的 PostgreSQL,學習進階的資料庫功能。 用 Docker Compose 啟動 PostgreSQL 不需要在本地安裝 PostgreSQL,用 Docker 最方便: # docker-compose.yml version: '3.8' services: postgres: image: postgres:16-alpine ports: - "5432:5432" environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: bookstore volumes: - pgdata:/var/lib/postgresql/data volumes: pgdata: docker compose up -d 連線字串:postgres://app:secret@localhost:5432/bookstore?sslmode=disable pgx Driver pgx 是目前 Go 生態中效能最好的 PostgreSQL driver,比傳統的 lib/pq 快且功能更完整。 安裝: go get github.com/jackc/pgx/v5 go get github.com/jackc/pgx/v5/pgxpool 基本連線 package main import ( "context" "fmt" "log" "github.com/jackc/pgx/v5/pgxpool" ) func main() { ctx := context.Background() // pgxpool 自帶連線池 pool, err := pgxpool.New(ctx, "postgres://app:secret@localhost:5432/bookstore?sslmode=disable") if err != nil { log.Fatal(err) } defer pool.Close() // 測試連線 if err := pool.Ping(ctx); err != nil { log.Fatal(err) } fmt.Println("Connected to PostgreSQL!") } 搭配 database/sql 使用 如果你想保持用 database/sql 的介面(例如搭配 sqlx): ...

2026年2月24日 · 4 min · 692 words · Jack

Go 語言系列(十四):ORM 與資料庫工具 — GORM vs sqlx

這是 Go 語言從零到 Web 應用系列的第十四篇。前面我們用 database/sql 手寫 SQL 操作資料庫,現在來看看 ORM 和資料庫工具如何簡化這些工作。 ORM vs 原生 SQL 面向 原生 SQL (database/sql) ORM (GORM) SQL-first (sqlx) 學習曲線 需要懂 SQL 可以少寫 SQL 需要懂 SQL 開發速度 較慢(模板程式碼多) 快(自動產生 SQL) 中等 SQL 控制力 完全控制 有限(複雜查詢困難) 完全控制 效能 最佳 有反射開銷 接近原生 Migration 需自行處理 內建 AutoMigrate 需自行處理 GORM 安裝: go get -u gorm.io/gorm go get -u gorm.io/driver/sqlite # 或 postgres、mysql Model 定義 package main import ( "time" "gorm.io/gorm" ) type User struct { ID uint `gorm:"primaryKey" json:"id"` Name string `gorm:"size:100;not null" json:"name"` Email string `gorm:"uniqueIndex;not null" json:"email"` Books []Book `gorm:"foreignKey:UserID" json:"books"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 軟刪除 } type Book struct { ID uint `gorm:"primaryKey" json:"id"` Title string `gorm:"size:200;not null" json:"title"` Author string `gorm:"size:100" json:"author"` UserID uint `json:"user_id"` } 連線與 AutoMigrate import ( "gorm.io/driver/sqlite" "gorm.io/gorm" ) func main() { db, err := gorm.Open(sqlite.Open("app.db"), &gorm.Config{}) if err != nil { panic("failed to connect database") } // AutoMigrate 自動建立或更新表結構 db.AutoMigrate(&User{}, &Book{}) } CRUD 操作 // Create user := User{Name: "Jack", Email: "jack@example.com"} result := db.Create(&user) // result.RowsAffected = 1 // user.ID 自動填入 // Read var u User db.First(&u, 1) // 找 ID=1 db.Where("email = ?", "jack@example.com").First(&u) // 條件查詢 // 查詢多筆 var users []User db.Where("name LIKE ?", "%Jack%").Find(&users) // Update db.Model(&user).Update("name", "Jack Tse") db.Model(&user).Updates(User{Name: "Jack Tse", Email: "new@example.com"}) // Delete(軟刪除,因為 User 有 DeletedAt 欄位) db.Delete(&user, 1) // 真正刪除 db.Unscoped().Delete(&user, 1) 關聯查詢 // 建立使用者和書籍 db.Create(&User{ Name: "Jack", Email: "jack@example.com", Books: []Book{ {Title: "Go in Action", Author: "William Kennedy"}, {Title: "The Go Programming Language", Author: "Donovan & Kernighan"}, }, }) // Preload 關聯資料 var user User db.Preload("Books").First(&user, 1) // user.Books 會自動填入 // 條件 Preload db.Preload("Books", "author LIKE ?", "%Kennedy%").First(&user, 1) Hook // 在建立之前自動處理 func (u *User) BeforeCreate(tx *gorm.DB) error { if u.Email == "" { return errors.New("email is required") } return nil } // 在刪除之後清理關聯 func (u *User) AfterDelete(tx *gorm.DB) error { tx.Where("user_id = ?", u.ID).Delete(&Book{}) return nil } Transaction err := db.Transaction(func(tx *gorm.DB) error { user := User{Name: "Jack", Email: "jack@example.com"} if err := tx.Create(&user).Error; err != nil { return err // 回傳 error 會自動 rollback } book := Book{Title: "New Book", UserID: user.ID} if err := tx.Create(&book).Error; err != nil { return err } return nil // 回傳 nil 會自動 commit }) sqlx 安裝: ...

2026年2月24日 · 4 min · 795 words · Jack

Go 語言系列(十三):Web 框架 — Gin vs Echo

這是 Go 語言從零到 Web 應用系列的第十三篇,進入進階篇章。前面我們用標準庫 net/http 建構了完整的 Web 應用,現在來看看 Web 框架在標準庫之上提供了什麼價值。 為什麼需要 Web 框架? 標準庫 net/http 非常強大,但在建構大型應用時會遇到一些不便: // 標準庫:手動解析路由參數 mux := http.NewServeMux() mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) { // 需要手動從 URL 提取 ID id := strings.TrimPrefix(r.URL.Path, "/users/") if id == "" { http.Error(w, "missing id", http.StatusBadRequest) return } // 還需要手動判斷 HTTP method switch r.Method { case http.MethodGet: // 處理 GET case http.MethodPut: // 處理 PUT default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } }) Web 框架幫你解決的問題: 路由參數:/users/:id 自動解析 HTTP method 路由:GET /users/:id 和 PUT /users/:id 分開定義 請求綁定:JSON、Query、Form 自動綁定到 struct 參數驗證:struct tag 自動驗證 中間件:標準化的中間件鏈 錯誤處理:集中式錯誤處理 Go 生態中最熱門的兩個框架是 Gin 和 Echo,讓我們用同一個 API 來對比。 ...

2026年2月24日 · 4 min · 844 words · Jack

Go 語言系列(十二):常見陷阱與反模式

這是 Go 語言從零到 Web 應用系列的第十二篇,也是本系列的最終篇。這篇整理 Go 開發中最常踩到的陷阱——即使是有經驗的 Go 開發者也可能中招。每個陷阱都附帶「踩坑範例」和「正確做法」,讓你一次看清問題在哪。 Slice 陷阱 陷阱 1:Append 可能修改原始 Slice 當 slice 的 capacity 還有剩餘空間時,append 會直接修改底層 array,影響到共享同一個 array 的其他 slice。 package main import "fmt" func main() { original := make([]int, 3, 5) // len=3, cap=5 original[0], original[1], original[2] = 1, 2, 3 // 截取一個子 slice sub := original[:2] // append 到 sub — cap 還有空間,不會分配新 array sub = append(sub, 99) fmt.Println("original:", original) // [1 2 99] ← 被改了! fmt.Println("sub:", sub) // [1 2 99] } 正確做法: 使用 full slice expression 限制 capacity // 用 original[:2:2] 限制 sub 的 cap = 2 sub := original[:2:2] // 現在 append 會分配新 array,不影響 original sub = append(sub, 99) fmt.Println("original:", original) // [1 2 3] ← 沒被改 fmt.Println("sub:", sub) // [1 2 99] 陷阱 2:大 Slice 截取小片段導致記憶體無法釋放 // 錯誤:小 slice 持有大 array 的引用,GC 無法回收 func getFirstTen(data []byte) []byte { return data[:10] // 底層 array 整個被保留 } // 正確:複製需要的資料到新 slice func getFirstTen(data []byte) []byte { result := make([]byte, 10) copy(result, data[:10]) return result // 原始的大 array 可以被 GC 回收 } 假設 data 是 1GB 的檔案內容,用截取的方式只取 10 bytes,那 1GB 的底層 array 都不會被回收。 ...

2026年2月24日 · 6 min · 1239 words · Jack

Go 語言系列(十一):面試必問問題

這是 Go 語言從零到 Web 應用系列的第十一篇。前十篇我們完成了從基礎到實戰的完整學習路徑,這篇整理 Go 面試中最常被問到的核心問題,幫助你在面試中展現對 Go 的深入理解。 Goroutine 與 Channel Q1:Go 的 Goroutine 排程模型是什麼? Go 使用 GMP 模型來排程 goroutine: G(Goroutine):要執行的工作單元,每個 go func() 建立一個 G M(Machine):作業系統執行緒,實際執行程式碼的載體 P(Processor):邏輯處理器,持有本地的 G 佇列,數量預設等於 CPU 核心數 排程流程: G1, G2, G3 ... → P(本地佇列)→ M(OS Thread)→ CPU P 從自己的本地佇列取出 G 放到 M 上執行。當本地佇列空了,會從其他 P「偷」G 來執行(work stealing)。 package main import ( "fmt" "runtime" ) func main() { // 查看 P 的數量 fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 查看目前的 goroutine 數量 fmt.Println("NumGoroutine:", runtime.NumGoroutine()) for i := 0; i < 5; i++ { go func(n int) { fmt.Printf("goroutine %d running on thread\n", n) }(i) } fmt.Println("NumGoroutine after launch:", runtime.NumGoroutine()) runtime.Gosched() // 讓出 CPU 給其他 goroutine } 面試加分點: ...

2026年2月24日 · 6 min · 1269 words · Jack

Go 語言系列(十):測試與部署

這是 Go 語言從零到 Web 應用系列的第十篇。前九篇我們從零開始建構了一個具備 CRUD、資料庫、認證的完整 Web 應用。這篇要學如何測試和部署它。 Go 的測試工具 Go 有一個內建的測試框架,不需要安裝任何第三方套件。這是 Go 「電池已附」哲學的體現。 基本規則 測試檔案以 _test.go 結尾 測試函式以 Test 開頭 接受一個 *testing.T 參數 用 go test 執行 單元測試 基本範例 假設有一個 math.go: package math func Add(a, b int) int { return a + b } func Divide(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return a / b, nil } 對應的 math_test.go: package math import "testing" func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("Add(2, 3) = %d, want 5", result) } } func TestDivide(t *testing.T) { result, err := Divide(10, 2) if err != nil { t.Fatalf("unexpected error: %v", err) } if result != 5.0 { t.Errorf("Divide(10, 2) = %f, want 5.0", result) } } func TestDivideByZero(t *testing.T) { _, err := Divide(10, 0) if err == nil { t.Fatal("expected error for division by zero") } } 常用的 testing.T 方法 t.Error("message") // 記錄錯誤但繼續執行 t.Errorf("format %d", val) // 格式化版本 t.Fatal("message") // 記錄錯誤並立即停止這個測試 t.Fatalf("format %d", val) // 格式化版本 t.Log("message") // 記錄訊息(只在 -v 模式顯示) t.Skip("reason") // 跳過這個測試 執行測試 # 執行當前目錄的測試 go test # 執行所有套件的測試 go test ./... # 詳細輸出 go test -v ./... # 執行特定測試 go test -run TestDivide ./... # 顯示覆蓋率 go test -cover ./... # 生成覆蓋率報告 go test -coverprofile=coverage.out ./... go tool cover -html=coverage.out 表格驅動測試 表格驅動測試是 Go 社群最推薦的測試風格。將測試案例定義為表格,用迴圈逐一測試: ...

2026年2月24日 · 6 min · 1217 words · Jack

Go 語言系列(九):使用者認證

這是 Go 語言從零到 Web 應用系列的第九篇。上一篇我們整合了資料庫,這篇要加入使用者認證,讓 API 知道「誰」在操作。 認證的基本概念 Authentication vs Authorization Authentication(認證):驗證「你是誰」——使用者登入 Authorization(授權):驗證「你能做什麼」——權限檢查 這篇聚焦在 Authentication。 JWT 認證流程 我們採用 JWT(JSON Web Token)認證方式: 1. 使用者註冊 → 密碼用 bcrypt 雜湊後存入資料庫 2. 使用者登入 → 驗證密碼 → 回傳 JWT Token 3. 後續請求 → 在 Header 帶上 Token → 中間件驗證 → 存取受保護資源 安裝依賴 go get golang.org/x/crypto/bcrypt go get github.com/golang-jwt/jwt/v5 bcrypt:密碼雜湊演算法 golang-jwt:Go 社群最廣泛使用的 JWT 套件 使用者模型 internal/model/user.go package model import "time" type User struct { ID int `json:"id"` Username string `json:"username"` Email string `json:"email"` PasswordHash string `json:"-"` // json:"-" 永遠不會出現在 JSON 回應中 CreatedAt time.Time `json:"created_at"` } type RegisterRequest struct { Username string `json:"username"` Email string `json:"email"` Password string `json:"password"` } func (r RegisterRequest) Validate() map[string]string { errs := make(map[string]string) if r.Username == "" { errs["username"] = "username is required" } if r.Email == "" { errs["email"] = "email is required" } if len(r.Password) < 8 { errs["password"] = "password must be at least 8 characters" } return errs } type LoginRequest struct { Email string `json:"email"` Password string `json:"password"` } type AuthResponse struct { Token string `json:"token"` User User `json:"user"` } 注意 PasswordHash 的 json:"-" 標籤——這確保密碼雜湊永遠不會被序列化到 JSON 回應中。 ...

2026年2月24日 · 7 min · 1342 words · Jack