傳統的 API 調用方式,存在不少困擾。簡單舉幾個常見問題:
- 許多應用的 API 沒有提供文檔,調用端完全不清楚請求參數的格式、返回數據的結構以及規範內容。
- 發佈 API 時,往往需要額外配備文檔(例如 Swagger)。然而多數開發者自己都難以寫出質量穩定的文檔,品質參差不齊。
- 文檔存放位置也是難題。即便有文檔,也未必能順利找到;部分文檔因網絡限制或權限問題而無法存取。
- 文檔與實際 API 經常不對應,更新不及時,甚至可能產生多種解釋歧義。
- 文檔管理本身就很棘手,遺失或遺忘的情況屢見不鮮。
- 開發者與調用方之間協作不順,彼此誤解的情形時常發生。
問題遠不只這些,上述僅是冰山一角。而 MetaMessage 的出現,或許能從根本上扭轉這個現狀。
MetaMessage 的核心特性是:自描述、自規範、自示例。舉例來說,輸出為文本格式 jsonc:
{// mm: type=datetime; desc=創建時間"create_time": "2026-01-01 00:00:00",}
看起來很簡單——type 明確了數據格式,各語言實作一致,不會出現某種語言把日期解釋為字串的尷尬;desc 可以詳細描述欄位的含義與使用方式。當然還有更多標籤,它們共同精準、完整地定義一份數據。
開發者可以直接從各語言的數據物件生成 mm,也可以透過 jsonc 字串來生成,兩種方式得到的數據完全一致。說到這裡,其實多語言之間的數據比對一直很困難,甚至可以說沒有一個真正有效的方法能辦到,這對測試來說是很大的問題。而如果都轉成 mm,比對文字就方便多了。
此外還有一些優點:例如可以產生緊湊的二進位格式,也可以產生更多文字格式。未來,一個 mm 就能同時輸出 jsonc、toml、yaml 等格式,作為配置檔案的轉換工具簡直不要太實用。
扯遠了,回歸正題。
調用 API 的時候,能不能直接獲取它的請求方法?這裡想到一個思路:利用 RESTful 的 OPTIONS 方法。OPTIONS 原本設計為資源探測,但幾十年來,大家基本上只用它來處理 CORS 跨域問題——太浪費了。而且瀏覽器預設會在 POST 等請求之前自動發送一次 OPTIONS,開發者根本無法取消,想想還挺荒謬的。如果將請求參數的結構資訊放在 OPTIONS 回傳的 body 中,調用方就能取得即時、準確的請求規範。
OPTIONS /api/v1/users/1:// mm: example{// mm: nullable; desc="用戶名稱""name": "",// mm: type=email; nullable; desc="電子郵箱""email": "",// mm: type=u8; nullable; desc="年齡""age": 0,// mm: nullable; desc="是否激活""is_active": false,}
這是一個修改用戶的 API。看到這個結果,我們就能知道:name 可以為 null,age 必須是 uint8 類型,整體上還能看出這是一組 example 示例數據。只要發送的請求完全符合這個描述,就是安全合法的。
實戰應用
讓我們來實作一個 Go Gin 的中間件,達到以下功能:
- 對 POST、PUT、PATCH 方法,將請求參數自動包裝並暴露到 OPTIONS 回應中。
- 自動以 mm 格式對請求進行編碼。
- 對返回結果自動解碼。
為了方便使用與示範,再製作一個用戶端:
- 請求時自動探測 schema(即發出 OPTIONS 請求)。
- 自動驗證請求的合法性。
伺服端與用戶端都會自動進行驗證。
伺服端範例:
...type CreateUserRequest struct {Namestring `mm:"desc=用戶名稱; min=1; max=50"`Email string `mm:"type=email; desc=電子郵箱"`Age uint8`mm:"desc=年齡; min=0; max=150"`}mmgin.POST("/users", createUser)func createUser(c *gin.Context, req *CreateUserRequest) {newUser := User{ID: int64(len(users) + 1),Name: req.Name,Email:req.Email,Age:req.Age,IsActive: true,}users = append(users, newUser)mmgin.RespondWithStatus(c, http.StatusCreated, APIResponse{Code:0,Message: "user created",Data:&newUser,}, "")}...
用戶端範例:
...type CreateUserRequest2 struct {Namestring `mm:"desc=用戶名稱; min=1; max=50"`Email string `mm:"type=email; desc=電子郵箱"`Age uint8`mm:"desc=年齡; min=0; max=150"`}createReq := &CreateUserRequest2{Name:"Da vid",Email: "da vid@example.com",Age: 28,}resp3, err := client.POST[CreateUserRequest2, APIResponse]("/api/v1/users", createReq)if err != nil {fmt.Printf("[Error] %vn", err)return}fmt.Printf("[OK] Message: %sn", resp3.Message)fmt.Printf(" New User: %+vn", resp3.Data)...
可以發現,使用起來非常直觀,甚至可以說是無感的。完整範例可參閱 metamessage/mm-gin 專案。
可測試的結果是:只要請求數據不合法,在 OPTIONS 階段就會失敗。請注意這是強一致性的即時驗證。
未來展望
API 不再需要單獨撰寫文檔——程式碼本身就是文件。每個 API 接口自動產生一個 schema。遇到陌生的 API 不用擔心,自動獲取請求參數;API 更新了也無妨,自動拿到最新的請求參數。
尤其值得一提的是瀏覽器——誰能想到 OPTIONS 還能這樣運用?如果瀏覽器能內建這種機制,對各種 API 的調用幫助會非常大。
MetaMessage 作為基礎協定,未來將在更多場景中持續發揮作用。
