跳到主要內容

遷移與導入 ​

最穩妥的遷移原則是:保留資料面,導入你能清楚說明的邊界。這裡的資料面,指的是實際處理請求、訊息或工作的那部分系統。

保留現有的 Web 框架、worker、排程器、訊息代理、logger 或 telemetry provider(具體實作)。針對一個真實的生命週期或服務呼叫問題,加上一個明確的 go-like 契約。確認這個邊界成立後,再導入下一個 provider。

分階段遷移 ​

  1. 保持現有的啟動流程,以及路由/資料面程式碼不變。
  2. 找出一個明確的擁有者:listener、worker、排程器、訊息代理訂閱、logger 輸出端或 telemetry provider。
  3. 增加結構式 Server adapter,或使用既有的 go-like adapter。定義資源如何被接納、如何停止、等待多久,以及在哪裡觀察終態。
  4. 在真正發生取消或 deadline 控制的邊界導入 @go-like/context,它提供帶有取消訊號、deadline 與 context value 的 Context,並把它當作操作的第一個參數往下傳。
  5. 使用 @go-like/health 與 @go-like/web/health 增加 liveness 和 readiness 檢查。
  6. 在測試中先用 @go-like/transport-memory 加上一個內部 typed unary call(帶型別的單請求/單回應呼叫)。
  7. 只有在確實需要網路線路或原生 Node host 時,才把這個呼叫切換到 @go-like/transport-http 或 @go-like/transport-http/node。
  8. 一次只增加一項能力:Registry、Config、Store、Cache、Broker、logging、metrics 或 tracing。
  9. 為每個新邊界記錄 provider、runtime、擁有者與證據通道。

不要一開始就重寫整個服務。小型契約的價值,就在於遷移單位可以維持很小。

框架遷移矩陣 ​

現有系統保留原生部分優先導入目前的邊界
NestJSModules、controllers、decorators、DI、interceptors、pipes、adapter在現有應用外包一層自訂結構式 Server,或建立獨立的內部 Client/Server 邊界目前 repository 沒有 go-like Nest bridge,也沒有自動 DI 整合
FastifyRoutes、plugins、hooks、request/reply、原生 listener自訂生命週期 wrapper,或明確實作的 Fetch bridge目前沒有證據證明 Fastify request/reply 能自動轉換成 go-like Handler
HonoRoutes、middleware、sub-apps、app.fetchnewNodeServer(app.fetch, ...),再接到 newApp(...)examples/hono 展示了直接使用原生 Fetch 的整合方式
ElysiaRoute tree、schema、decorators、derives、hooks、Bun/Web Standard 行為在適合的情況下使用原生 app.fetch 加上 Core host/lifecycle保留 Bun 專屬的 .listen() 語意;不要把它說成跨 runtime 的 go-like API
H3H3 router 與原生 handler 轉換目前 H3 範例中的 Fetch handler 路徑目前展示的是 H3 2.x 的 app.fetch;舊版 toWebHandler 指引需要獨立固定版本的範例
KoaMiddleware 與外部 router自訂擁有者 wrapper,或內部服務呼叫@go-like/web 不會直接接收 Koa 的 Node request/reply 物件,除非應用自行提供 bridge
tRPCRouter、procedure middleware、輸入/輸出解析器、adapter在 host 外圍加上 Core lifecycle,或使用獨立的內部 transport 邊界go-like Endpoint 不是 tRPC procedure router

Hono 範例 ​

這是 repository 已經展示的整合方式:

ts
import { Hono } from "hono"
import { name, newApp, server } from "@go-like/core"
import { signal } from "@go-like/core/node"
import { newNodeServer, port } from "@go-like/web/node"

const web = new Hono().get("/users/:id", (c) => c.json({ id: c.req.param("id") }))

const app = newApp(name("users"), server(newNodeServer(web.fetch, port(3000))), signal())

await app.run()

目前的 Hono 範例保留 Hono 的路由擁有權,把原生 Fetch handler 交給 Node host。它沒有新增 go-like route table,也沒有 Hono 專用的 bridge 套件。

Elysia 與 H3 ​

對於暴露標準 Fetch handler 的框架,可以採用相同的邊界:

text
framework route table
  -> framework native Fetch handler
  -> @go-like/web/node (when using the Node host)
  -> @go-like/core App

在匯入 Node 子路徑前,先檢查框架使用的 runtime adapter。Elysia 的 Bun adapter 與 Web Standard adapter 的 listen 行為並不完全相同;H3 的版本與 handler 轉換 API 也需要固定版本的範例。不要因為存在一個範例,就承諾所有框架版本或 runtime 組合都能運作。

Go 服務遷移 ​

如果你熟悉 Go 或 Kratos,遷移時應該遷移概念,而不是照抄拼法:

Go 概念go-like 概念重要差異
context.Context@go-like/context Contextdone() 回傳的是 AbortSignal 或 null,不是 Go channel
Server lifecycleCore 結構式 Serverstart(ctx) 可能長時間維持執行,不等於 readiness
App runnernewApp、App.run、App.stopApp.stop() 沒有呼叫端 Context,並回傳一個共用的 Promise
RPC client@go-like/client內部呼叫是 JSON Fetch;retry 預設關閉
Transport@go-like/transportProvider 與 Fetch headers 都是 TypeScript/Web 契約
Registry@go-like/registryWatcher 回傳完整替換後的 snapshot
SelectornewRoundRobinSelector、newRandomSelector、newWeightedRoundRobinSelector、newP2CSelector、newEWMASelectorfeedback 是同步的,而且取決於具體策略
Protobuf/IDL@go-like/protoc-gen-like基於 Protobuf-ES 的 Context-first 程式碼
gRPC stream@go-like/transport-grpc-buf/native/native 提供標準 gRPC 四種呼叫形態

一個適合漸進遷移的第一步,是用 Memory Transport 做一次直連位址的 typed call:

ts
import { newClient, withEndpoint, withTransport } from "@go-like/client"
import { background, type Context } from "@go-like/context"
import { address, newServer, transport as serverTransport } from "@go-like/server"
import { struct } from "@go-like/struct"
import { defineService } from "@go-like/transport"
import { newMemoryTransport } from "@go-like/transport-memory"

const quoteService = defineService("pricing", {
  quote: {
    request: struct.object({ sku: struct.string() }),
    response: struct.object({ amount: struct.number() })
  }
})

const transport = newMemoryTransport()
const server = newServer(serverTransport(transport), address("memory://pricing"))
quoteService.registerHandler(server, {
  quote(_ctx, request) {
    return { amount: request.sku.length }
  }
})

const client = newClient(withTransport(transport), withEndpoint("memory://pricing"))
const caller = quoteService.newClient(client)
const ctx: Context = background()
const result = await caller.quote(ctx, { sku: "a" })
void result

先把這個邊界測通,再引入 Discovery、真正的 Registry provider 或 HTTP transport。這樣替換的是目的地與所有權接線,領域契約仍然保持穩定。

Kubernetes 導入 ​

Kubernetes 的原生能力繼續由 Kubernetes 負責:

  • Deployment、Service、DNS、Ingress、RBAC、探針、發布策略、HPA 與 network policy 仍是平台責任;
  • @go-like/config-kubernetes 透過注入的 Fetch capability,從一個 namespace 內的 ConfigMap 或 Secret 讀取一個 key;
  • @go-like/registry-kubernetes 在確實需要直接探索時使用 EndpointSlice 記錄;
  • EndpointSlice 不是 Kubernetes Service DNS,也不會提供通用的註冊 TTL;
  • 可選的 Pod owner reference 與明確註銷具有不同的故障語意。

先導入 health 與 configuration,再處理直接的 EndpointSlice selection。如果應用已有穩定的 Service DNS 名稱,withEndpoint(...) 加上 HTTP transport 可能比引入 Registry provider 更簡單,也更符合實際。

訊息代理與工作導入 ​

保留原生的 settlement 語意與工作策略:

現有資料面保留用 go-like 增加
NATS CoreConnection、subscription、queue group、Msg、drainnewNatsCoreServer、newNatsCoreBroker、生命週期與位元組邊界
NATS JetStreamStream、durable consumer、JsMsg、ack/nak/term、redelivery、DLQnewNatsJetStreamServer、newNatsJetStreamBroker、生命週期
RabbitMQConnection、topology、confirm policy、channel借用或復原中的 subscriber lifecycle,以及 generation-safe 的原生結算
BullMQQueue、Worker、processor、retry/backoff、Redis圍繞官方 dormant Worker 使用 newBullMqWorkerServer
CronerCron expression、time zone、callback、overlap policy圍繞暫停狀態的原生 Cron jobs 使用 newCronerServer
Memory Broker程序內 topic map 與測試語意newBrokerServer 與可選的 event codec

不要把 NATS ack/nak/term、JetStream durable settlement、RabbitMQ confirmations 或 BullMQ retries 搬進通用的 go-like Broker 抽象。正因為這些語意重要,provider 的原生物件才應該繼續可見。

狀態遷移 ​

一次只遷移一個狀態領域:

  • Config 用於不可變的程序設定快照與 reload;
  • Registry 用於暫時性的服務可達性;
  • Store 用於權威記錄、revision、CAS、TTL 與分頁;
  • Cache 用於可以重新計算的可丟棄值。

一個實用的遷移測試,是寫下程序重啟、讀到過期資料、provider 故障、watcher compaction、CAS 衝突和 cache miss 後會發生什麼。如果答案不同,它們就不應該共用一個通用 repository interface。

增加可觀測性 ​

先建立原生 provider,再包住邊界:

text
application creates logger / Registry / MeterProvider / TracerProvider
  -> go-like wrapper records bounded operation facts
  -> application-owned exporter or destination
  -> explicit Core lifecycle adapter closes the admitted resource

@go-like/prometheus 不使用 global registry。@go-like/otel 不會安裝 global providers 或 exporters。Pino 與 Winston adapter 不會取代原生 logger 設定。讓 labels 與 attributes 保持有界,並另外為應用自有的 log 設定脫敏策略。

遷移驗收清單 ​

合併一個邊界前,確認:

  • 有一個命名清楚的擁有者;
  • 擁有者拿到正確的 Context,且不會用 background() 取代它;
  • 啟動接納與 readiness 是兩個不同概念;
  • stop timeout 的行為已按等待邊界寫清楚;
  • 原生終態觀察能力仍然保留(如果 provider 有提供);
  • 外部 Web handler 與內部 unary handler 沒有混用;
  • retry 授權符合業務操作;
  • credentials、metadata、log 與 trace attributes 有脫敏策略;
  • provider 專屬語意仍然可見;
  • 目標 checkout 中的 focused unit/typecheck 指令已通過;
  • 相關 runtime、provider、published 或 example E2E 指令要嘛已執行並記錄,要嘛明確標示為未執行。

目前支援邊界 ​

@go-like/protoc-gen-like 基於 Protobuf-ES 產生 Context-first Protobuf RPC 程式碼。@go-like/transport-grpc-buf 的 Fetch 入口支援 Connect/gRPC-Web unary 與 server-streaming;/native 提供標準 gRPC 的四種呼叫形態,包含 client-streaming 與 bidi。這是獨立於內部 Fetch/SSE 路徑。

這不包含瀏覽器標準 gRPC、Fetch request-streaming/bidi、官方 health/reflection、通用驗證、Event Store/replay、ORM 或叢集編排。

Node 26.12.1 與 Bun 1.4.4 的託管 gRPC 已通過慢速消費者 drain 回歸測試;Connect 2.1.2 與 Bun 1.4.2 Fetch 的歷史取消與停機限制仍未驗證已修復。