Ian Chou's Blog

邊緣運算部署實戰:Go (Chi + sqlc) on Fly.io vs TypeScript (Hono + Drizzle) on Vercel/Cloudflare

前言:Edge 部署的兩種哲學

在 2026 年,「邊緣運算 (Edge Computing)」已經不再是新鮮詞彙。但你知道嗎?當你在選擇部署平台時,其實是在選擇兩種完全不同的哲學:

  1. 輕量級 V8 Isolate (Cloudflare Workers, Vercel Edge Functions)
  2. 分佈式微型容器 (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),但做法更極端:

-- 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

相信讀者對這組合已經很熟悉,簡單回顧:

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 --from=builder /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 會自動把資料庫同步到所有節點:


八、選擇建議

選 Cloudflare Workers + Hono + Drizzle,如果你:

選 Vercel Edge Functions + Hono + Drizzle,如果你:

Fly.io + Go (Chi + sqlc),如果你:


九、結論:沒有銀彈,只有 Trade-off

選擇矩陣

業務複雜度 冷啟動敏感 運算密集
簡單 CRUD Cloudflare (Hono + D1) Vercel Edge (Hono + Neon)
複雜業務邏輯 Fly.io (Go + SQLite) Fly.io (Go + Postgres)

最終建議:

  1. 如果你是 TypeScript 開發者:從 Cloudflare Workers 開始,遇到限制再往上升級。
  2. 如果你想學 Go:直接用 Fly.io,它讓你體驗「真正的」Go 部署,不用搞 Wasm。
  3. 如果你在做微服務: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