天天看點

go post 參數_在 Go 中踐行 ASP.NET WebAPI 的經驗

go post 參數_在 Go 中踐行 ASP.NET WebAPI 的經驗

大規模路由表與混雜模式的業務邏輯

公司内不少使用 Golang 編寫的業務,目前所在的部門使用 Go 開發業務 WebAPI 的時大多數采用原生的 HTTPHandler

func(w http.ResponseWriter, r *http.Request)

或者使用 gin 架構。于是便在代碼中看到長達數頁的路由系統資料庫:

router.Group("/v1", func() {
    router.POST("/api/1", middlewares...)
    router.POST("/api/2", middlewares...)
    router.POST("/api/3", middlewares...)
    router.POST("/api/4", middlewares...)
    router.POST("/api/5", middlewares...)
    router.POST("/api/6", middlewares...)
    router.POST("/api/7", middlewares...)
    router.POST("/api/8", middlewares...)
    router.POST("/api/9", middlewares...)
    router.POST("/api/10", middlewares...)
    router.POST("/api/11", middlewares...)
    router.POST("/api/12", middlewares...)
    router.POST("/api/13", middlewares...)
    router.POST("/api/14", middlewares...)
    router.POST("/api/15", middlewares...)
...
}
           

不乏拼寫錯誤和重複注冊。雖然這樣看上去直覺,似乎很好排查錯誤,但是實際使用中發現,這樣的大型路由系統資料庫往往能讓錯誤排查更加複雜:

  1. 業務邏輯中混雜了大量的序列化反序列化操作;
  2. 防禦性程式設計與業務混編處理,某些錯誤處理不當導緻不具合參數依然進入了後繼邏輯;
  3. 需要手動調用不少工具函數通過辨別擷取會話對象,出現大量需要約定的臨時存儲空間;
  4. 雖然代碼即文檔的扁平路由表可以更直覺看到 API 路徑,但是實際需要的參數、正文接納的類型依然無法直覺感覺;
  5. 基礎參數通過 Query/Header 傳遞,對于日志記錄和遙測分析來說并不友好。

另外,還有:

  • 與用戶端回話的加密解密手動處理;
  • 無法生成符合 OpenAPI 協定的文檔做到代碼即文檔;

等等一些問題不大,但是确實讓人不爽的地方。曾經使用層疊路由表去解決大規模系統資料庫,但是發現,注冊層次清晰了,卻帶來了大量的子檔案,查端點異常辛苦,跟俄羅斯套娃似的,一套又一套。

.NET 與 Java 現場 Battle

Golang 這樣的開發模式對于很多以前使用 http://ASP.NET WebAPI、Spring Boot 的開發者來說,變化确實有點大,相對于

func(w http.ResponseWriter, r *http.Request)

來說,C# 與 Java 前輩的代碼更加直覺并且省時省力:

namespace WebApp.Controllers {
    public class AdminController : Controller {
        WebAppContext entity = new WebAppContext();
        //POST
        [HttpPost]
        public ActionResult NewUser(User usr) {
            //Business logic
            return View(usr.Id);
        }
    }
}
           

運作起來的時候,通路位址就是:

(basepath)/admin/newuser
           

寫業務代碼的時候直接使用

usr

這個傳入參數,不需要像 Golang 那樣去做:

json.NewDecoder(request.Body).Decode(&obj)
           

每個函數都要這麼處理,枯燥備援并且更容易出錯。另外,http://ASP.NET MVC 中,如果方法還有 Int32、String 之類的參數,是可以展現在 URL 中的。例如:

public ActionResult Result(Int32 Id) {
    //logic
    return View("Detail", t);
}
           

通路的方法可以直接是:

(basepath)/result/{Id}
           

Go × WebAPI

同樣是 WebAPI 開發,Go 可不可以這樣的?我們簡單翻譯一下上面的 AdminController,用 Go 寫的話,應該這樣:

type AdminController struct {
    webapi.Controller
    entity *WebAppContext
}

func (controller *AdminController) Init() error {
    entity = infrastructure.DatabaseConnection
}

func (*AdminController) NewUser(usr *models.User) ActionResult {
    //Business logic
    return ActionResult { Body: usr.ID }
}
           

對于 URL 參數化,最好還可以:

func (*AdminController) Result(id int) (result ActionResult) {
    //logic
    return
}
           

出于這樣的目的,寫了一個簡單的基礎類庫:go-webapi/webapi。

介紹 go-WebAPI

自動路由注冊與複合控制器

聚焦業務核心回避無關的規則,DDD 與 MVVM 等設計模式高度相容。不同業務子產品擁有自身控制器(組),支援子產品細分與多子產品統一整合的設計(允許多人協同)。基于約定的自動路由注冊降低備援代碼,提高設計效率同時回避公開位址與内部實作失去同步的可能。

聲明控制器:

type Article struct {
	webapi.Controller
}
           

聲明接入點:

func (article *Article) Show(query struct {
  GUID string `json:"guid"`
}) string {
	return fmt.Sprintf("you are reading post-%s", query.GUID)
}
           
WebAPI 遵循 Golang 原則,但凡可通路的方法(大寫字母開頭的函數)均會被注冊為 API 接入點。

接入點會被注冊為

/article/show?guid=[guid]

,如果有多個控制器處理不同業務,那麼可以通過

RouteAlias() string

方法來指定控制器别名:

type article struct {
	webapi.Controller
	id uint
}

func (article *article) RouteAlias() string {
	return "article"
}
           

然後不管在

article

還是

Article

下的方法均會注冊到

/article

下。不過需要注意的是,此類注冊器需要回避重名問題。不過在運作時 WebAPI

會針對重名方法做出緻命警告 (panic)。

使用時,不妨将各個業務子產品細分給不同的人去完成。最後通過

RouteAlias

可以輕松将他們整合在一起。

查詢/正文自動序列化與反序列化支援

完全消除在業務邏輯中的正文讀取、序列化/反序列化、查詢檢索與轉化,借助于中間件,甚至還可以配置 MsgPack 等非系統内建/私有化序列器。

我們使用

curl

通路一下剛才的 API:

~ curl http://localhost:9527/article/show?guid=79526
#you are reading post-79526
           

可以看到參數自動放到了

query

中。亦可支援正文自動序列化,例如聲明方法:

func (article *article) Save(entity *struct {
	ID         uint
	Title      string
	Content    string
	CreateTime time.Time
}, query struct {
	CreateTime string `json:"time"`
}) {
	entity.CreateTime, _ = time.Parse("2006-01-02", query.CreateTime)
	entity.ID = article.id
	article.Reply(http.StatusAccepted, entity)
}
           

這個方法将會注冊為

[POST] /article/{digits}/save

,因為存在

*struct{}

結構,是以預設為 POST,但是可以通過

method[HTTPMETHOD] struct

的私有字段的形式去顯式聲明 HTTP 方法。同樣使用

curl

~ curl -X "POST" "http://localhost:9527/article/123/save?time=2019-01-01" 
     -H 'Content-Type: application/json; charset=utf-8' 
     -d $'{
  "Title": "Hello WebAPI for Golang",
  "Content": "Awesome!"
}'

#{"ID":123,"Title":"Hello WebAPI for Golang","Content":"Awesome!","CreateTime":"2019-01-01T00:00:00Z"}
           

可以看到查詢中的時間成功被通路并賦到了正文。也請留意到,不管是之前使用的

string

作為傳回值,還是這個節點的,手動使用

.Reply(STATUSCODE, INTERFACE{})

都可以自動處理并回複給用戶端。

序列化器可以手動指定。在上下文 (Context) 的 Serializer 屬性中指定。

同時,查詢和正文的結構支援檢查,為他們添加

Check() error

方法即可在進入業務代碼之前檢查資料的合法性,将防範性編碼與業務隔離開來。

路由前置條件(前參數化通路)支援

收束控制器處理資料範疇設立 API 通路準入門檻。提供其他路由服務無法提供的比對-回落和具體業務控制器前置條件能力,從根本隔離開非法通路,降低出錯幾率,提高業務編碼效率并提高系統魯棒性。

剛才的請求中我們看到,通路位址

/article/123/save

中的

123

被捕獲并且最後在回複正文的

ID

中出現。WebAPI 允許為控制器設立前置條件(Precondition),聲明的方法:

func (article *article) Init(id uint) (err error) {
	article.id = id
	return
}
           

隻需要為控制器聲明傳回值為

error

且名稱為

Init

的方法即可自動在進入實際方法前調用它。節點注冊形式也發生了些許變更。如果參數為

  • 但凡整型(Int)會得到一個

    /{digits}

    的注冊點;
  • 所有浮點(Float)那麼将會得到一個

    /{float}

    的注冊點;
  • 布爾值(Bool)會産生

    /{bool}

    注冊點 ;
  • 字元串(String)将會得到一個 /{string} 的注冊點(回落注冊點)。

是以上面的

Init(uint) error

函數将會産生

/{digits}

的注冊節。

如果調用函數傳回的錯誤值不為空,那麼将會通知用戶端 Bad Request。這從側面區分了對象方法(Object Method)和靜态方法(Static Method),編碼時可以更關注業務本身而不用去操心各種前置條件審查,或節省大量近似的代碼。

端點條件(後參數化通路)支援

不需要反向代理配置僞靜态即可提供原生支援參數化通路,提供更直覺簡潔的 API。

既然前參數化通路都支援,那麼自然後參數化通路也可以。剛才我們遇到,通路文章正文需要使用查詢參數,雖然可以正常工作,但是未免顯得太過單調。通過前置參數支援需要使用

Read

一類的方法,感覺不自然。我們可以通過後置參數的形式來提供形如

/article/{guid}

的通路形式:

func (article *Article) Index(guid string) string {
	return fmt.Sprintf("you are reading post-%s", guid)
}
           

使用

curl

測試一下:

~ curl http://localhost:9527/article/id-233666
#you are reading post-id-233666
           

⚠️ 注意

此方法也可以通過

func (article *article) Index(id int)

的方法實作。在本例中兩個方法允許共存,因為前者為

/article/{string}

後者為

/article/{digits}

。在協作的時候務必注意此類問題。如果出現重複注冊節點,控制器将會注冊失敗并提示錯誤。

中途加密政策支援

完全可托付的原生加密解密,相容密鑰協商機制,即使密鑰變更或不唯一,隻需一次設定,流水線明文密文處理更加可靠,完全杜絕因為疏忽或者意外造成的機密中繼資料洩露的可能,資料安全高枕無憂。

加解密服務依托于上下文,可以在中間件中指定上下文中的

CryptoService

來實作提供統一的加解密服務,将無關業務的加解密方法獨立出去,提高開發者效率。

方法亦可在使用中途更改,即此加密解密的子產品是動态可替換的。