go語言實戰向導


版權聲明:本文由魏佳原創文章,轉載請注明出處: 
文章原文鏈接:https://www.qcloud.com/community/article/173

來源:騰雲閣 https://www.qcloud.com/community

 

使用go語言做后台服務已經有3年了,通過項目去檢驗一個又一個的想法,然后不斷總結,優化,最終形成了自己的一整套體系,小到一個打印對象的方法,大到一個web后台項目最佳實踐指導,這一點一滴都是在不斷的實踐中進化開來。以下內容將是一次整體的匯報,各位看官如有興致,請移步GitHub 關注最新的代碼變更。

wsp (go http webserver)

實現初衷

  • 簡單可依賴,充分利用go已有的東西,不另外增加復雜、難以理解的東西,這樣做的好處包括:更容易跟隨go的升級而升級,降低使用者學習成本
  • yii提供的controller/action的路由方式比較常用,在wsp里實現一套
  • java annotation的功能挺方便,在wsp里,通過注釋來實現過濾器方法的調用定義
  • 不能因為wsp的引入而降低原生go http webserver的性能

使用場景

  • 以http webserver方式對外提供服務
  • 后台接口服務

使用案例

大型互聯網社交業務

實現方式

路由自動生成,按要求提供controller/action的實現代碼,wsp執行后會分析項目代碼,自動生成路由表並記錄在文件demo/WSP.go里,controller/action定義代碼必須符合函數定義:func(http.ResponseWriter, *http.Request),並且是帶receiver的methoddemo_set.go

package controller import ( "net/http" "github.com/simplejia/wsp/demo/service" ) // @prefilter("Login", {"Method":{"type":"get"}}) // @postfilter("Boss") func (demo *Demo) Set(w http.ResponseWriter, r *http.Request) { key := r.FormValue("key") value := r.FormValue("value") demoService := service.NewDemo() demoService.Set(key, value) json.NewEncoder(w).Encode(map[string]interface{}{ "code": 0, }) } 

WSP.go

// generated by wsp, DO NOT EDIT. package main import "net/http" import "time" import "github.com/simplejia/wsp/demo/controller" import "github.com/simplejia/wsp/demo/filter" func init() { http.HandleFunc("/Demo/Get", func(w http.ResponseWriter, r *http.Request) { t := time.Now() _ = t var e interface{} c := new(controller.Demo) defer func() { e = recover() if ok := filter.Boss(w, r, map[string]interface{}{"__T__": t, "__C__": c, "__E__": e}); !ok { return } }() c.Get(w, r) }) http.HandleFunc("/Demo/Set", func(w http.ResponseWriter, r *http.Request) { t := time.Now() _ = t var e interface{} c := new(controller.Demo) defer func() { e = recover() if ok := filter.Boss(w, r, map[string]interface{}{"__T__": t, "__C__": c, "__E__": e}); !ok { return } }() if ok := filter.Login(w, r, map[string]interface{}{"__T__": t, "__C__": c, "__E__": e}); !ok { return } if ok := filter.Method(w, r, map[string]interface{}{"type": "get", "__T__": t, "__C__": c, "__E__": e}); !ok { return } c.Set(w, r) }) } 
  • wsp分析項目代碼,尋找符合要求的注釋(見demo/controller/demo_set.go),自動生成過濾器調用代碼在文件demo/WSP.go里,filter注解分為前置過濾器(prefilter)和后置過濾器(postfilter),格式如:@prefilter({json body}),{json body}代表傳入參數,符合json array定義格式(去掉前后的中括號),可以包含string值或者object值,filter函數定義滿足:func (http.ResponseWriter*http.Requestmap[string]interface{}) bool,過濾器函數如下: method.go
package filter import ( "net/http" "strings" ) func Method(w http.ResponseWriter, r *http.Request, p map[string]interface{}) bool { method, ok := p["type"].(string) if ok && strings.ToLower(r.Method) != strings.ToLower(method) { http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) return false } return true } 

filter輸入參數map[string]interface{},會自動設置"T",time.Time類型,值為執行起始時間,可用於耗時統計,"C",{Controller}類型,值為{Controller}實例,可通過接口方式存取相關數據(這種方式存取數據較context方式更簡單實用),"E",值為recover()返回值,用於檢測錯誤並處理(后置過濾器必須recover())

  • 項目main.go代碼示例 main.go
package main import ( "log" "github.com/simplejia/clog" "github.com/simplejia/lc" "net/http" _ "github.com/simplejia/wsp/demo/clog" _ "github.com/simplejia/wsp/demo/conf" _ "github.com/simplejia/wsp/demo/mysql" _ "github.com/simplejia/wsp/demo/redis" ) func init() { lc.Init(1e5) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) }) } func main() { clog.Info("main()") log.Panic(http.ListenAndServe(":8080", nil)) } 

miscellaneous

  • 通過wrk壓測工具在同樣環境下(8核,8g),wsp空跑qps:9萬,beego1.7.1空跑qps:5.5萬
  • 更方便加入middleware(func(http.Handler) http.Handler),其實更推薦通過定義過濾器的方式支持類似功能
  • 更方便編寫如下的測試用例:
    test (測試用例運行時需要用到項目配置文件,所以請在test目錄生成../clog,../conf,../mysql,../redis的軟鏈接)

demo

提供一個簡單易擴展的項目stub

實現初衷

  • 簡單可依賴,充分利用go已有的東西,不另外增加復雜、難以理解的東西,這樣做的好處包括:更容易跟隨go的升級而升級,降低使用者學習成本
  • 提供常用組件的簡單包裝,如下:
    • config,提供項目主配置文件自動解析,見conf
    • redis,使用(github.com/garyburd/redigo),提供配置文件自動解析,見redis
    • mysql,使用(database/sql),提供配置文件自動解析,見mysql,同時為了方便對象映射,提供了最常用的orm組件供選擇使用,見orm

項目編寫指導意見

  • 目錄結構:
├── WSP.go
├── clog │ └── clog.go ├── conf │ ├── conf.go │ └── conf.json ├── controller │ ├── base.go │ ├── demo.go │ ├── demo_get.go │ └── demo_set.go ├── demo ├── filter │ ├── boss.go │ ├── login.go │ └── method.go ├── main.go ├── model │ ├── demo.go │ ├── demo_get.go │ └── demo_set.go ├── mysql │ ├── demo_db.json │ └── mysql.go ├── redis │ ├── demo.json │ └── redis.go ├── service │ ├── demo.go │ ├── demo_get.go │ └── demo_set.go └── test ├── clog -> ../clog ├── conf -> ../conf ├── demo_get_test.go ├── demo_set_test.go ├── init_test.go ├── mysql -> ../mysql └── redis -> ../redis 
  • controller目錄:負責request參數解析,service調用
  • service目錄:負責邏輯處理,model調用
  • model目錄:負責數據處理

接口實現上,建議一個接口對應一個文件,如controller/demo_get.go, service/demo_get.go, model/demo_get.go

lc (local cache)

實現初衷

  • 純用redis做緩存,相比lc,redis有網絡調用開銷,反復調用多次,延時急劇增大,當網絡偶爾出現故障時,我們的數據接口也就拿不到數據,但lc里的數據就算是超過了設置的過期時間,我們一樣能拿到過期的數據做備用
  • 使用mysql,當緩存失效,有數據穿透的風險,lc自帶並發控制,有且只允許同一時間同一個key的唯一一個client穿透到數據庫,其它直接返回lc緩存數據

特性

  • 本地緩存
  • 支持Get,Set,Mget,Delete操作
  • 當緩存失效時,返回失效標志同時,還返回舊的數據,如:v, ok := lc.Get(key),當key已經過了失效時間了,並且key還沒有被lru淘汰掉,v是之前存的值,ok返回false
  • 實現代碼沒有用到鎖
  • 使用到lru,淘汰長期不用的key
  • 結合lm使用更簡單快捷

demo

lc_test.go

package lc import ( "testing" "time" ) func init() { Init(65536) // 使用lc之前必須要初始化 } func TestGetValid(t *testing.T) { key := "k" value := "v" Set(key, value, time.Second) time.Sleep(time.Millisecond * 10) // 給異步處理留點時間 v, ok := Get(key) if !ok || v != value { t.Fatal("") } } 

lm (lc+redis+[mysql|http] glue)

實現初衷

寫redis+mysql代碼時(還可能加上lc),示意代碼如下:

func orig(key string) (value string) { value = redis.Get(key) if value != "" { return } value = mysql.Get(key) redis.Set(key, value) return } // 如果再加上lc的話 func orig(key string) (value string) { value = lc.Get(key) if value != "" { return } value = redis.Get(key) if value != "" { lc.Set(key, value) return } value = mysql.Get(key) redis.Set(key, value) lc.Set(key, value) return } 

有了lm,再寫上面的代碼時,一切變的那么簡單 lm_test.go

func tGlue(key, value string) (err error) { err = Glue( key, &value, func(p, r interface{}) error { _r := r.(*string) *_r = "test value" return nil }, func(p interface{}) string { return fmt.Sprintf("tGlue:%v", p) }, &LcStru{ Expire: time.Millisecond * 500, Safety: false, }, &McStru{ Expire: time.Minute, Pool: pool, }, ) if err != nil { return } return } 

功能

自動添加緩存代碼,支持lc, redis,減輕你的心智負擔,讓你的代碼更加簡單可靠,少了大段的冗余代碼,復雜的事全交給lm自動幫你做了
支持Glue[Lc|Mc]及相應批量操作Glues[Lc|Mc],詳見lm_test.go示例代碼

注意

lm.LcStru.Safety,當置為true時,對lc在並發狀態下返回的nil值不接受,因為lc.Get在並發狀態下,同一個key返回的value有可能是nil,並且ok狀態為true,Safety置為true后,對以上情況不接受,會繼續調用下一層邏輯

orm (配合sql.Rows使用的超簡單數據到對象映射功能函數)

實現初衷

  • database/sql包,Db.Query返回的sql.Rows,通過Rows.Scan方式示例代碼如下:
rows, err := db.Query("SELECT ...") defer rows.Close() for rows.Next() { var id int var name string err = rows.Scan(&id, &name) } err = rows.Err() ... 

但實際項目場景里,我們更想這樣:

rows, err := db.Query("SELECT ...") defer rows.Close() var d []*stru err = Rows2Strus(rows, &d) 

這就是一種簡單的對象映射,通過轉為對象的方式,我們的代碼更方便處理了

功能

一共提供四種場景的使用方法:

  • Rows2Strus, sql.Rows轉為struct slice

  • sql.Rows轉為struct,等同db.QueryRow

  • Rows2Cnts, sql.Rows轉為int slice

  • Rows2Cnt, sql.Rows轉為int,用於select count(1)操作

支持tag: orm,如下:

type Demo struct { Id int DemoName string `orm:"demo_name"` // 映射成demo_name字段 } 

支持匿名成員,如下:

type C struct { Id int } type P struct { C // 映射成id字段 Name string } 

支持snakecase配置,通過設置orm.IsSnakeCase = true,如下:

type Demo struct { Id int DemoName string // 映射成demo_name字段 } 

demo

orm_test.go

cmonitor

功能

用於進程監控,管理

實現

  • 被監控進程啟動后,按每300ms執行一次狀態檢測(通過發signal0信號檢測),每個被監控進程在一個獨立的協程里被監測。
  • cmonitor啟動后會監聽一個http端口用於接收管理命令(start|stop|status|...)

使用方法

配置文件:conf.json (json格式,支持注釋) conf.json

{
    "env": "dev", // 配置運行環境 "envs": { "dev": { "port": 29118, // 配置監聽端口 "rootpath": "/home/simplejia/tools/go/ws/src/github.com/simplejia", "environ": "ulimit -n 65536", // 配置環境變量 "svrs": { // demo "demo": "wsp/demo/demo" // key: 名字 value: 將與rootpath拼接在一起運行 }, "log": { "mode": 3, // 0: none, 1: localfile, 2: collector (數字代表bit位) "level": 15 // 0: none, 1: debug, 2: warn 4: error 8: info (數字代表bit位) } }, "test": { "port": 29118, // 配置監聽端口 "rootpath": "/home/simplejia/tools/go/ws/src/github.com/simplejia", "environ": "ulimit -n 65536", // 配置環境變量 "svrs": { // demo "demo": "wsp/demo/demo" }, "log": { "mode": 3, // 0: none, 1: localfile, 2: collector (數字代表bit位) "level": 15 // 0: none, 1: debug, 2: warn 4: error 8: info (數字代表bit位) } }, "prod": { "port": 29118, // 配置監聽端口 "rootpath": "/home/simplejia/tools/go/ws/src/github.com/simplejia", "environ": "ulimit -n 65536", // 配置環境變量 "svrs": { // demo "demo": "wsp/demo/demo" }, "log": { "mode": 2, // 0: none, 1: localfile, 2: collector (數字代表bit位) "level": 14 // 0: none, 1: debug, 2: warn 4: error 8: info (數字代表bit位) } } } } 
  • 運行方法:cmonitor.sh [start|stop|restart|status|check]
  • 進程管理:cmonitor -[h|status|start|stop|restart] [all|["svrname"]]

注意

  • cmonitor的運行日志通過clog上報,也可記錄在本地cmonitor.log日志文件里,注意:此cmonitor.log日志文件不會被切分,所以盡量保持較少的日志輸出,建議通過clog方式上報日志
  • cmonitor啟動監控進程后,被監控進程控制台日志cmonitor.log會輸出到相應進程目錄,最多保存30天,歷史日志以cmonitor.{day}.log方式備份
  • 當cmonitor啟動時,會根據conf.json配置啟動所有被監控進程,當被監控進程已經啟動過,並且符合配置要求時,cmonitor會自動將其加入監控列表
  • cmonitor會定期檢查進程運行狀態,如果進程異常退出,cmonitor會反復重試拉起,並且記錄日志
  • 當被監控進程為多進程運行模式,cmonitor只監控管理父進程(子進程應實現檢測父進程運行狀態,並隨父進程退出而退出)
  • 被監控進程以nohup方式啟動,所以你的程序就不要自己設定daemon運行了
  • 每分鍾通過ps方式檢測一次進程狀態,如果出現任何異常,比如有多份進程啟動等,記日志
  • 由於cmonitor會同時啟動內部httpserver(綁內網ip),所以也支持遠程管理,比如在瀏覽器里輸入:http://xxx.xxx.xxx.xxx:29118/?command=status&service=all

demo

$ cmonitor -status all

*****STATUS OK SERVICE LIST***** demo PID:13539 *****STATUS FAIL SERVICE LIST***** $ cmonitor -restart demo SUCCESS 

clog (集中式日志收集服務)

實現初衷

  • 實際項目中,服務會部署到多台服務器上去,機器本地日志不方便查看,通過集中收集日志到一台或兩台機器上,日志以文件形式存在,按服務名,ip,日期,日志類型分別存儲,這樣查看日志時就方便多了
  • 我們做服務時,經常需要添加一些跟業務邏輯無關的功能,比如按錯誤日志報警,上報數據用於統計等等,這些功能和業務邏輯混在一起,實在沒有必要,有了clog,我們只需要發送有效的數據,然后就可把數據處理的工作留給clog去做

    功能

  • 通過發送日志至本機agent,然后agent轉發至遠程master主機,api目前提供golang,c支持
  • 根據配置(master/conf/conf.json)運行相關日志分析程序,目前已實現:日志輸出,報警
  • 輸出日志文件按master/logs/{模塊名}/log{dbg|err|info|war}/{day}/log{ip}{+}{sub}規則命名,最多保存30天日志

使用方法

  • agent機器

    布署本機agent服務:agent/agent,配置文件:agent/conf/conf.json

  • master機器

    布署master服務:master/master,配置文件:master/conf/conf.json

  • agent和master服務建議用cmonitor啟動管理

注意

  • api.go文件里定義了agent服務端口(agent啟動后會監聽127.0.0.1:xxx),見clog.Port變量
  • master/conf/conf.json文件里,tpl定義模板,然后通過$xxx方式引用,目前支持的handler有:filehandler和alarmhandler,filehandler用來記錄本地日志,alarmhandler用來發報警
  • 對於alarmhandler,相關參數配置見params,目前的報警只是打印日志,實際實用,應替換成自己的報警處理邏輯,重新賦值procs.AlarmFunc就可以了,可以在master/procs目錄下新建一個go文件,如下示例:
package procs import ( "encoding/json" "os" ) func init() { // 請替換成你自己的報警處理函數 AlarmFunc = func(sender string, receivers []string, text string) { params := map[string]interface{}{ "Sender": sender, "Receivers": receivers, "Text": text, } json.NewEncoder(os.Stdout).Encode(params) } } 
  • alarmhandler有防騷擾控制邏輯,相同內容,一分鍾內不再報,兩次報警不少於30秒,以上限制和日志文件一一對應
  • 如果想添加新的handler,只需在master/procs目錄下新建一個go文件,如下示例:
package procs func XxxHandler(cate, subcate string, content []byte, params map[string]interface{}) { } func init() { RegisterHandler("xxxhandler", XxxHandler) } 

demo

api_test.go
demo (demo項目里有clog的使用例子)

simplesvr (simple udp server)

功能:

  • 超簡單c/c++服務,多進程,udp通信,沒有高深復雜的事件驅動,沒有多線程帶來的數據共享問題(加鎖對性能的影響),代碼結構簡單,直達業務
  • 適用場景:業務邏輯重,追求高吞吐量,容忍udp帶來的不可靠。(已有c lib庫,不方便采用golang包裝時)
  • c開發新手也可以快速上手

特性

  • 代碼結構簡單,僅有一個.cpp文件:main/main.cpp,其它均是.h文件。
  • 調用協議簡單,'\x00'分隔字段
  • 多進程,同時啟動多個業務子進程,任何一個進程(包括父進程)退出,所有其它進程均退出。
  • 支持json格式配置文件
  • 可選通過clog方式記錄日志並報警
  • 提供很多有用的小組件,包括: > 簡單高效的http get及post操作組件 > 類似go lc的本地緩存組件(支持lru, 支持過期后還能返回舊數據,這個在獲取新數據失敗時尤其有用)
  • 提供些小的庫函數,如:定時器,獲取本機內網ip等

    注意

  • 加入新依賴庫時,只需要在main/main.cpp里加入庫頭文件,修改Makefile文件
  • api目錄提供api.go示例代碼用於和simplesvr服務通信

gop (go REPL)

實現初衷

有時想快速驗證go某個函數的使用,臨時寫個程序太低效,有了gop,立馬開一個shell環境,邊寫邊運行,自動為你保存上下文,還可隨時導入導出snippet,另外還有代碼自動補全等等特性

特性

  • history record(gop啟動后會在home目錄下生成.gop文件夾, 輸入歷史會記錄在此)
  • tab complete,可以補全package,補全庫函數,需要系統安裝有gocode
  • r|w兩種模式切換,r是默認模式,對用戶輸入實時解析運行,執行w命令切換到w模式,w模式下,只有當執行run命令時,代碼才會真正執行
  • 代碼實時查看和編輯功能[!命令功能]
  • snippet,可以導入和導出模板[<,>命令功能]

    注意:

  • 輸入代碼時,支持續行
  • 對於如下代碼,只會在執行結束后一並輸出 > print(1);time.Sleep(time.Second);print(2)
  • 可以通過echo 123這種方式輸出, echo是println的簡寫,你甚至可以重新定義println變量來使用自己的打印方法,比如像我這樣定義(utils.IprintD的特點是可以打印出指針指向的實際內容,就算是嵌套的指針也可以,fmt.Printf做不到):
    import "github.com/simplejia/utils" var println = utils.IprintD 
  • 導入項目package時,最好提前通過go install方式安裝包文件到pkg目錄,這樣可以加快執行速度
  • 可以提前import包,后續使用時再自動引入
  • gop啟動后會自動導入$PWD/gop.tmpl或者$HOME/.gop/gop.tmpl模板代碼,可以把常用的代碼保存到gop.tmpl里

demo

$ gop
Welcome to the Go Partner! [[version: 1.7, created by simplejia] Enter '?' for a list of commands. [r]$ ? Commands: ?|help help menu -[dpc][#],[#]-[#],... pop last/specific (declaration|package|code) ![!] inspect source [with linenum] <tmpl source tmpl >tmpl write tmpl [#](...) add def or code run run source compile compile source w write source mode on r write source mode off reset reset list tmpl list [r]$ for i:=1; i<3; i++ { ..... print(i) ..... time.Sleep(time.Millisecond) .....} 1 2 [r]$ import _ "github.com/simplejia/wsp/demo/mysql" [r]$ import _ "github.com/simplejia/wsp/demo/redis" [r]$ import _ "github.com/simplejia/wsp/demo/conf" [r]$ import "github.com/simplejia/lc" [r]$ import "github.com/simplejia/wsp/demo/service" [r]$ lc.Init(1024) [r]$ demoService := service.NewDemo() [r]$ demoService.Set("123", "456") [r]$ time.Sleep(time.Millisecond) [r]$ echo demoService.Get("123") 456 [r]$ >gop [r]$ <gop [r]$ ! package main p0: import _ "github.com/simplejia/wsp/demo/mysql" p1: import _ "github.com/simplejia/wsp/demo/redis" p2: import _ "github.com/simplejia/wsp/demo/conf" p3: import "github.com/simplejia/lc" p4: import "github.com/simplejia/wsp/demo/service" p5: import "fmt" // imported and not used p6: import "strconv" // imported and not used p7: import "strings" // imported and not used p8: import "time" // imported and not used p9: import "encoding/json" // imported and not used p10: import "bytes" // imported and not used func main() { c0: lc.Init(1024) c1: demoService := service.NewDemo() c2: _ = demoService c3: demoService.Set("123", "456") c4: time.Sleep(time.Millisecond) } [r]$

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM