寫在學習golang一個月后

遇到的問題

連接池。由于PHP沒有連接池,當高并發時就會有大量的數據庫連接直接沖擊到MySQL上,最終導致數據庫掛掉。雖然Swoole有連接池,但是Swoole只是PHP的一個擴展,之前使用Swoole過程中就踩過很多的坑。經過我們的討論還是覺得使用Golang更加可控一些。

框架的選擇

在PHP中一直用的是Yaf,所以在Go中自然而言就選擇了Gin。因為我們一直以來的原則是:盡量接近底層代碼。

封裝過于完善的框架不利于對整個系統的掌控及理解。我不需要你告訴我這個目錄是干嘛的,這個配置怎么寫,這個函數怎么用等等。

Gin是一個輕路由框架,很符合我們的需求。為了更好地開發,我們也做了幾個中間件。

中間件——input

每個接口都需要獲取GET或POST的參數,但是gin自帶的方法只能返回string,所以我們進行了簡單的封裝。封裝過后我們就可以根據所需直接轉換成想要的數據類型。

package input

import (
    "strconv"
)

type I struct {
    body string
}

func (input *I) get(p string) *I {
    d, e := Context.GetQuery(p)
    input.body = d
    if e == false {
        return input
    }

    return input
}

func (input *I) post(p string) *I {
    d, e := Context.GetPostForm(p)
    input.body = d
    if e == false {
        return input
    }

    return input
}

func (input *I) String() string {
    return input.body
}

func (input *I) Atoi() int {
    body, _ := strconv.Atoi(input.body)
    return body
}

package input

//獲取GET參數
func Get(p string) *I {
    i := new(I)
    return i.get(p)
}

//獲取POST參數
func Post(p string) *I {
    i := new(I)
    return i.get(p)
}

封裝之前

pid, _ := strconv.Atoi(c.Query("product_id"))
alias := c.Query("product_alias")

封裝之后

  pid := input.Get("product_id").Atoi()
  alias := input.Get("product_alias").String()

中間件——logger

gin自身的logger比較簡單,一般我們都需要將日志按日期分文件寫到某個目錄下。所以我們自己重寫了一個logger,這個logger可以實現將日志按日期分文件并將錯誤信息發送給Sentry。

package ginx

import (
    "fmt"
    "io"
    "os"
    "time"

    "github.com/gin-gonic/gin"
    "sao.cn/configs"
)

var (
    logPath string
    lastDay int
)

func init() {
    logPath = configs.Load().Get("SYS_LOG_PATH").(string)
    _, err := os.Stat(logPath)
    if err != nil {
        os.Mkdir(logPath, 0755)
    }
}

func defaultWriter() io.Writer {
    writerCheck()
    return gin.DefaultWriter
}

func defaultErrorWriter() io.Writer {
    writerCheck()
    return gin.DefaultErrorWriter
}

func writerCheck() {
    nowDay := time.Now().Day()
    if nowDay != lastDay {
        var file *os.File
        filename := time.Now().Format("2006-01-02")
        logFile := fmt.Sprintf("%s/%s-%s.log", logPath, "gosapi", filename)

        file, _ = os.Create(logFile)
        if file != nil {
            gin.DefaultWriter = file
            gin.DefaultErrorWriter = file
        }
    }

    lastDay = nowDay
}

package ginx

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/url"
    "time"

    "github.com/gin-gonic/gin"
    "gosapi/application/library/output"
    "sao.cn/sentry"
)

func Logger() gin.HandlerFunc {
    return LoggerWithWriter(defaultWriter())
}

func LoggerWithWriter(outWrite io.Writer) gin.HandlerFunc {
    return func(c *gin.Context) {
        NewLog(c).CaptureOutput().Write(outWrite).Report()
    }
}

const (
    LEVEL_INFO  = "info"
    LEVEL_WARN  = "warning"
    LEVEL_ERROR = "error"
    LEVEL_FATAL = "fatal"
)

type Log struct {
    startAt time.Time
    conText *gin.Context
    writer  responseWriter
    error   error

    Level     string
    Time      string
    ClientIp  string
    Uri       string
    ParamGet  url.Values `json:"pGet"`
    ParamPost url.Values `json:"pPost"`
    RespBody  string
    TimeUse   string
}

func NewLog(c *gin.Context) *Log {
    bw := responseWriter{buffer: bytes.NewBufferString(""), ResponseWriter: c.Writer}
    c.Writer = &bw

    clientIP := c.ClientIP()
    path := c.Request.URL.Path
    method := c.Request.Method
    pGet := c.Request.URL.Query()
    var pPost url.Values
    if method == "POST" {
        c.Request.ParseForm()
        pPost = c.Request.PostForm
    }
    return &Log{startAt: time.Now(), conText: c, writer: bw, Time: time.Now().Format(time.RFC850), ClientIp: clientIP, Uri: path, ParamGet: pGet, ParamPost: pPost}
}

func (l *Log) CaptureOutput() *Log {
    l.conText.Next()
    o := new(output.O)
    json.Unmarshal(l.writer.buffer.Bytes(), o)
    switch {
    case o.Status_code != 0 && o.Status_code < 20000:
        l.Level = LEVEL_ERROR
        break
    case o.Status_code > 20000:
        l.Level = LEVEL_WARN
        break
    default:
        l.Level = LEVEL_INFO
        break
    }

    l.RespBody = l.writer.buffer.String()
    return l
}

func (l *Log) CaptureError(err interface{}) *Log {
    l.Level = LEVEL_FATAL
    switch rVal := err.(type) {
    case error:
        l.RespBody = rVal.Error()
        l.error = rVal
        break
    default:
        l.RespBody = fmt.Sprint(rVal)
        l.error = errors.New(l.RespBody)
        break
    }

    return l
}

func (l *Log) Write(outWriter io.Writer) *Log {
    l.TimeUse = time.Now().Sub(l.startAt).String()
    oJson, _ := json.Marshal(l)
    fmt.Fprintln(outWriter, string(oJson))
    return l
}

func (l *Log) Report() {
    if l.Level == LEVEL_INFO || l.Level == LEVEL_WARN {
        return
    }

    client := sentry.Client()
    client.SetHttpContext(l.conText.Request)
    client.SetExtraContext(map[string]interface{}{"timeuse": l.TimeUse})
    switch {
    case l.Level == LEVEL_FATAL:
        client.CaptureError(l.Level, l.error)
        break
    case l.Level == LEVEL_ERROR:
        client.CaptureMessage(l.Level, l.RespBody)
        break
    }
}

由于Gin是一個輕路由框架,所以類似數據庫操作和Redis操作并沒有相應的包。這就需要我們自己去選擇好用的包。

Package - 數據庫操作

最初學習階段使用了datbase/sql,但是這個包有個用起來很不爽的問題。

pid := 10021
rows, err := db.Query("SELECT title FROM `product` WHERE id=?", pid)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
    var title string
    if err := rows.Scan(&title); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s is %d\n", title, pid)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

上述代碼,如果select的不是title,而是*,這時就需要提前把表結構中的所有字段都定義成一個變量,然后傳給Scan方法。

這樣,如果一張表中有十個以上字段的話,開發過程就會異常麻煩。那么我們期望的是什么呢。提前定義字段是必須的,但是正常來說應該是定義成一個結構體吧? 我們期望的是查詢后可以直接將查詢結果轉換成結構化數據。

花了點時間尋找,終于找到了這么一個包——github.com/jmoiron/sqlx。

    // You can also get a single result, a la QueryRow
    jason = Person{}
    err = db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason")
    fmt.Printf("%#v\n", jason)
    // Person{FirstName:"Jason", LastName:"Moiron", Email:"jmoiron@jmoiron.net"}

    // if you have null fields and use SELECT *, you must use sql.Null* in your struct
    places := []Place{}
    err = db.Select(&places, "SELECT * FROM place ORDER BY telcode ASC")
    if err != nil {
        fmt.Println(err)
        return
    }

sqlx其實是對database/sql的擴展,這樣一來開發起來是不是就爽多了,嘎嘎~

為什么不用ORM? 還是上一節說過的,盡量不用過度封裝的包。

Package - Redis操作

最初我們使用了redigo【github.com/garyburd/redigo/redis】,使用上倒是沒有什么不爽的,但是在壓測的時候發現一個問題,即連接池的使用。

func factory(name string) *redis.Pool {
    conf := config.Get("redis." + name).(*toml.TomlTree)
    host := conf.Get("host").(string)
    port := conf.Get("port").(string)
    password := conf.GetDefault("passwd", "").(string)
    fmt.Printf("conf-redis: %s:%s - %s\r\n", host, port, password)

    pool := &redis.Pool{
        IdleTimeout: idleTimeout,
        MaxIdle:     maxIdle,
        MaxActive:   maxActive,
        Dial: func() (redis.Conn, error) {
            address := fmt.Sprintf("%s:%s", host, port)
            c, err := redis.Dial("tcp", address,
                redis.DialPassword(password),
            )
            if err != nil {
                exception.Catch(err)
                return nil, err
            }

            return c, nil
        },
    }
    return pool
}

/**
 * 獲取連接
 */
func getRedis(name string) redis.Conn {
    return redisPool[name].Get()
}

/**
 * 獲取master連接
 */
func Master(db int) RedisClient {
    client := RedisClient{"master", db}
    return client
}

/**
 * 獲取slave連接
 */
func Slave(db int) RedisClient {
    client := RedisClient{"slave", db}
    return client
}

以上是定義了一個連接池,這里就產生了一個問題,在redigo中執行redis命令時是需要自行從連接池中獲取連接,而在使用后還需要自己將連接放回連接池。最初我們就是沒有將連接放回去,導致壓測的時候一直壓不上去。

那么有沒有更好的包呢,答案當然是肯定的 —— gopkg.in/redis.v5

func factory(name string) *redis.Client {
    conf := config.Get("redis." + name).(*toml.TomlTree)
    host := conf.Get("host").(string)
    port := conf.Get("port").(string)
    password := conf.GetDefault("passwd", "").(string)
    fmt.Printf("conf-redis: %s:%s - %s\r\n", host, port, password)

    address := fmt.Sprintf("%s:%s", host, port)
    return redis.NewClient(&redis.Options{
        Addr:        address,
        Password:    password,
        DB:          0,
        PoolSize:    maxActive,
    })
}

/**
 * 獲取連接
 */
func getRedis(name string) *redis.Client {
    return factory(name)
}

/**
 * 獲取master連接
 */
func Master() *redis.Client {
    return getRedis("master")
}

/**
 * 獲取slave連接
 */
func Slave() *redis.Client {
    return getRedis("slave")
}

可以看到,這個包就是直接返回需要的連接了。

那么我們去看一下他的源碼,連接有沒有放回去呢。

func (c *baseClient) conn() (*pool.Conn, bool, error) {
    cn, isNew, err := c.connPool.Get()
    if err != nil {
        return nil, false, err
    }
    if !cn.Inited {
        if err := c.initConn(cn); err != nil {
            _ = c.connPool.Remove(cn, err)
            return nil, false, err
        }
    }
    return cn, isNew, nil
}

func (c *baseClient) putConn(cn *pool.Conn, err error, allowTimeout bool) bool {
    if internal.IsBadConn(err, allowTimeout) {
        _ = c.connPool.Remove(cn, err)
        return false
    }

    _ = c.connPool.Put(cn)
    return true
}

func (c *baseClient) defaultProcess(cmd Cmder) error {
    for i := 0; i <= c.opt.MaxRetries; i++ {
        cn, _, err := c.conn()
        if err != nil {
            cmd.setErr(err)
            return err
        }

        cn.SetWriteTimeout(c.opt.WriteTimeout)
        if err := writeCmd(cn, cmd); err != nil {
            c.putConn(cn, err, false)
            cmd.setErr(err)
            if err != nil && internal.IsRetryableError(err) {
                continue
            }
            return err
        }

        cn.SetReadTimeout(c.cmdTimeout(cmd))
        err = cmd.readReply(cn)
        c.putConn(cn, err, false)
        if err != nil && internal.IsRetryableError(err) {
            continue
        }

        return err
    }

    return cmd.Err()
}

可以看到,在這個包中的底層操作會先去connPool中Get一個連接,用完之后又執行了putConn方法將連接放回connPool。

結束語

package main

import (
    "github.com/gin-gonic/gin"

    "gosapi/application/library/initd"
    "gosapi/application/routers"
)

func main() {
    env := initd.ConfTree.Get("ENVIRONMENT").(string)
    gin.SetMode(env)

    router := gin.New()
    routers.Register(router)

    router.Run(":7321") // listen and serve on 0.0.0.0:7321
}

3月21日開始寫main,現在已經上線一個星期了,暫時還沒發現什么問題。

經過壓測對比,在性能上提升了大概四倍左右。原先響應時間在70毫秒左右,現在是10毫秒左右。原先的吞吐量大概在1200左右,現在是3300左右。

雖然Go很棒,但是我還是想說:PHP是最好的語言!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,579評論 25 707
  • 并發IO問題一直是服務器端編程中的技術難題,從最早的同步阻塞直接Fork進程,到Worker進程池/線程池,到現在...
    零一間閱讀 1,721評論 1 34
  • 常常會吸入濕氣,如果買回來就馬上套上塑料護郵袋,濕氣就悶在里面散不出來,一段時間后郵票更容易發黃發霉。所以買回郵票...
    后來呢_1911閱讀 1,136評論 1 0
  • 為保證銷售水平,業務員必須要把握消費者的心理等,這毋庸置疑。其實這也差不多與心理學有點共通之處,但也存在差別。比...
    d97c72b61c01閱讀 172評論 0 0