go語言實戰嚮導

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

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

 

使用go語言作後臺服務已經有3年了,經過項目去檢驗一個又一個的想法,而後不斷總結,優化,最終造成了本身的一整套體系,小到一個打印對象的方法,大到一個web後臺項目最佳實踐指導,這一點一滴都是在不斷的實踐中進化開來。如下內容將是一次總體的彙報,各位看官若有興致,請移步GitHub 關注最新的代碼變動。c++

wsp (go http webserver)

實現初衷

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

使用場景

  • 以http webserver方式對外提供服務
  • 後臺接口服務

使用案例

大型互聯網社交業務git

實現方式

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

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.gogolang

// 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())web

  • 項目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

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

實現初衷

  • 簡單可依賴,充分利用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.gosql

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.goshell

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]$
相關文章
相關標籤/搜索