請點擊上方藍字TonyBai訂閱公眾號!
大家好,我是 Tony Bai。
作為開發者,我們每天都在與 API 打交道——呼叫它們,設計它們,有時也會為糟糕的 API 設計而頭痛不已。一個優秀的 API,如同一位技藝精湛的嚮導,能清晰、高效地引領我們通往複雜功能的彼岸;而一個蹩腳的 API,則可能像一座佈滿陷阱的迷宮,讓我們步履維艱。
那麼,在 Go 語言的世界裡,一個「好」的 API 應該是什麼樣子的?它應該如何體現 Go 語言簡潔、高效、並行安全的哲學?它又如何在滿足功能需求的同時,保持對開發者的友好和對未來的相容?
最近,Go 官方團隊為 Model Context Protocol (MCP) 發起了一項 Go SDK 的設計討論,並公開了其詳細的設計草案以及一個初期的原型程式碼實現。這份設計稿與程式碼,在我看來,不僅僅是對 MCP 協定的 Go 語言實現規劃,更是一份Go 官方團隊關於 API 設計思考與實踐的「公開課」。它向我們生動地展示了,在打造一個既強大又符合 Go 慣例 (Idiomatic Go) 的 SDK 時,需要在哪些維度進行權衡取捨,以及如何將 Go 的設計哲學融入到每一個細節之中。
今天,就讓我們一同走進這份設計稿和它的原型程式碼,探尋 Go 團隊在 API 設計中所追求的「Go 境界」。
API 設計的「初心」:Go 團隊為 MCP SDK 設定的目標
在深入細節之前,我們先來看看 Go 團隊為這個官方 MCP SDK 設定了哪些核心目標 (Requirements)。這些目標,本身就是設計任何高品質 Go SDK 的重要準則:
1. 完整性 (Complete): 能夠實現 MCP 規範中的所有特性,並嚴格遵循其語義。這是 SDK 作為協定實現的基本要求。
2. 符合 Go 慣例 (Idiomatic): 這是「Go 境界」的核心。SDK 應最大限度地利用 Go 語言自身的特性和標準庫的設計風格,並重複 Go 生態中相似領域(如net/http, grpc-go)已形成的習慣用法。
3. 健壯性 (Robust): SDK 自身必須是經過良好測試、穩定可靠的,並且要能讓使用者輕鬆地對他們基於 SDK 建構的應用進行測試。
4. 面向未來 (Future-proof): 設計必須考慮到 MCP 規範未來可能的演進,盡可能地避免因規範變更而導致 SDK API 發生不相容的破壞性改動。
5. 可擴展性 (Extensible) 與最小化 (Minimal): 為了最好地服務於前述四個目標,SDK 的核心 API 應保持最小化、正交化。同時,它必須允許用戶透過簡單、清晰的方式(如介面、中間件、鉤子等)進行擴展,以滿足特定需求。
這些目標清晰地勾勒出了 Go 團隊對一個「好」的 Go SDK 的期望:它不僅要功能完備,更要「寫起來像 Go,用起來像 Go」,並且能經受住時間的考驗。
庖丁解牛:MCP Go SDK 設計中的「Go 味」與權衡
設定了清晰的 API 設計目標後,Go 團隊便開始將這些原則付諸實踐,著手設計 MCP Go SDK 的具體結構與介面。細細品讀這份設計稿和其原型程式碼,我們能從多個關鍵的決策中,清晰地品味出濃濃的「Go 味」,並深刻體會到他們在功能完備性、語言慣例、當前易用性與未來演進性之間所做的精妙權衡。
包佈局
在 SDK 的整體結構上,Go 團隊針對包的佈局做出了一個顯著的選擇,這直接體現了他們對 Go 生態習慣的深刻理解和對開發者體驗的優先考量。不同於其他語言的 MCP SDK 可能會將客戶端、伺服器端、傳輸層等功能細緻地拆分到各自獨立的包中,Go 團隊提議將 SDK 的核心使用者介面集中在單個mcp包內。
這種做法與 Go 標準庫中的net/http、net/rpc以及社群廣泛採納的google.golang.org/grpc等核心包的組織方式保持了高度一致。對於 Go 開發者而言,這意味著更低的認知門檻——當他們需要使用 MCP 功能時,幾乎所有的核心 API 都能在同一個mcp包下找到,這極大地提升了 API 的發現性。同時,集中的包結構也更利於生成聚合的包文件,並在 IDE 中提供更流暢的程式碼提示與導航體驗。
更深一層的考量,則是為了 SDK 的長期穩定性和面向未來的適應性。如果將功能過度拆分到多個細粒度的包中,未來 MCP 規範的任何微小調整,都可能引發連鎖的包結構變動或複雜的跨包依賴問題。而單一核心包的設計,則能更好地吸收這些變化,減少對使用者程式碼的衝擊。當然,像 JSON Schema 這種與 MCP 核心邏輯不直接相關、但又可能被 SDK 使用者需要的輔助功能,則被合理地規劃到了獨立的子包(如jsonschema/)中,做到了關注點分離。雖然這種策略可能會讓一些追求極致「模組化」的開發者覺得核心包略顯「龐大」,但 Go 團隊在此顯然是權衡了使用者發現性、文件清晰度以及長期演進的穩定性,將它們放在了更高的優先級。
JSON-RPC 與傳輸層抽象 (Transports)
MCP 協定的核心在於透過 JSON-RPC 在客戶端和伺服器端之間交換訊息,而其底層可以有多種傳輸方式,如 stdio、可流式 HTTP、SSE 等。如何為這些形態各異的傳輸方式設計一個統一且靈活的抽象層,是對 SDK 設計者的一大考驗。Go 團隊在這裡再次展現了其對介面設計藝術的嫻熟運用。
在transport.go中,他們定義了一個非常底層的Transport介面: // A Transport is used to create a bidirectional connection between MCP client // and server. type Transport interface { Connect(ctx context.Context) (Stream, error) }
其核心職責僅在於透過Connect方法建立一個邏輯連接,並返回一個Stream介面實例。這個Stream介面則更為基礎,借鑒了golang.org/x/tools/internal/jsonrpc2_v2的設計:
// A Stream is a bidirectional jsonrpc2 Stream. type Stream interface { jsonrpc2.Reader jsonrpc2.Writer io.Closer }
它組合了讀、寫和關閉能力。這種設計充滿了「Go 味」:介面被設計得小巧而精煉,只暴露了最根本的抽象,完美體現了 Go 「定義小介面,實現大價值」的理念。
具體來看,Stream介面因為內嵌了io.Closer,使其自然地遵循了標準庫的慣例,這使得它可以無縫整合到 Go 的資源管理模式中。更重要的是,Connect方法的簽名嚴格遵循了(ctx context.Context, ...params) (...results, error)的形式。context.Context作為第一個參數,用於優雅地處理操作的逾時和取消;而error作為最後一個返回值,則用於明確、一致地傳遞錯誤信息。這些都是 Go I/O 和網路程式設計中雷打不動的標準模式。這種底層介面的簡潔性不僅巧妙地隱藏了內部 JSON-RPC 實現的複雜細節(如mcp/internal/jsonrpc2_v2的使用),也為使用者實現自定義的傳輸方式(如設計稿中提到的InMemoryTransport或LoggingTransport)提供了極大的便利。
例如,NewCommandTransport用於建立透過子進程 stdio 通訊的客戶端傳輸:
// NewCommandTransport returns a [CommandTransport] that runs the given command // and communicates with it over stdin/stdout. func NewCommandTransport(cmd *exec.Cmd) *CommandTransport { /* ... */ }
得到的CommandTransport的Connect方法會啟動命令並連接到其 stdin/stdout。這種清晰的職責劃分和對 Go 標準模式的遵循,使得整個傳輸層易於理解和擴展。
客戶端與伺服器端 API (Clients & Servers)
在客戶端和伺服器端核心物件的 API 設計上,Go 團隊同樣融入了對 Go 並行模型的深刻理解。設計稿清晰地區分了Client/Server實例與ClientSession/ServerSession的概念,這在client.go和server.go中得到了體現。一個Client或Server實例可以處理多個並行的連接,即對應多個會話。這與我們熟悉的標準庫http.Client可以發起多個 HTTP 請求,而http.Server可以同時為多個客戶端提供服務的模式如出一轍。
// In client.go type Client struct { // ... mu sync.Mutex sessions []*ClientSession // ... } func NewClient(name, version string, opts *ClientOptions) *Client { /* ... */ } func (c *Client) Connect(ctx context.Context, t Transport) (*ClientSession, error) { /* ... */ } // In server.go type Server struct { // ... mu sync.Mutex sessions []*ServerSession // ... } func NewServer(name, version string, opts *ServerOptions) *Server { /* ... */ } func (s *Server) Connect(ctx context.Context, t Transport) (*ServerSession, error) { /* ... */ }
這種 N:1(多個會話對應一個 Client/Server 實例)的設計,天然地利用並體現了 Go 語言強大的並行處理能力,透過sync.Mutex保護共享狀態。考慮到 Client 和 Server 本身都是有狀態的(例如,Client 可以動態添加或移除其追蹤的根資源,Server 則可以動態添加或移除其提供的工具),當這些核心實例的狀態發生變化時,設計確保了所有與其連接的對等方(即各個會話)都會收到相應的通知,從而維持了狀態的一致性。
在配置方式上,Go 團隊為Client和Server的建立選擇了使用獨立的ClientOptions和ServerOptions結構體,如:
// In client.go type ClientOptions struct { CreateMessageHandler func(context.Context, *ClientSession, *CreateMessageParams) (*CreateMessageResult, error) ToolListChangedHandler func(context.Context, *ClientSession, *ToolListChangedParams) // ... other handlers } // In server.go type ServerOptions struct { Instructions string InitializedHandler func(context.Context, *ServerSession, *InitializedParams) // ... other handlers and fields like PageSize, LoggerName, LogInterval }
而不是像社群中某些庫(包括設計稿中對比的mcp-go)那樣採用可變參數選項 (variadic options) 的模式。他們認為,對於配置項較多或邏輯較複雜的情況,顯式的結構體選項在可讀性上更勝一籌,也使得包的公開文件更容易組織和理解。這是一個在 API 的簡潔性(可變參數有時更短)與明確性和長期可維護性之間做出的典型且值得借鑒的權衡。
協定類型與 JSON Schema
MCP 協定的訊息體是基於 JSON Schema 定義的。Go SDK 需要將這些 schema 映射為 Go 的結構體。設計稿中提到協定類型是從 MCP 規範的 JSON schema 生成的,並且在mcp包內,除非 API 使用者需要,否則這些類型是未導出的。
以content.go中的Content類型為例:
// Content is the wire format for content. // It represents the protocol types TextContent, ImageContent, AudioContent // and EmbeddedResource. type Content struct { Type string `json:"type"` Text string `json:"text,omitempty"` MIMEType string `json:"mimeType,omitempty"` Data []byte `json:"data,omitempty"` Resource *ResourceContents `json:"resource,omitempty"` Annotations *Annotations `json:"annotations,omitempty"` } func (c *Content) UnmarshalJSON(data []byte) error { // ... custom unmarshaling logic to validate Type field ... } func NewTextContent(text string) *Content { return &Content{Type: "text", Text: text} } // ... other constructors like NewImageContent, NewAudioContent ...
這裡有幾個值得注意的「Go 味」設計:
- 清晰的結構體定義:直接映射 JSON 結構,使用jsonstruct tag 控制序列化行為。
- 建構函式:提供NewXXXContent這樣的輔助函式來建立特定類型的Content實例,確保Type字段被正確設定,提升了易用性和安全性。
- 自訂 JSON 處理:Content類型實現了UnmarshalJSON方法,用於在反序列化時對Type字段進行校驗,確保其為協定定義的合法類型。對於ResourceContents,它甚至實現了MarshalJSON來處理Blob字段nil與空切片的細微差別(為了相容 Go 1.24 之前的omitzero行為)。這種在必要時介入編解碼過程以保證資料正確性的做法,是 Go 類型系統能力的體現。
- json.RawMessage的使用:設計稿提到,對於使用者提供的資料,SDK 會使用json.RawMessage,這樣可以將Marshal/Unmarshal的責任委託給客戶端或伺服器的業務邏輯。這是一種延遲解析的策略,可以提高效能,也增加了靈活性。
此外,jsonschema/子包提供了完整的 JSON Schema 實現,包括從 Go 類型推斷 Schema (infer.go) 和校驗 (validate.go)。jsonschema/generate.go(在建構時忽略) 則展示了如何從遠程的 MCP JSON Schema URL 建立protocol.go中的 Go 類型定義,這體現了程式碼建立的工程實踐。
RPC 方法簽名
對於 MCP 規範中定義的具體 RPC 方法,Go 團隊在 SDK 中的簽名設計上,將一致性和對向後相容的執著追求體現得淋漓盡致。所有這些方法都嚴格遵循func (s *SessionType) MethodName(ctx context.Context, params *XXXParams) (*XXXResult, error)的模式。例如,在client.go中:
// ListPrompts lists prompts that are currently available on the server. func (c *ClientSession) ListPrompts(ctx context.Context, params *ListPromptsParams) (*ListPromptsResult, error) { return standardCall[ListPromptsResult](ctx, c.conn, methodListPrompts, params) }
這裡,context.Context作為第一個參數,error作為最後一個返回值,而參數 (*ListPromptsParams) 和結果 (*ListPromptsResult) 均使用指針類型——這些都是 Go API 設計的「黃金法則」,確保了介面風格的統一和與 Go 生態的無縫對接。
唯一的例外是ClientSession.CallTool方法:
// CallTool calls the tool with the given name and arguments. // Pass a [CallToolOptions] to provide additional request fields. func (c *ClientSession) CallTool(ctx context.Context, name string, args map[string]any, opts *CallToolOptions) (*CallToolResult, error) { /* ... */ }
為了提升使用者直接呼叫工具時的便捷性,它接受工具的名稱字串和map[string]any{}類型的具體參數,以及一個可選的*CallToolOptions,而不是要求使用者預先封裝一個CallToolParams結構體。這是一種在嚴格遵循模式與提升特定場景易用性之間做出的實用性調整。
設計稿中一個特別值得稱道的細節,是對向後相容性的深思熟慮。團隊明確指出:「我們認為,任何需要呼叫者傳遞新參數的規範更改都是不向後相容的。因此,對於目前非必需的任何XXXParams參數,始終可以傳遞nil。」這意味著,即使未來 MCP 規範為某個方法增加了新的可選參數(這些參數會被加入到對應的XXXParams結構體中),現有的、傳遞nil作為參數的呼叫程式碼也無需修改,依然能夠正常工作。這種對 API 演進的未雨綢繆,充分體現了 Go 團隊對相容性承諾的高度重視和豐富經驗。至於為何不直接暴露完整的 JSON-RPC 請求物件,團隊的考量是盡可能隱藏與業務邏輯無關的底層協定細節(如請求 ID),方法名由 Go 方法本身即可隱含,無需在參數中冗餘體現,保持了 API 的純粹性。
錯誤處理 (Errors) 與取消 (Cancellation)
在錯誤處理和操作取消這兩個關鍵機制上,SDK 的設計力求透明化,並與 Go 語言的核心理念保持高度一致。除了工具處理程序自身的業務邏輯錯誤外,所有協定級別的錯誤都會被透明地處理為標準的 Goerror類型。例如,伺服器端特性處理程序中發生的錯誤,會作為錯誤從ClientSession的相應呼叫中傳播出來,反之亦然,使得錯誤處理路徑清晰統一。
為了幫助上層程式碼更精確地理解錯誤的具體性質,設計稿提到協定層面的錯誤會包裝一個JSONRPCError類型(其定義在protocol.go中自動建立),該類型能夠暴露底層的 JSON-RPC 錯誤碼,便於進行針對性的處理。
// (Generated in protocol.go, but conceptually similar to design doc) type JSONRPCError struct { Code int64 `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data,omitempty"` }
而對於操作的取消,則完全依賴並無縫整合了 Go 標準的context.Context機制。在transport.go的call函式中,可以看到這樣的邏輯:
// ... (inside call function) case ctx.Err() != nil: // Notify the peer of cancellation. err := conn.Notify(xcontext.Detach(ctx), "notifications/cancelled", &CancelledParams{ Reason: ctx.Err().Error(), RequestID: call.ID().Raw(), }) return errors.Join(ctx.Err(), err) // ...
當客戶端程式碼取消一個傳遞給 SDK 方法的context時,SDK 會負責向伺服器發送一個 "notifications/cancelled" 通知,同時客戶端的該方法呼叫會立即返回ctx.Err()。相應地,伺服器端在處理該請求時,其持有的context會被取消,從而可以進行適當的清理或中止操作。這種設計讓熟悉 Go 並行程式設計的開發者在處理取消邏輯時倍感親切和自然,無需學習新的機制。
可擴展性:中間件模式的青睞
為了滿足使用者對 SDK 功能進行定制和擴展的需求,同時保持核心 API 的簡潔性,Go 團隊在可擴展性機制設計上也體現了其偏好。在伺服器端(server.go)和客戶端(client.go),都提供了AddMiddleware方法:
// In shared.go (conceptual definition) type MethodHandler[S ClientSession | ServerSession] func( ctx context.Context, _ *S, method string, params any) (result any, err error) type Middleware[S ClientSession | ServerSession] func(MethodHandler[S]) MethodHandler[S] // In server.go func (s *Server) AddMiddleware(middleware ...Middleware[ServerSession]) { /* ... */ } // In client.go func (c *Client) AddMiddleware(middleware ...Middleware[ClientSession]) { /* ... */ }
這些方法允許使用者註冊一個或多個遵循特定簽名的Middleware函式。這些函式本質上構成了 MCP 協定級別的中間件 (middleware) 鏈,它們會在伺服器/客戶端收到請求、請求被解析之後,但在進入正常的業務處理邏輯之前依次執行(從右到左應用,即第一個中間件最先執行)。mcp_test.go中的traceCalls就是一個很好的範例,它展示了如何用中間件來記錄請求和回應。
這種設計與 Go Web 開發(如net/http的HandlerFunc鏈)以及許多其他 Go 生態庫中廣泛採用的中間件模式一脈相承。它提供了一種強大且靈活的方式來注入橫切關注點,如日誌記錄、認證、請求修改等。相比之下,社群的mcp-go實現(如設計稿中提到的)定義了多達 24 個具體的 Server Hooks,每個 Hook 對應一個特定的事件點。Go 團隊的選擇顯然更傾向於透過一種更為通用和模式化的方式來滿足擴展需求,從而避免了在核心 Server/Session 類型上暴露過多的、細粒度的鉤子方法,保持了其介面的最小化和正交性。而對於像 HTTP 級別的身份驗證這類與 MCP 協定本身不直接相關的橫切關注點,設計稿則推薦使用標準的 HTTP 中間件模式來處理,進一步體現了關注點分離和利用現有生態成熟方案的設計思想。
透過對這些設計細節的「庖丁解牛」,我們不難發現,Go 團隊在打造這個 MCP SDK 的過程中,無時無刻不在思考如何將 Go 語言的設計哲學、慣用模式以及對工程實踐的深刻理解融入其中,力求在滿足協定規範的完整性的同時,為 Go 開發者提供一個簡潔、健壯、易用且面向未來的程式設計介面。
API 設計的「Go 境界」:我們能學到什麼?
Go 團隊對 MCP SDK 的設計過程,如同一面鏡子,映照出 API 設計的諸多考量和 Go 語言的獨特氣質。從中,我們可以提煉出一些寶貴的啟示:
1. 「Go 味」始於目標:完整性、符合慣例、健壯性、面向未來、可擴展與最小化——這些目標共同構成了設計優秀 Go API 的基石。
2. 標準庫是最好的老師:學習並模仿 net/http, io, context 等核心庫的設計模式和 API 風格,是通往「Idiomatic Go」的捷徑。
3. 介面的力量:用小而美的介面來抽象行為、解耦組件,是 Go 設計哲學的精髓。
4. context 與 error 的「一等公民」地位:在任何涉及 I/O、並行或可能失敗的操作中,將它們融入 API 設計是標準做法。
5. 向後相容性是生命線:API 一旦發佈,就需要慎重對待變更。在設計之初就考慮未來的演進,預留擴展點,比事後打補丁要優雅得多。
6. 權衡的藝術:API 設計充滿了權衡——簡潔性與表達力、靈活性與易用性、目前需求與未來可能……沒有絕對的「正確」,只有在特定上下文下的「更優」。Go 團隊在包佈局、配置方式等方面的選擇,都體現了這種權衡。
小結
API 設計沒有銀彈,更像是一門手藝,需要在不斷的實踐、反思和學習中精進。Go 團隊為 MCP SDK 所做的這些思考和設計決策,為我們提供了一個寶貴的學習範例,展示了如何在 Go 的世界裡,打造出既滿足複雜需求,又不失簡潔與優雅的 API。
這種對「Go 境界」的追求——即程式碼不僅能工作,而且寫得像 Go、用得像 Go,感覺像 Go——正是 Go 語言強大生命力和獨特魅力的源泉。
希望這篇文章能為你未來的 API 設計帶來一些啟發。也歡迎你在評論區分享你對 API 設計的理解,或者你認為一個「好的 Go API」應該具備哪些特質。
參考資料地址:https://github.com/orgs/modelcontextprotocol/discussions/364
精進有道,更上層樓:解鎖 Go API 設計的「Go 境界」
對今天的 Go API 設計案例意猶未盡?想系統學習,將 Go 官方的設計智慧融入你的每一個介面嗎?
我在最新上架的Go語言進階課中,特設「API 設計:建構使用者喜愛、健壯可靠的公共介面」一講。它將為你深入剖析 Go API設計的五大核心要素,並結合更多實戰案例,助你從「會用 Go」邁向「精通 Go」。
掃描下方二維碼,立即開啟你的進階之旅!
感謝閱讀!
如果這篇文章讓你對 Go API 設計有了新的認識,請幫忙點讚👍、在看👀、轉發🚀,讓更多朋友一起學習和進步!
點擊下面標題,閱讀更多乾貨!
- 手把手帶你玩轉GOEXPERIMENT=jsonv2:Go下一代JSON庫初探
- AI新寵?解讀MCP、A2A為何偏愛JSON-RPC 2.0
- Go encoding/json/v2提案:JSON處理新引擎
- 我來告訴你Go專案標準結構如何佈局
- 一文搞懂Go標準庫context包
- gRPC服務的回應設計
- 「錯誤即值」,不同實現:Go與Zig錯誤處理哲學對比