写在学习golang一个月后

405 阅读8分钟
原文链接: click.aliyun.com

遇到的问题

连接池。由于PHP没有连接池,当高并发时就会有大量的数据库连接直接冲击到MySQL上,最终导致数据库挂掉。虽然Swoole有连接池,但是Swoole只是PHP的一个扩展,之前使用Swoole过程中就踩过很多的坑。经过我们的讨论还是觉得使用Golang更加可控一些。

框架的选择

在PHP中一直用的是Yaf,所以在Go中自然而言就选择了Gin。因为我们一直以来的原则是:尽量接近底层代码。

封装过于完善的框架不利于对整个系统的掌控及理解。我不需要你告诉我这个目录是干嘛的,这个配置怎么写,这个函数怎么用等等。

Gin是一个轻路由框架,很符合我们的需求。为了更好地开发,我们也做了几个中间件。

中间件——input

每个接口都需要获取GET或POST的参数,但是gin自带的方法只能返回string,所以我们进行了简单的封装。封装过后我们就可以根据所需直接转换成想要的数据类型。


1package input 2 3import ( 4 "strconv" 5) 6 7type I struct { 8 body string 9} 10 11func (input *I) get(p string) *I { 12 d, e := Context.GetQuery(p) 13 input.body = d 14 if e == false { 15 return input 16 } 17 18 return input 19} 20 21func (input *I) post(p string) *I { 22 d, e := Context.GetPostForm(p) 23 input.body = d 24 if e == false { 25 return input 26 } 27 28 return input 29} 30 31func (input *I) String() string { 32 return input.body 33} 34 35func (input *I) Atoi() int { 36 body, _ := strconv.Atoi(input.body) 37 return body 38}

1package input 2 3//获取GET参数 4func Get(p string) *I { 5 i := new(I) 6 return i.get(p) 7} 8 9//获取POST参数 10func Post(p string) *I { 11 i := new(I) 12 return i.get(p) 13}

封装之前


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

封装之后


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

中间件——logger

gin自身的logger比较简单,一般我们都需要将日志按日期分文件写到某个目录下。所以我们自己重写了一个logger,这个logger可以实现将日志按日期分文件并将错误信息发送给Sentry。



1package ginx 2 3import ( 4 "fmt" 5 "io" 6 "os" 7 "time" 8 9 "github.com/gin-gonic/gin" 10 "sao.cn/configs" 11) 12 13var ( 14 logPath string 15 lastDay int 16) 17 18func init() { 19 logPath = configs.Load().Get("SYS_LOG_PATH").(string) 20 _, err := os.Stat(logPath) 21 if err != nil { 22 os.Mkdir(logPath, 0755) 23 } 24} 25 26func defaultWriter() io.Writer { 27 writerCheck() 28 return gin.DefaultWriter 29} 30 31func defaultErrorWriter() io.Writer { 32 writerCheck() 33 return gin.DefaultErrorWriter 34} 35 36func writerCheck() { 37 nowDay := time.Now().Day() 38 if nowDay != lastDay { 39 var file *os.File 40 filename := time.Now().Format("2006-01-02") 41 logFile := fmt.Sprintf("%s/%s-%s.log", logPath, "gosapi", filename) 42 43 file, _ = os.Create(logFile) 44 if file != nil { 45 gin.DefaultWriter = file 46 gin.DefaultErrorWriter = file 47 } 48 } 49 50 lastDay = nowDay 51}

1package ginx 2 3import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/url" 10 "time" 11 12 "github.com/gin-gonic/gin" 13 "gosapi/application/library/output" 14 "sao.cn/sentry" 15) 16 17func Logger() gin.HandlerFunc { 18 return LoggerWithWriter(defaultWriter()) 19} 20 21func LoggerWithWriter(outWrite io.Writer) gin.HandlerFunc { 22 return func(c *gin.Context) { 23 NewLog(c).CaptureOutput().Write(outWrite).Report() 24 } 25} 26 27const ( 28 LEVEL_INFO = "info" 29 LEVEL_WARN = "warning" 30 LEVEL_ERROR = "error" 31 LEVEL_FATAL = "fatal" 32) 33 34type Log struct { 35 startAt time.Time 36 conText *gin.Context 37 writer responseWriter 38 error error 39 40 Level string 41 Time string 42 ClientIp string 43 Uri string 44 ParamGet url.Values `json:"pGet"` 45 ParamPost url.Values `json:"pPost"` 46 RespBody string 47 TimeUse string 48} 49 50func NewLog(c *gin.Context) *Log { 51 bw := responseWriter{buffer: bytes.NewBufferString(""), ResponseWriter: c.Writer} 52 c.Writer = &bw 53 54 clientIP := c.ClientIP() 55 path := c.Request.URL.Path 56 method := c.Request.Method 57 pGet := c.Request.URL.Query() 58 var pPost url.Values 59 if method == "POST" { 60 c.Request.ParseForm() 61 pPost = c.Request.PostForm 62 } 63 return &Log{startAt: time.Now(), conText: c, writer: bw, Time: time.Now().Format(time.RFC850), ClientIp: clientIP, Uri: path, ParamGet: pGet, ParamPost: pPost} 64} 65 66func (l *Log) CaptureOutput() *Log { 67 l.conText.Next() 68 o := new(output.O) 69 json.Unmarshal(l.writer.buffer.Bytes(), o) 70 switch { 71 case o.Status_code != 0 && o.Status_code < 20000: 72 l.Level = LEVEL_ERROR 73 break 74 case o.Status_code > 20000: 75 l.Level = LEVEL_WARN 76 break 77 default: 78 l.Level = LEVEL_INFO 79 break 80 } 81 82 l.RespBody = l.writer.buffer.String() 83 return l 84} 85 86func (l *Log) CaptureError(err interface{}) *Log { 87 l.Level = LEVEL_FATAL 88 switch rVal := err.(type) { 89 case error: 90 l.RespBody = rVal.Error() 91 l.error = rVal 92 break 93 default: 94 l.RespBody = fmt.Sprint(rVal) 95 l.error = errors.New(l.RespBody) 96 break 97 } 98 99 return l 100} 101 102func (l *Log) Write(outWriter io.Writer) *Log { 103 l.TimeUse = time.Now().Sub(l.startAt).String() 104 oJson, _ := json.Marshal(l) 105 fmt.Fprintln(outWriter, string(oJson)) 106 return l 107} 108 109func (l *Log) Report() { 110 if l.Level == LEVEL_INFO || l.Level == LEVEL_WARN { 111 return 112 } 113 114 client := sentry.Client() 115 client.SetHttpContext(l.conText.Request) 116 client.SetExtraContext(map[string]interface{}{"timeuse": l.TimeUse}) 117 switch { 118 case l.Level == LEVEL_FATAL: 119 client.CaptureError(l.Level, l.error) 120 break 121 case l.Level == LEVEL_ERROR: 122 client.CaptureMessage(l.Level, l.RespBody) 123 break 124 } 125}

由于Gin是一个轻路由框架,所以类似数据库操作和Redis操作并没有相应的包。这就需要我们自己去选择好用的包。

Package - 数据库操作

最初学习阶段使用了datbase/sql,但是这个包有个用起来很不爽的问题。


1pid := 10021 2rows, err := db.Query("SELECT title FROM `product` WHERE id=?", pid) 3if err != nil { 4 log.Fatal(err) 5} 6defer rows.Close() 7for rows.Next() { 8 var title string 9 if err := rows.Scan(&title); err != nil { 10 log.Fatal(err) 11 } 12 fmt.Printf("%s is %d\n", title, pid) 13} 14if err := rows.Err(); err != nil { 15 log.Fatal(err) 16}

上述代码,如果select的不是title,而是*,这时就需要提前把表结构中的所有字段都定义成一个变量,然后传给Scan方法。

这样,如果一张表中有十个以上字段的话,开发过程就会异常麻烦。那么我们期望的是什么呢。提前定义字段是必须的,但是正常来说应该是定义成一个结构体吧? 我们期望的是查询后可以直接将查询结果转换成结构化数据。

花了点时间寻找,终于找到了这么一个包——github.com/jmoiron/sqlx。


1// You can also get a single result, a la QueryRow 2 jason = Person{} 3 err = db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason") 4 fmt.Printf("%#v\n", jason) 5 // Person{FirstName:"Jason", LastName:"Moiron", Email:"jmoiron@jmoiron.net"} 6 7 // if you have null fields and use SELECT *, you must use sql.Null* in your struct 8 places := []Place{} 9 err = db.Select(&places, "SELECT * FROM place ORDER BY telcode ASC") 10 if err != nil { 11 fmt.Println(err) 12 return 13 }

sqlx其实是对database/sql的扩展,这样一来开发起来是不是就爽多了,嘎嘎~

为什么不用ORM? 还是上一节说过的,尽量不用过度封装的包。

Package - Redis操作

最初我们使用了redigo【github.com/garyburd/redigo/redis】,使用上倒是没有什么不爽的,但是在压测的时候发现一个问题,即连接池的使用。


1func factory(name string) *redis.Pool { 2 conf := config.Get("redis." + name).(*toml.TomlTree) 3 host := conf.Get("host").(string) 4 port := conf.Get("port").(string) 5 password := conf.GetDefault("passwd", "").(string) 6 fmt.Printf("conf-redis: %s:%s - %s\r\n", host, port, password) 7 8 pool := &redis.Pool{ 9 IdleTimeout: idleTimeout, 10 MaxIdle: maxIdle, 11 MaxActive: maxActive, 12 Dial: func() (redis.Conn, error) { 13 address := fmt.Sprintf("%s:%s", host, port) 14 c, err := redis.Dial("tcp", address, 15 redis.DialPassword(password), 16 ) 17 if err != nil { 18 exception.Catch(err) 19 return nil, err 20 } 21 22 return c, nil 23 }, 24 } 25 return pool 26} 27 28/** 29 * 获取连接 30 */ 31func getRedis(name string) redis.Conn { 32 return redisPool[name].Get() 33} 34 35/** 36 * 获取master连接 37 */ 38func Master(db int) RedisClient { 39 client := RedisClient{"master", db} 40 return client 41} 42 43/** 44 * 获取slave连接 45 */ 46func Slave(db int) RedisClient { 47 client := RedisClient{"slave", db} 48 return client 49}

以上是定义了一个连接池,这里就产生了一个问题,在redigo中执行redis命令时是需要自行从连接池中获取连接,而在使用后还需要自己将连接放回连接池。最初我们就是没有将连接放回去,导致压测的时候一直压不上去。

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


1func factory(name string) *redis.Client { 2 conf := config.Get("redis." + name).(*toml.TomlTree) 3 host := conf.Get("host").(string) 4 port := conf.Get("port").(string) 5 password := conf.GetDefault("passwd", "").(string) 6 fmt.Printf("conf-redis: %s:%s - %s\r\n", host, port, password) 7 8 address := fmt.Sprintf("%s:%s", host, port) 9 return redis.NewClient(&redis.Options{ 10 Addr: address, 11 Password: password, 12 DB: 0, 13 PoolSize: maxActive, 14 }) 15} 16 17/** 18 * 获取连接 19 */ 20func getRedis(name string) *redis.Client { 21 return factory(name) 22} 23 24/** 25 * 获取master连接 26 */ 27func Master() *redis.Client { 28 return getRedis("master") 29} 30 31/** 32 * 获取slave连接 33 */ 34func Slave() *redis.Client { 35 return getRedis("slave") 36}

可以看到,这个包就是直接返回需要的连接了。

那么我们去看一下他的源码,连接有没有放回去呢。


1func (c *baseClient) conn() (*pool.Conn, bool, error) { 2 cn, isNew, err := c.connPool.Get() 3 if err != nil { 4 return nil, false, err 5 } 6 if !cn.Inited { 7 if err := c.initConn(cn); err != nil { 8 _ = c.connPool.Remove(cn, err) 9 return nil, false, err 10 } 11 } 12 return cn, isNew, nil 13} 14 15func (c *baseClient) putConn(cn *pool.Conn, err error, allowTimeout bool) bool { 16 if internal.IsBadConn(err, allowTimeout) { 17 _ = c.connPool.Remove(cn, err) 18 return false 19 } 20 21 _ = c.connPool.Put(cn) 22 return true 23} 24 25func (c *baseClient) defaultProcess(cmd Cmder) error { 26 for i := 0; i <= c.opt.MaxRetries; i++ { 27 cn, _, err := c.conn() 28 if err != nil { 29 cmd.setErr(err) 30 return err 31 } 32 33 cn.SetWriteTimeout(c.opt.WriteTimeout) 34 if err := writeCmd(cn, cmd); err != nil { 35 c.putConn(cn, err, false) 36 cmd.setErr(err) 37 if err != nil && internal.IsRetryableError(err) { 38 continue 39 } 40 return err 41 } 42 43 cn.SetReadTimeout(c.cmdTimeout(cmd)) 44 err = cmd.readReply(cn) 45 c.putConn(cn, err, false) 46 if err != nil && internal.IsRetryableError(err) { 47 continue 48 } 49 50 return err 51 } 52 53 return cmd.Err() 54}

可以看到,在这个包中的底层操作会先去connPool中Get一个连接,用完之后又执行了putConn方法将连接放回connPool。

结束语


1package main 2 3import ( 4 "github.com/gin-gonic/gin" 5 6 "gosapi/application/library/initd" 7 "gosapi/application/routers" 8) 9 10func main() { 11 env := initd.ConfTree.Get("ENVIRONMENT").(string) 12 gin.SetMode(env) 13 14 router := gin.New() 15 routers.Register(router) 16 17 router.Run(":7321") // listen and serve on 0.0.0.0:7321 18}

3月21日开始写main,现在已经上线一个星期了,暂时还没发现什么问题。

经过压测对比,在性能上提升了大概四倍左右。原先响应时间在70毫秒左右,现在是10毫秒左右。原先的吞吐量大概在1200左右,现在是3300左右。


原文发布时间为:2018-08-29

本文来自云栖社区合作伙伴“Golang语言社区”,了解相关信息可以关注“ Golang语言社区”。