邊緣運算部署實戰:Go (Chi + sqlc) on Fly.io vs TypeScript (Hono + Drizzle) on Vercel/Cloudflare
前言:Edge 部署的兩種哲學
在 2026 年,「邊緣運算 (Edge Computing)」已經不再是新鮮詞彙。但你知道嗎?當你在選擇部署平台時,其實是在選擇兩種完全不同的哲學:
- 輕量級 V8 Isolate (Cloudflare Workers, Vercel Edge Functions)
- 分佈式微型容器 (Fly.io, Google Cloud Run)
本文將透過實戰對比 Go (Chi + sqlc) 與 TypeScript (Hono + Drizzle) 這兩套組合,讓你清楚了解「什麼場景該用什麼武器」。
一、技術棧基礎介紹
Go 組合:Chi + sqlc
Chi:Go 界的 Hono
Chi 是一個輕量級、符合慣例 (Idiomatic) 的 Go HTTP Router。如果你習慣 Express/Hono 的風格,Chi 會讓你感到非常親切。
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
http.ListenAndServe(":3000", r)
}
sqlc:SQL-first 的 Type-Safe 生成器
sqlc 是一個魔法工具。它的哲學跟 Drizzle 類似(Type-Safe、SQL-first),但做法更極端:
- Drizzle:用 TypeScript 定義 Schema → 推導出 SQL
- sqlc:你直接寫純 SQL → 它幫你編譯成 Type-Safe 的 Go 程式碼
-- query.sql
-- name: GetUser :one
SELECT * FROM users WHERE id = $1 LIMIT 1;
執行 sqlc generate 後,你會得到:
// 自動生成的,完全 Type-Safe
func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRowContext(ctx, getUser, id)
var i User
err := row.Scan(&i.ID, &i.Name, &i.Email)
return i, err
}
這比 Drizzle 更暴力。Drizzle 還是在 Runtime 組裝 SQL,sqlc 是在編譯前就幫你把 DB 存取層的 Go Code 寫好了。
TypeScript 組合:Hono + Drizzle
相信讀者對這組合已經很熟悉,簡單回顧:
- Hono:極輕量的 Web Framework,原生支援 Edge Runtime
- Drizzle:Type-Safe 的 SQL Builder,完美整合 D1/Neon/Turso 等 Edge DB
import { Hono } from 'hono'
import { drizzle } from 'drizzle-orm/d1'
const app = new Hono<{ Bindings: Env }>()
app.get('/users/:id', async (c) => {
const db = drizzle(c.env.DB)
const user = await db.select().from(users).where(eq(users.id, c.req.param('id')))
return c.json(user)
})
export default app
二、部署平台的本質差異
在深入部署流程前,必須先理解這三個平台的本質差異:
| 特性 | Cloudflare Workers | Vercel Edge Functions | Fly.io |
|---|---|---|---|
| 本質 | V8 Isolate (輕量腳本) | V8 Isolate (輕量腳本) | Firecracker VM (完整 Linux) |
| 執行環境 | 受限的 JavaScript Runtime | 受限的 Edge Runtime | 完整的 Linux Container |
| 冷啟動 | 無 (~0ms) | 極短 (~50ms) | 短 (<300ms) |
| 運算限制 | CPU 時間 10-50ms | CPU 時間 25ms~5s | 無限制 (看你買多少) |
| 記憶體 | 128MB | 128MB | 256MB~16GB |
| 支援語言 | JS/TS/Wasm | JS/TS | 任何語言 (Container) |
| Go 支援 | ❌ 需轉 Wasm | ❌ 需轉 Wasm | ✅ 原生 Binary |
⚠️ 關鍵差異:Cloudflare/Vercel 是「輕量級腳本執行器」,Fly.io 是「分佈式 Linux 容器」。這決定了你能跑什麼、怎麼跑。
三、Fly.io 部署 Go (Chi + sqlc) 實戰
Step 1: 專案初始化
# 建立專案資料夾
mkdir my-go-api && cd my-go-api
# 初始化 Go Module
go mod init github.com/username/my-go-api
# 安裝依賴
go get github.com/go-chi/chi/v5
go get github.com/jackc/pgx/v5
Step 2: 設定 sqlc
建立 sqlc.yaml:
version: "2"
sql:
- engine: "postgresql"
queries: "db/query.sql"
schema: "db/schema.sql"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5"
建立 db/schema.sql:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
建立 db/query.sql:
-- name: GetUser :one
SELECT * FROM users WHERE id = $1 LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users ORDER BY created_at DESC LIMIT $1;
-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING *;
執行程式碼生成:
sqlc generate
Step 3: 撰寫主程式
main.go:
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/username/my-go-api/db"
)
func main() {
// 連接資料庫
pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer pool.Close()
queries := db.New(pool)
// 設定 Router
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
// Health Check (for Fly.io)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
// API Routes
r.Route("/api/users", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
users, err := queries.ListUsers(r.Context(), 100)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(users)
})
r.Get("/{id}", func(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
user, err := queries.GetUser(r.Context(), int32(id))
if err != nil {
http.Error(w, "User not found", 404)
return
}
json.NewEncoder(w).Encode(user)
})
})
// 啟動伺服器
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Server starting on :%s", port)
http.ListenAndServe(":"+port, r)
}
Step 4: 建立 Dockerfile
這是 Fly.io 的核心——你需要一個 Dockerfile 來打包你的 Go Binary:
# Build Stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
# 安裝 sqlc
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# 複製依賴檔案
COPY go.mod go.sum ./
RUN go mod download
# 複製原始碼
COPY . .
# 生成 sqlc 程式碼 (如果需要)
RUN sqlc generate
# 編譯成靜態 Binary
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# Runtime Stage (極小映像檔)
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY /app/server .
EXPOSE 8080
CMD ["./server"]
💡 極致優化:你可以用
FROM scratch取代FROM alpine:latest,讓整個 Docker Image 只有 Go Binary 本身(約 10-20MB)。
Step 5: 部署到 Fly.io
# 安裝 flyctl (如果還沒安裝)
brew install flyctl # macOS
# 或
curl -L https://fly.io/install.sh | sh # Linux/WSL
# 登入
flyctl auth login
# 初始化專案 (會生成 fly.toml)
flyctl launch
# 設定 Database 連線 (使用 Fly Postgres 或外部 DB)
flyctl secrets set DATABASE_URL="postgresql://user:password@host:5432/dbname"
# 部署!
flyctl deploy
生成的 fly.toml 會長這樣:
app = 'my-go-api'
primary_region = 'nrt' # 東京
[build]
[env]
PORT = '8080'
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true # Scale to Zero!
auto_start_machines = true # 瞬間喚醒
min_machines_running = 0
[[vm]]
memory = '256mb'
cpu_kind = 'shared'
cpus = 1
Step 6: 全球部署 (可選)
Fly.io 的殺手級功能:把你的 App 部署到多個區域
# 在新加坡增加一個節點
flyctl scale count 1 --region sin
# 在洛杉磯增加一個節點
flyctl scale count 1 --region lax
# 查看所有節點狀態
flyctl status
四、Vercel 部署 Hono + Drizzle 實戰
Step 1: 專案初始化
# 使用 Bun 建立專案
bun create hono my-api --template vercel
cd my-api
# 安裝 Drizzle
bun add drizzle-orm @neondatabase/serverless
bun add -d drizzle-kit
Step 2: 設定 Drizzle Schema
src/db/schema.ts:
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow()
})
Step 3: 撰寫 API
src/index.ts:
import { Hono } from 'hono'
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'
import { users } from './db/schema'
import { eq } from 'drizzle-orm'
type Bindings = {
DATABASE_URL: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/health', (c) => c.text('OK'))
app.get('/api/users', async (c) => {
const sql = neon(c.env.DATABASE_URL)
const db = drizzle(sql)
const result = await db.select().from(users).limit(100)
return c.json(result)
})
app.get('/api/users/:id', async (c) => {
const sql = neon(c.env.DATABASE_URL)
const db = drizzle(sql)
const id = parseInt(c.req.param('id'))
const result = await db.select().from(users).where(eq(users.id, id))
if (result.length === 0) {
return c.json({ error: 'User not found' }, 404)
}
return c.json(result[0])
})
export default app
Step 4: 設定 Vercel
vercel.json:
{
"buildCommand": "bun run build",
"devCommand": "bun run dev",
"installCommand": "bun install",
"framework": null,
"outputDirectory": ".vercel/output"
}
Step 5: 部署
# 安裝 Vercel CLI
bun add -g vercel
# 登入
vercel login
# 設定環境變數
vercel env add DATABASE_URL
# 部署
vercel deploy --prod
五、Cloudflare Workers 部署 Hono + Drizzle 實戰
Step 1: 專案初始化
# 使用 Wrangler 建立專案
bunx wrangler init my-api --type javascript
cd my-api
# 安裝依賴
bun add hono drizzle-orm
bun add -d drizzle-kit
Step 2: 設定 D1 資料庫
# 建立 D1 資料庫
bunx wrangler d1 create my-database
# 會輸出綁定資訊,加到 wrangler.toml
wrangler.toml:
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxx-xxxx-xxxx"
Step 3: Drizzle Schema (D1 版)
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: text('created_at').default('CURRENT_TIMESTAMP')
})
Step 4: 撰寫 API
import { Hono } from 'hono'
import { drizzle } from 'drizzle-orm/d1'
import { users } from './db/schema'
import { eq } from 'drizzle-orm'
type Bindings = {
DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/api/users', async (c) => {
const db = drizzle(c.env.DB)
const result = await db.select().from(users).limit(100)
return c.json(result)
})
app.get('/api/users/:id', async (c) => {
const db = drizzle(c.env.DB)
const id = parseInt(c.req.param('id'))
const result = await db.select().from(users).where(eq(users.id, id))
if (result.length === 0) {
return c.json({ error: 'User not found' }, 404)
}
return c.json(result[0])
})
export default app
Step 5: 部署
# 推送 Schema 到 D1
bunx wrangler d1 execute my-database --file=./drizzle/schema.sql
# 部署 Worker
bunx wrangler deploy
六、三種方案綜合比較
開發體驗 (DX)
| 項目 | Go (Chi + sqlc) | TS (Hono + Drizzle on Vercel) | TS (Hono + Drizzle on CF) |
|---|---|---|---|
| 學習曲線 | 中(需懂 Go) | 低 | 低 |
| IDE 支援 | ⭐⭐⭐⭐ (gopls) | ⭐⭐⭐⭐⭐ (TypeScript) | ⭐⭐⭐⭐⭐ (TypeScript) |
| 熱重載 | 需用 air | 原生支援 | 原生支援 |
| Schema 變更 | sqlc generate |
drizzle-kit push |
drizzle-kit push |
運行效能
| 項目 | Go (Chi + sqlc) on Fly.io | Hono + Drizzle on Vercel | Hono + Drizzle on CF |
|---|---|---|---|
| 冷啟動 | 100-300ms | 50-100ms | ~0ms |
| 執行效能 | ⭐⭐⭐⭐⭐ (原生機器碼) | ⭐⭐⭐⭐ (V8 JIT) | ⭐⭐⭐⭐ (V8 JIT) |
| 記憶體使用 | 低 (可控) | 極低 (受限) | 極低 (受限) |
| CPU 限制 | 無 | 有 (10-60s) | 有 (10-50ms) |
部署與維運
| 項目 | Go on Fly.io | TS on Vercel | TS on Cloudflare |
|---|---|---|---|
| 部署複雜度 | 中 (需 Dockerfile) | 低 (零配置) | 低 (wrangler) |
| 全球分佈 | 手動 scale | 自動 | 自動 |
| Scale to Zero | ✅ | ✅ | ✅ |
| 資料庫選項 | 任何 (完整網路) | Neon/Turso/PlanetScale | D1/Hyperdrive |
| 價格 | Pay-as-you-go | Generous Free Tier | Generous Free Tier |
七、資料庫延遲問題:Edge 的致命傷
⚠️ 注意:這是選擇 Edge 架構時最容易被忽略的問題。
當你把程式碼部署到全球 Edge 節點時,你的程式離使用者很近,但如果資料庫還在單一區域:
使用者 (台北) → Edge (台北): 2ms ✅
Edge (台北) → DB (美國): 180ms ❌
DB → Edge → 使用者: 180ms + 2ms ❌
結果:你的 API 還是慢!
各平台的解決方案
| 平台 | 解法 | 適用場景 |
|---|---|---|
| Cloudflare | D1 (邊緣 SQLite) + Hyperdrive (連線池) | 讀多寫少 |
| Vercel | Edge Config + Neon Serverless Driver | 簡單查詢 |
| Fly.io | LiteFS (分佈式 SQLite) 或就近 Postgres | 完整 SQL 需求 |
💡 Fly.io 的殺手組合:SQLite + LiteFS
因為 Fly.io 給你完整的 Linux 環境,你可以直接把 SQLite 跟 Go 程式包在一起。LiteFS 會自動把資料庫同步到所有節點:
- 讀取:直接讀本地檔案 (0ms 延遲)
- 寫入:轉發到主節點 (單一寫入點)
八、選擇建議
選 Cloudflare Workers + Hono + Drizzle,如果你:
- 需要極致冷啟動 (0ms)
- 業務邏輯簡單,不需要長時間運算
- 願意使用 D1/KV 等 Cloudflare 原生儲存
- 追求最低成本 (Free tier 非常慷慨)
選 Vercel Edge Functions + Hono + Drizzle,如果你:
- 已經在用 Next.js/Vercel 生態系
- 想要零配置部署體驗
- 需要跟 Neon/Turso 等 Serverless Postgres 整合
- 不介意某些 Node.js API 不可用
選 Fly.io + Go (Chi + sqlc),如果你:
- 需要完整的運算能力 (長時間任務、背景處理)
- 使用 Go/Rust 等編譯語言
- 想要完全掌控執行環境 (Docker)
- 資料庫需求複雜,需要本地 SQLite 或完整 Postgres
- 追求極致效能和最小 Container Size
九、結論:沒有銀彈,只有 Trade-off
選擇矩陣
| 業務複雜度 | 冷啟動敏感 | 運算密集 |
|---|---|---|
| 簡單 CRUD | Cloudflare (Hono + D1) | Vercel Edge (Hono + Neon) |
| 複雜業務邏輯 | Fly.io (Go + SQLite) | Fly.io (Go + Postgres) |
最終建議:
- 如果你是 TypeScript 開發者:從 Cloudflare Workers 開始,遇到限制再往上升級。
- 如果你想學 Go:直接用 Fly.io,它讓你體驗「真正的」Go 部署,不用搞 Wasm。
- 如果你在做微服務:Go on Fly.io 是無敵的。小 Binary、快啟動、低記憶體。
無論選哪個,記住:Edge 的價值在於讓「運算」靠近「使用者」。但如果你的資料庫還在地球另一端,所有的 Edge 優勢都會被延遲吃掉。
先解決 Data Gravity,再談 Edge Computing。
附錄:快速指令對照表
| 任務 | Go (Fly.io) | TS (Vercel) | TS (Cloudflare) |
|---|---|---|---|
| 初始化專案 | go mod init |
bun create hono |
bunx wrangler init |
| 安裝依賴 | go get |
bun add |
bun add |
| 本地開發 | go run . or air |
bun run dev |
bunx wrangler dev |
| 生成 DB Code | sqlc generate |
bunx drizzle-kit generate |
bunx drizzle-kit generate |
| 部署 | flyctl deploy |
vercel deploy |
bunx wrangler deploy |
| 設定環境變數 | flyctl secrets set |
vercel env add |
Dashboard 或 wrangler.toml |
| 查看日誌 | flyctl logs |
Vercel Dashboard | bunx wrangler tail |