用 Go 開發介面服務--保證高效能專案的法寶
我們在《準備專案所需的 Go 類包》章節裡,選擇所需的類包,其實是為保證專案的高效能做好了準備。整個專案自上而下,我們都儘量避免產生效能損耗發生的情況。
最頂層路由器部分,是終端每次請求服務必經的模組,我們直接放棄了第三方的路由器,採用 Go 原生內建的 ServeMux 路由器, 並全面禁止在 URL 上攜帶任何引數,每個 URL 在部署之前都是已固定了。採用此方案後,我們的 chapter01 專案和現在比較流行的 RESTfull 架構,有了較大的區別。RESTfull 在路由器上使用了一定的約定,把 URL 動態引數和請求方式結合起來使用,產生靈活的路由用法。我們的方案卻類似相反的,我們禁止了 URL 任何形式的引數,避免路由器複雜的字串匹配和 Go 的反射功能,請求方式也採用了單一的 POST 方式,忽略了 PUT、DELETE、OPTIONS 一系列的請求方式,保持路由的簡單性。而引數都是通過 JSON 直接傳遞給控制器。 正是因為我們的路由器足夠簡單單一,所以我們使用 ServeMux 已足夠,如果採用 RESTfull 架構,恐怕需要依賴一些第三方類包,比如 :
- gorilla/mux:github.com/gorilla/mux
- httprouter:github.com/julienschmidt/httprouter
gorilla/mux 追求可擴充套件性,用法傾向於 Go 理念;httprouter 追求更高效能,獨創性的嵌入式使用方式。這裡我們展示這三者的簡單使用方式:
import ( "net/http" "github.com/gorilla/mux" "github.com/julienschmidt/httprouter" ) // Go 原生內建 ServeMux 的使用 router := http.NewServeMux() router.HandleFunc("/api/v1/products", controller.ProductList) router.HandleFunc("/api/v1/products/detail", controller.ProductDetail) // gorilla/mux 的使用 router := mux.NewRouter() router.HandleFunc("/api/v1/products", controller.ProductList) router.HandleFunc("/api/v1/products/{id:[0-9]+}", controller.ProductDetail) // httprouter 的使用 router := httprouter.New() router.HandleFunc("/api/v1/products", controller.ProductList) router.HandleFunc("/api/v1/products/:id", controller.ProductDetail)
路由器採用 Go 原生內建的 ServeMux,並禁止 URL 攜帶引數,直接帶來的效應是:超高效能,極低記憶體損耗,更加符合 Go 的使用習慣。截止目前為止還沒有哪個路由器的高效能和低消耗可以超過 Go 原生的 ServeMux。
有人說 Go 語言的反射是一把雙刃劍,它既可以保證靈活性,也能保證一定的效能。但 Go 還算是一門比較年輕的語言,她的反射機制還是有很大的提升空間的,她的反射尚未能做到效能上的極致,所以我們暫時儘量避免使用它,類包的選擇上,我們的原則也是一樣的,比如 http 中介軟體 martini 和 negroni,我們選了 negroni。雖然兩者都出自同一個作者,但 martini 使用了大量的反射機制,整個底層架構都是基於反射構造出來的,以至於 martini 在實際專案中,很多人反饋它存在致命的效能缺陷,這點也得到了作者的共識和反思,所以才會有後來的negroni 誕生。為了吸取教訓,我們專案選擇了 negroni,開發過程中很明顯得到了很好的體驗,第一是它非常簡單實用,第二是它非常接近原生的 net/http 的使用方式,可相互切換,第三是它的效能和損耗,跟 net/http 是幾乎相同的。
通常一個專案,訪問壓力都集中在資料的存取上,這和資料庫軟硬體配置、資料層編碼直接有關,除了資料庫外部設施需提供足夠的抗壓能力外,我們開發人員也需要重視 dao 資料層的效能,儘可能避免效能的損耗,所以我們選擇了 sqlx 作為資料的存取方案。sqlx 與原生 database/sql 高度相容,另外還做了許多擴充套件,相對於 database/sql 效能損耗 2%-3% 左右,效能非常優秀。
*程式碼清單 - sqlx 和原生 database/sql 保持相容的部分函式*
Exec(...) (sql.Result, error) // 和 database/sql 的一樣 Query(...) (*sql.Rows, error) // 和 database/sql 的一樣 QueryRow(...) *sql.Row // 和 database/sql 的一樣
*程式碼清單 - sqlx 擴充套件的 Get、Select函式*
<em>p := Place{} // this will pull the first place directly into p err = db.Get(&p, "SELECT * FROM place LIMIT 1") pp := []Place{} // this will pull places with telcode > 50 into the slice pp err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)</em>
sqlx 保證高效能的同時仍然保持原生 database/sql 的使用方式,是非常好的選擇,在此方案基礎上再增加它靈活性,我們可以使用 sqrl 來做輔助生成 sql 語句和引數,使用起來和 orm 一樣靈活和便利,並可以做到非嵌入的結構體,又不損耗效能。
*程式碼清單 - sqrl 使用的部分例子*
<em>import sq "github.com/elgris/sqrl" users := sq.Select("*").From("users").Where(sq.Eq{"status": 1}) sql, args, err := users.ToSql() // 生成的 sql 語句 sql == "SELECT * FROM users WHERE status=?" // 生成的引數 args == [1] sql, args, err := sq. Insert("users").Columns("name", "age"). Values("moe", 13).Values("larry", sq.Expr("? + 5", 12)). ToSql() // 生成的 sql 語句 sql == "INSERT INTO users (name,age) VALUES (?,?),(?,? + 5)"</em>
資料層採用的方案,是目前我覺得最滿意的,它既保證了高效能的底層資料存取,又不缺乏操作上的靈活性,實體結構體又能做的很漂亮。
頂層、中間層和底層,我們都做了嚴格的技術考量和實施,減少專案效能損耗,保證穩定的高效能。實際上我們專案部署完成後,在伺服器極端的環境下,一直長期保持服務高效能、低損耗的執行著,而且在業務交易不斷增長的過程中沒有出現過任何中斷的問題。