Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
用Golang流行的web框架Gin渲染HTML模板頁面的簡單例子
Gin是Golang最流行的web框架之一。我之前已經寫過如何使用Golang基礎模板包渲染HTML頁面。使用Gin渲染HTML模板更加簡單。
為了使工作流更加順暢,嘗試新的想法并進行調試,我還決定使用Codegangsta的自動重載工具Gin。
安裝Gin HTTP web框架就像安裝大多數(如果不是所有)Golang包一樣簡單:
go get -u github.com/gin-gonic/gin
package main
import (
"html/template"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func main() {
router :=gin.Default()
router.SetFuncMap(template.FuncMap{
"upper": strings.ToUpper,
})
router.Static("/assets", "./assets")
router.LoadHTMLGlob("templates/*.html")
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"content": "This is an index page...",
})
})
router.GET("/about", func(c *gin.Context) {
c.HTML(http.StatusOK, "about.html", gin.H{
"content": "This is an about page...",
})
})
router.Run("localhost:8080")
}
在第4到7行,我們引入了一些包:
在第11行,我們創建了一個名為router的默認Gin路由。默認的Gin路由使用日志和恢復中間件,以及基本功能。
在第12到14行,使用SetFuncMap()創建一個供模板使用的函數映射。這里我們添加了一個簡單的模板函數upper,它使用strings.ToUpper()函數將字符串中的所有字符設置為大寫。
在第15行,我們讓Gin路由知道我們在./assets目錄中保存了一些靜態資產。Gin可以以這種方式訪問任何靜態資源。
在這個例子中,我在那個目錄中放了一個Bulma CSS庫的最小版本。通過使用Static()函數,現在HTML模板可以訪問這個庫。
在第16行,所有滿足template/*.html模式的模板都由LoadHTMLGlob()函數加載。這個模式意味著模板文件應該有.html的擴展名,并且位于/template目錄中。
在第18到22行,我們告訴Gin路由接受URL路徑/上的HTTP GET方法請求。當收到請求時,Gin發送一個HTTP OK狀態消息,并用gin.H{}括號內提供的數據渲染index.html模板。在這種情況下,數據只包括一個鍵/值對,鍵名為content。
在第24到28行,與上面類似,我們告訴Gin路由接受/about路徑上的HTTP GET方法請求。這次渲染的是about.html模板。
在第29行,我們讓Gin在localhost端口8080上運行web服務器。
下面你可以找到本例中使用的四個模板。這些模板每個都需要在自己的文件中。每段代碼之前都有文件名。
模板的語法與html/template基礎包中的相同。你可以我之前的文章中閱讀更多關于模板語法的內容。
// header.html
{{define "header.html"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/assets/bulma.min.css">
<title></title>
</head>
<body>
<div class="container">
<br><br>
{{end}}
// footer.html
{{define "footer.html"}}
</div>
</body>
</html>
{{end}}
// index.html
{{template "header.html"}}
<h1 class="title">
INDEX PAGE
</h1>
<p>{{ .content }}</p>
{{template "footer.html"}}
// about.html
{{template "header.html"}}
<h1 class="title">
ABOUT PAGE
</h1>
<p>{{ .content | upper}}</p>
{{template "footer.html"}}
如前所述,我在開發時使用codegangsta/gin工具來自動重載gin。這使得在瀏覽器中檢查我們代碼的結果變得更加容易。當我們改變了代碼,無需停止當前的可執行文件,重新構建和運行它,就可以做到這一點。
以下是Github頁面上對這個工具的描述:
gin是一個簡單的命令行工具,用于實時重新加載Go web應用程序。只需在你的應用程序目錄下運行gin,你的web應用就會以gin作為代理來服務。gin會在檢測到更改時自動重新編譯你的代碼。下次它接收到HTTP請求時,你的應用將會重啟。
go get github.com/codegangsta/gin
gin -i --appPort 8080 --port 3000 run main.go
上述代碼需要在命令行中運行。它假設你正在localhost的8080端口上運行你的Go web應用,并且你將使用3000端口作為Gin自動重載代理。
在從命令行運行代碼之前,你可能需要運行以下命令:
go mod init
go mod tidy
在瀏覽器的URL字段中輸入以下內容以檢查代碼的結果:
localhost:3000/
…對于索引頁面,和…
localhost:3000/about
對于關于頁面。
請記住,始終保持學習的態度,并享受編碼的樂趣!祝您編碼愉快!
如果你喜歡我的文章,點贊,關注,轉發!
在移動端平臺開發中,為了增加代碼復用,降低開發成本,通常會需要采用跨平臺的開發技術,花椒也不例外。本次新的單品開發,由于時間緊,人員有限,經過調研選型,最終確定了 flutter 方案(具體選型過程不在本文討論之內)。
為了讓客戶端更專注業務實現,降低接口聯調測試成本,我們選用了 gRPC 方案。gRPC是一個高性能、通用的開源 RPC 框架,由 Google 開發并基于 HTTP/2 協議標準而設計,基于 ProtoBuf(Protocol Buffers)序列化協議開發,且支持當前主流開發語言。gRPC通過定義一個服務并指定一個可以遠程調用的帶有參數和返回類型的的方法,使客戶端可以直接調用不同機器上的服務應用的方法,就像是本地對象一樣。在服務端,服務實現這個接口并且運行 gRPC 服務處理客戶端調用。在客戶端,有一個stub提供和服務端相同的方法。
特點
gRPC-Web 為前端瀏覽器提供了 Javascript 庫用來訪問 gRPC 服務,但是需要通過 Envoy 提供代理服務。相比 JSON 的方式對前端不夠友好,同時也增加了服務端的部署成本。因此在這次項目中前端未使用 gRPC 服務,而是由 gRPC-Gateway 提供代理的 RESTful 接口。
grpc-gateway 是 protoc 的一個插件,它能讀取 gRPC 的服務定義并生成反向代理服務器,將 RESTful 的 JSON 請求轉換為 gRPC 的方式。這樣無需太多工作即可實現一套基于 gRPC 服務的 RESTful 接口,方便前端使用調用接口,同時也方便開發過程中通過 Postman/Paw 之類的工具調試接口。
gateway -> gRPC 映射方式:
例如,gRPC 接口要求的通用的 metadata 參數(如 platform, device_id 等)在 HTTP RESTful 的傳遞方式如下:
dart
為了便于客戶端調用,連接復用及通用參數傳遞,我們封裝了 dart 的基礎庫。
BaseClient 維護了針對 HOST 緩存的連接池,同時也提供了接口需要傳遞的 metadata 信息。
golang
golang 后端服務需要同時支持 gRPC 和 gateway 兩種請求方式。為了簡化部署和上線依賴,gateway 和 gRPC 的功能放在了一起,并通過攔截器注入對應的功能,主要包括 gRPC 統計,訪問日志,接口鑒權,請求參數校驗,gateway JSON 編碼等。
為了提高開發效率,方便維護及模塊復用,服務端按功能進行組件化開發。每個組件可以單獨運行一個服務,也可以和其它組件共同組成一個服務。每個組件都需要實現 Component 接口:
對應組件開發完成后,需要開發對應的服務容器,步驟如下。
1 base.Init(context.TODO(), cfg, &global.Callback{ 2 Authenticator: &auth.Callback{}, 3 LogCapture: &log.Capture{}, 4 })
base.DefaultServer.AddPublicServer(rpcPort, gatewayPort, setting.TLSConfig)
1 base.DefaultServer.RegisterComponent(&user.Component{}) 2 base.DefaultServer.RegisterComponent(&push.Component{}) 3 ...
base.DefaultServer.Serve()
proto 規范
gRPC 基于標準化的 IDL(ProtoBuf)來生成服務器端和客戶端代碼,我們決定將所有的接口描述及文檔說明都放到 proto 文件中,便于查看及修改。對 proto 的接口描述及注釋的規范如下:
代碼生成
golang
1 gengo: 2 @protoc -Iproto \ 3 -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 4 -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \ 5 -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \ 6 --go_out=plugins=grpc:go/pb \ 7 --grpc-gateway_out=logtostderr=true:go/pb \ 8 --validate_out="lang=go:go/pb" \ 9 --auth_out="lang=go:go/pb" \ 10 proto/*.proto
golang 使用 go mod 的方式直接引入 pb 生成的 .go 文件
dart
修改 pubspec.yaml,執行 flutter packages get 或 flutter packages upgrade
1 dependencies: 2 flutter: 3 sdk: flutter 4 5 protobuf: ^0.13.4 6 grpc: ^1.0.1 7 user: 8 git: 9 url: git@github.com:project/repo.git 10 path: dart/user
gRPC gateway 提供了通過 proto 文件生成 swagger API 文檔,缺點是只支持 gateway 的 RESTful 接口,并且默認的展示方式有點不符合我們的常規文檔使用方式。
我們基于 protoc 插件開發了 protoc-gen-markdown 工具,可以由 proto 文件生成 markdown 文檔,提供 gRPC 接口描述,以及 RESTful 接口描述及 JSON 示例,提供全文目錄,支持錨點導航等。生成方式如下:
1gendoc: 2 @protoc -Iproto \ 3 -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ 4 -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \ 5 -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \ 6 --markdown_out=":doc" \ 7 proto/*.proto
文檔會在對應路徑生成接口列表 README.md,以及每個 protobuf 對應的接口文檔。
傳統的 RESTful 接口在調試及問題排查時,可以通過抓包或者 MitM(中間人攻擊)的方式,配置也比較容易。而 gRPC 因為使用了 HTTP2 及 protobuf 二進制流,抓包及數據流反解難度相對較高,調試及問題排查時會比較復雜。為了解決這個問題,我們通過服務端注入的方式,配合查詢后臺過濾對應的請求日志,從而實現如下類似抓包的效果。
Go語言中文網,致力于每日分享編碼、開源等知識,歡迎關注我,會有意想不到的收獲!
本文由花椒服務端團隊原創授權發布
近在給項目代碼完善單元測試,發現go語言單元測試相關的資料都是零零散散的,所以在這兒整理總結一下。項目中使用的是goconvey+monkey+sqlmock (項目的web框架為gin, 持久層框架為gorm), 使用時也碰到一些坑,也會在這篇文章中做一些相關的記錄。 文章大約4200字,囿于篇幅,很多地方都是一筆帶過,不過在每一部分之后提供了一些筆者讀過覺得不錯的資料的鏈接,大家可以根據需要查看。
1.1 testing——Go內置的單元測試庫。
要編寫一個新的測試,需要創建一個以 _test.go 結尾的文件,該文件包含 TestXxx 函數。 將該文件放在與被測試的包相同的包中。
通過 go test 命令,能夠自動執行如下形式的任何函數:
func TestXxx(*testing.T)
注意:Xxx 可以是任何字母數字字符串,但是第一個字母不能是小寫字母(一般接被測試函數名字,不強求)。傳遞給測試函數的參數是 *testing.T 類型。它用于管理測試狀態并支持格式化測試日志。測試日志會在執行測試的過程中不斷累積,并在測試完成時轉儲至標準輸出。
詳情參見:The-Golang-Standard-Library-by-Example
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.1.html
1.2 TestMain
在寫測試時,有時需要在測試之前或之后進行額外的設置(setup)或拆卸(teardown);有時,測試還需要控制在主線程上運行的代碼。為了支持這些需求,testing 提供了 TestMain 函數:
func TestMain(m *testing.M)
如果測試文件中包含該函數,那么生成的測試將調用 TestMain(m),而不是直接運行測試。
TestMain 運行在主 goroutine 中, 可以在調用 m.Run 前后做任何設置和拆卸。注意,在 TestMain 函數的最后,應該使用 m.Run 的返回值作為參數調用 os.Exit。
詳情參見:TestMain
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.5.html#testmain
1.3 httptest——HTTP測試輔助工具
Go 標準庫專門提供了 httptest 包專門用于進行 http Web 開發測試。
httptest包最關鍵的是提供了一個 http.ReponseWriter接口的實現結構:httptest.ReponseRecorder,通過它可以得到一個http.ReponseWriter,并以此來接收服務器返回的響應包。
詳情參見:httptest - HTTP 測試輔助工具
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.6.html
1.4 測試覆蓋率
Go 從 1.2 開始,引入了測試覆蓋率的支持,使用的是 cover 相關的工具(go test -cover、go tool cover)。
詳情參見:The cover story
https://blog.golang.org/cover
Go標準包里并沒有斷言庫,但是不使用斷言庫進行結果校驗的話,測試代碼將會變得非常臃腫,可讀性和可維護性都會很差。不過好在有第三方框架可以讓我們使用。
2.1 testify
github地址:https://github.com/stretchr/testify
特性:
2.2 gocheck
godoc地址:https://godoc.org/gopkg.in/check.v1
特性:
詳情參見:gocheck 使用介紹
https://zhuanlan.zhihu.com/p/45570168
2.3 goconvey
github地址: https://github.com/smartystreets/goconvey
特性:
詳情參見:GoConvey框架使用指南
https://www.jianshu.com/p/e3b2b1194830
2.4 比較
其實gocheck我沒怎么用過,只是當時調研的時候看到了,在斷言方面看起來和其他的差不多。
testify和goconvey都有嘗試,最后采用的是goconvey,所以對goconvey更熟悉一點。
3.1 識別依賴
普遍來說,我們遇到最常見的依賴無非下面幾種:
3.2 mock和stub的區別
這個話題也算是老生常談了。幾句話很難解釋清楚,有興趣可以閱讀Martin Fowler的文章。
stub本質上是對真實對象的一個模擬,比如調用者需要一個值,那就讓stub輸出一個值,如果調用者需要傳遞一個值給stub,那就在stub中定義一個方法接受該參數,相當于“依賴部分”的一個簡化實現。mock則是在程序代碼中向被測試代碼注入“依賴部分”,模擬出函數調用返回的結果。
個人認為兩者最大的區別在于依賴對象是否和被測對象有交互,從結果來看,stub不會使測試失敗,它只是為被測對象提供依賴的對象,并不改變測試結果,而mock則會根據不同的交互測試要求,很可能會更改測試的結果。stub是state-based,關注的是輸入和輸出。mock是interaction-based,關注的是交互過程。
mock和stub還有一個重要的區別就是expectiation。對于mock來說,expectiation是重中之重:我們期待方法有沒有被調用,期待適當的參數,期待調用的次數,甚至期待多個mock之間的調用順序。所有的一切期待都是事先準備好,在測試過程中和測試結束后驗證是否和預期的一致。而對于stub,通常都不會關注expectiation,沒有任何代碼來幫助判斷這個stub類是否被調用。雖然理論上某些stub實現也可以通過自己編碼的方式增加對expectiation的內容,比如增加一個計數器,每次調用+1之類,但是實際上極少這樣做。
在Go中,如果要用stub,那將是侵入式的,必須將代碼設計成可以用stub方法替換的形式。為了測試,需要專門用一個全局變量 來保存具有外部依賴的方法。然而在不提倡使用全局變量的Go語言當中,這顯然是不合適的。所以,并不提倡這種Stub方式。
但其實這兩種方法并不是割裂的,例如像下文提到的gomock框架除了像其名字一樣可以mock對象以外,還提供了stub的功能。軟件工程沒有銀彈,我們需要根據合適的場景選用合適的方法,甚至可以結合多種方法使用。
詳情參見:Mocks Aren't Stubs(Martin Fowler)
https://martinfowler.com/articles/mocksArentStubs.html
以及 中文翻譯
https://www.cnblogs.com/anf/archive/2006/03/27/360248.html
3.3 gostub
github地址:https://github.com/prashantv/gostub
特性:
缺陷:
詳情參見:GoStub框架使用指南
3.4 gomock
github地址:https://github.com/golang/mock
特性:
缺陷:
詳情參見:使用Golang的官方mock工具—gomock
https://www.jianshu.com/p/598a11bbdafb
和 GoMock框架使用指南
https://www.jianshu.com/p/f4e773a1b11f
3.5 gomonkey
github地址:https://github.com/bouk/monkey
特性:
缺陷:
詳情參見:Monkey框架使用指南
https://www.jianshu.com/p/2f675d5e334e
3.6 sqlmock
github地址: https://github.com/DATA-DOG/go-sqlmock
特性:
缺陷:
3.7 httpexpect
github地址:https://github.com/gavv/httpexpect
特性:
sqlmock和httpexpect都蠻簡單的,看完github主頁的QuickStart基本就會用了~~
4.1 選擇原因
newDB=MysqlDB.ModelTable(c, &Basexxx{}, c.AppID()).Where("type=?", libType).Limit(limit).Offset(offset).Order("created_at desc").Find(&libxxxs)
4.2 gorm+sqlmock使用方法
初始化sqlmock后,然后使用dialect和dsn打開一個新的gorm連接并賦值給數據庫操作實例
_, mock, _=sqlmock.NewWithDSN("sqlmock_db") MysqlDB.DB, _=gorm.Open("sqlmock", "sqlmock_db")
接下來就和sqlmock的普通使用沒什么區別了,只要mock時能夠成功的匹配gorm生成的sql語句即可
詳情參見:Stub database connection with GORM
https://blog.valletta.io/blog/2018-07-05-stub-database-connection-with-gorm/
4.3 踩坑記錄(持續更新~)
func A(arg string) error { return B(arg) }
原因:
在不改動原有代碼的情況下,有2種解決方案:
5.1 單元測試的粒度
對于剛開始做單元測試的同學來說,如何把握單元測試的粒度是一個讓人頭疼的問題。
測試粒度做的太細,會耗費大量的開發以及維護時間,每改一個方法,都要改動其對應的測試方法。當發生代碼重構的時候那簡直就是噩夢(因為所有的單元測試又都要寫一遍了…)。
如果單元測試粒度太粗,一個測試方法測試了n多方法,那么單元測試將顯的非常臃腫,脫離了單元測試的本意,容易把單元測試寫成集成測試。
5.2 單元測試的成本和收益
在受益于單元測試的好處的同時,也必然增加了代碼量以及維護成本。
下面這張成本/價值象限圖清晰闡述了在不同性質的系統中單元測試的成本和價值之間的關系。
參考鏈接
下面是一些其他的參考資料:
1. The-Golang-Standard-Library-by-Example
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/
2. 搞定Go單元測試(一)——基礎原理
https://juejin.im/post/5ce93447e51d45775746b8b0#heading-12
*請認真填寫需求信息,我們會在24小時內與您取得聯系。