go語言實戰嚮導

simplejia發表於2016-10-22

> 導語: 用 golang 實現後臺服務,你想要的都在這兒,有 webserver 最簡框架 (wsp),有互動式程式設計環境 (gop),有程式管理服務 (cmonitor),有 orm(orm),有本地 kv 快取 (lc),等等


使用 go 語言做後臺服務已經有 3 年了,通過專案去檢驗一個又一個的想法,然後不斷總結,優化,最終形成了自己的一整套體系,小到一個列印物件的方法,大到一個 web 後臺專案最佳實踐指導,這一點一滴都是在不斷的實踐中進化開來,以下內容是其中一小部分的彙報,各位看官如有興致,請前往 https://github.com/simplejia 關注最新的程式碼變更,希望和 golang 愛好者們共同探討,也感謝 xie 大提供這樣一個優秀的社群場所。

wsp (go http webserver)

實現初衷

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

使用場景

  • 以 http webserver 方式對外提供服務
  • 後臺介面服務

使用案例

  • 大型網際網路社交業務

實現方式

  • 路由自動生成,按要求(見 demo/controller/demo.go)提供 controller/action 的實現程式碼,wsp 執行後會分析專案程式碼,自動生成路由表並記錄在檔案 demo/WSP.go 裡,controller/action 定義程式碼必須符合函式定義:func(http.ResponseWriter, *http.Request),並且是帶 receiver 的 method demo_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.Request, map[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),其實更推薦通過定義過濾器的方式支援類似功能
  • 更方便編寫如下的測試用例:

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
更多原創文章乾貨分享,請關注公眾號
  • go語言實戰嚮導
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章