寫在學習golang一個月後

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

遇到的問題

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

架構的選擇

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

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

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

中介軟體——input

每個介面都需要擷取GET或POST的參數,但是gin內建的方法只能返回string,所以我們進行了簡單的封裝。封裝過後我們就可以根據所需直接轉換成想要的資料類型。

package inputimport (    "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 ginximport (    "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 ginximport (    "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 := 10021rows, 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 mainimport (    "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是最好的語言!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.