自從go語言r59版本(一個1.0之前的版本)以來,我一直在寫Go程序,並且在過去七年裏一直在Go中構建HTTP API和服務.
多年來,我編寫服務的方式發生了變化,所以我想分享今天如何編寫服務 - 以防模式對您和您的工作有用.
1. Server Struct
我的所有組件都有一個server
結構,通常看起來像這樣:
type server struct {
db * someDatabase
router * someRouter
email EmailSender
}
共享依賴項是結構的字段
2. routes.go
我在每個組件中都有一個文件routes.go
,其中所有路由都可以存在:
package app
func(s * server)routes(){
s.router.HandleFunc("/ api/",s.handleAPI())
s.router.HandleFunc("/ about",s.handleAbout())
s.router .HandleFunc("/",s.handleIndex())
}
這很方便,因爲大多數代碼維護都是從URL
和錯誤報告
開始的,所以只需一眼就routes.go
可以指示我們在哪裏查看.
3. server 掛載 handler
我的HTTP
server 掛載 handler
:
func(s * server)handleSomething()http.HandlerFunc {...}
handler可以通過s服務器變量訪問依賴項.
4. return Handler
我的處理函數實際上並不處理Request,它們返回一個handler函數.
這給了我們一個閉包環境
,我們的處理程序可以在其中運行
func(s * server)handleSomething()http.HandlerFunc {
thing:= prepareThing()
return func(w http.ResponseWriter,r * http.Request){
// use thing
}
}
該prepareThing
只調用一次,所以你可以用它做一次每處理程序初始化,然後用thing在處理程序.
確保只讀取共享數據,如果處理程序正在修改任何內容,請記住您需要一個互斥鎖或其他東西來保護它.
5. 參數是handler函數的依賴
如果特定處理程序具有依賴項,請將其作爲參數.
func(s * server)handleGreeting(format string)http.HandlerFunc {
return func(w http.ResponseWriter,r * http.Request){
fmt.Fprintf(w,format,"World")
}
}
format處理程序可以訪問該變量.
6. HandlerFunc over Handler
我http.HandlerFunc
現在幾乎用在每一個案例中,而不是http.Handler
.
func(s * server)handleSomething()http.HandlerFunc {
return func(w http.ResponseWriter,r * http.Request){
...
}
}
它們或多或少是可以互換的,所以只需選擇更容易閱讀的內容.對我來說,就是這樣http.HandlerFunc.
5. Middleware中間件
中間件函數接受http.HandlerFunc並返回一個可以在調用原始處理程序之前和/
或之後運行代碼的新函數 - 或者它可以決定根本不調用原始handler.
func(s * server)adminOnly(h http.HandlerFunc)http.HandlerFunc {
return func(w http.ResponseWriter,r * http.Request){
if!currentUser(r).IsAdmin {
http.NotFound(w,r)
return
}
h(w,r)
}
}
處理程序內部的邏輯可以選擇是否調用原始處理程序 - 在上面的示例中,如果IsAdmin
是false
,HandlerFunc將返回HTTP 404 Not Found
並返回(abort
); 注意沒有調用h
處理程序.
如果IsAdmin
是true
,則將執行傳遞給傳入的h處理程序.
通常我在routes.go
文件中列出了中間件:
package app
func(s * server)routes(){
s.router.HandleFunc("/ api
/",s.handleAPI())s.router.HandleFunc("/ about",s.handleAbout())
s.router .HandleFunc("/",s.handleIndex())
s.router.HandleFunc("/ admin",s.adminOnly( s.handleAdminIndex()))
}
7. Request 和 Response類
如果Server有自己的請求
和響應
類型,通常它們僅對該特定Handler有用.
如果是這種情況,您可以在函數內定義它們.
func(s * server)handleSomething()http.HandlerFunc {
type request struct {
Name string
}
type response struct {
Greeting string`json :"greeting"`
}
return func(w http.ResponseWriter,r * http.Request){
. ..
}
}
這會對您的包空間
進行整理,並允許您將這些類型命名爲相同
,而不必考慮特定於處理程序的版本.
在測試代碼中,您只需將類型複製到測試函數中並執行相同的操作即可.要麼…
8. 測試框架
如果您的請求/響應
類型隱藏在處理程序中,您只需在測試代碼中聲明新類型即可.
這是一個爲需要了解您的代碼的後代做一些故事講述的機會.
例如,假設Person我們的代碼中有一個類型,我們在許多端點上重用它.如果我們有一個/greet
endpoint,我們可能只關心他們的名字,所以我們可以在測試代碼中表達:
func TestGreet(t * testing.T){
is:= is.New(t)
p:= struct {
Name string`json :"name"`
} {
Name:"Mat Ryer",
}
var buf bytes.Buffer
err: = json.NewEncoder(&buf).Encode(p)
is.NoErr(err)// json.NewEncoder
req,err:= http.NewRequest(http.MethodPost,"/ greet",&buf)
is.NoErr(err)
/ / ...這裏有更多測試代碼
從這個測試中可以清楚地看出,我們關心的唯一領域就是Name人.
9. sync.Once 配置依賴項
如果我在準備處理程序時必須做任何昂貴的事情,我會推遲到第一次調用該處理程序時.
這改善了應用程序啓動時間
func(s * server)handleTemplate(files string ...)http.HandlerFunc {
var(
init sync.Once
tpl * template.Template
err error
)
return func(w http.ResponseWriter,r * http.Request){
init.Do (func(){
tpl,err = template.ParseFiles(files ...)
})
if err!= nil {
http.Error(w,err.Error(),http.StatusInternalServerError)
return
}
// use tpl
}
}
10. sync.Once 確保代碼只執行一次
其他調用(其他人發出相同的請求)將一直阻塞,直到完成.
錯誤檢查在init函數之外,所以如果出現問題我們仍然會出現錯誤,並且不會在日誌中丟失錯誤
如果未調用處理程序,則永遠不會完成昂貴的工作 - 根據代碼的部署方式,這可能會帶來很大的好處
請記住,執行此操作時,您將初始化時間從啓動時移至運行時(首次訪問端點時).我經常使用Google App Engine,所以這對我來說很有意義,但是你的情況可能會有所不同,所以值得思考何時何地使用sync.Once這樣.
11. 服務器是可測試的
我們的服務器類型非常可測試.
func TestHandleAbout(t * testing.T){
is:= is.New(t)
srv:= server {
db:mockDatabase,
email:mockEmailSender,
}
srv.routes()
req,err:= http.NewRequest("GET" ,"/ about",nil)
is.NoErr(錯誤)
w:= httptest.NewRecorder()
srv.ServeHTTP(w,req)
is.Equal(w.StatusCode,http.StatusOK)
}
在每個測試中創建一個服務器實例 - 如果昂貴的東西延遲加載,這將不會花費太多時間,即使對於大組件
通過在服務器上調用ServeHTTP
,我們正在測試整個堆棧,包括路由和中間件等.如果你想避免這種情況,你當然可以直接調用處理程序方法.
使用httptest.NewRecorder
記錄什麼處理程序在做
此代碼示例使用我的測試迷你框架(作爲Testify
的迷你替代品)