13input.body = d
14ife == false{
15returninput
16}
17
18returninput
19}
20
21func(input *I)post(p string)*I{
22d, e := Context.GetPostForm(p)
23input.body = d
24ife == false{
25returninput
26}
27
28returninput
29}
30
31func(input *I)String()string{
32returninput.body
33}
34
35func(input *I)Atoi()int{
36body, _ := strconv.Atoi(input.body)
37returnbody
38}
1packageinput
2
3//获取GET参数
4funcGet(p string)*I{
5i := new(I)
6returni.get(p)
7}
8
9//获取POST参数
10funcPost(p string)*I{
11i := new(I)
12returni.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。
1packageginx
2
3import(
4"fmt"
5"io"
6"os"
7"time"
8
9"github.com/gin-gonic/gin"
10"sao.cn/configs"
11)
12
13var(
14logPath string
15lastDay int
16)
17
18funcinit(){
19logPath = configs.Load().Get( "SYS_LOG_PATH").( string)
20_, err := os.Stat(logPath)
21iferr != nil{
22os.Mkdir(logPath, 0755)
23}
24}
25
26funcdefaultWriter()io.Writer{
27writerCheck()
28returngin.DefaultWriter
29}
30
31funcdefaultErrorWriter()io.Writer{
32writerCheck()
33returngin.DefaultErrorWriter
34}
35
36funcwriterCheck(){
37nowDay := time.Now().Day()
38ifnowDay != lastDay {
39varfile *os.File
40filename := time.Now().Format( "2006-01-02")
41logFile := fmt.Sprintf( "%s/%s-%s.log", logPath, "gosapi", filename)
42
43file, _ = os.Create(logFile)
44iffile != nil{
45gin.DefaultWriter = file
46gin.DefaultErrorWriter = file
47}
48}
49
50lastDay = nowDay
51}
1packageginx
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
17funcLogger()gin.HandlerFunc{
18returnLoggerWithWriter(defaultWriter())
19}
20
21funcLoggerWithWriter(outWrite io.Writer)gin.HandlerFunc{
22returnfunc(c *gin.Context){
23NewLog(c).CaptureOutput().Write(outWrite).Report()
24}
25}
26
27const(
28LEVEL_INFO = "info"
29LEVEL_WARN = "warning"
30LEVEL_ERROR = "error"
31LEVEL_FATAL = "fatal"
32)
33
34typeLog struct{
35startAt time.Time
36conText *gin.Context
37writer responseWriter
38error error
39
40Level string
41Time string
42ClientIp string
43Uri string
44ParamGet url.Values `json:"pGet"`
45ParamPost url.Values `json:"pPost"`
46RespBody string
47TimeUse string
48}
49
50funcNewLog(c *gin.Context)*Log{
51bw := responseWriter{buffer: bytes.NewBufferString( ""), ResponseWriter: c.Writer}
52c.Writer = &bw
53
54clientIP := c.ClientIP()
55path := c.Request.URL.Path
56method := c.Request.Method
57pGet := c.Request.URL.Query()
58varpPost url.Values
59ifmethod == "POST"{
60c.Request.ParseForm()
61pPost = c.Request.PostForm
62}
63return&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{
67l.conText.Next()
68o := new(output.O)
69json.Unmarshal(l.writer.buffer.Bytes(), o)
70switch{
71caseo.Status_code != 0&& o.Status_code < 20000:
72l.Level = LEVEL_ERROR
73break
74caseo.Status_code > 20000:
75l.Level = LEVEL_WARN
76break
77default:
78l.Level = LEVEL_INFO
79break
80}
81
82l.RespBody = l.writer.buffer.String()
83returnl
84}
85
86func(l *Log)CaptureError(err interface{})*Log{
87l.Level = LEVEL_FATAL
88switchrVal := err.( type) {
89caseerror:
90l.RespBody = rVal.Error()
91l.error = rVal
92break
93default:
94l.RespBody = fmt.Sprint(rVal)
95l.error = errors.New(l.RespBody)
96break
97}
98
99returnl
100}
101
102func(l *Log)Write(outWriter io.Writer)*Log{
103l.TimeUse = time.Now().Sub(l.startAt).String()
104oJson, _ := json.Marshal(l)
105fmt.Fprintln(outWriter, string(oJson))
106returnl
107}
108
109func(l *Log)Report(){
110ifl.Level == LEVEL_INFO || l.Level == LEVEL_WARN {
111return
112}
113
114client := sentry.Client()
115client.SetHttpContext(l.conText.Request)
116client.SetExtraContext( map[ string] interface{}{ "timeuse": l.TimeUse})
117switch{
118casel.Level == LEVEL_FATAL:
119client.CaptureError(l.Level, l.error)
120break
121casel.Level == LEVEL_ERROR:
122client.CaptureMessage(l.Level, l.RespBody)
123break
124}
125}
由于Gin是一个轻路由框架,所以类似数据库操作和Redis操作并没有相应的包。这就需要我们自己去选择好用的包。
Package - 数据库操作
最初学习阶段使用了datbase/sql,但是这个包有个用起来很不爽的问题。
1pid := 10021
2rows, err:= db.Query( "SELECT title FROM `product` WHERE id=?", pid)
3iferr!= nil {
4log.Fatal( err)
5}
6defer rows.Close()
7forrows. Next() {
8var title string
9iferr:= rows.Scan(&title); err!= nil {
10log.Fatal( err)
11}
12fmt.Printf( "%s is %dn", title, pid)
13}
14iferr:= rows. Err(); err!= nil {
15log.Fatal( err)
16}
上述代码,如果select的不是title,而是*,这时就需要提前把表结构中的所有字段都定义成一个变量,然后传给Scan方法。
这样,如果一张表中有十个以上字段的话,开发过程就会异常麻烦。那么我们期望的是什么呢。提前定义字段是必须的,但是正常来说应该是定义成一个结构体吧? 我们期望的是查询后可以直接将查询结果转换成结构化数据。
花了点时间寻找,终于找到了这么一个包——github.com/jmoiron/sqlx。
1// You can also get a single result, a la QueryRow
2jason = Person{}
3err = db.Get(&jason, "SELECT * FROM person WHERE first_name=$1", "Jason")
4fmt.Printf( "%#vn", jason)
5// Person{FirstName: "Jason", LastName: "Moiron", Email: "jmoiron@jmoiron.net"}
6
7// ifyou have null fields anduseSELECT *, you must usesql.Null* in your struct
8places := []Place{}
9err = db.Select(&places, "SELECT * FROM place ORDER BY telcode ASC")
10iferr != nil {
11fmt.Println(err)
12return
13}
sqlx其实是对database/sql的扩展,这样一来开发起来是不是就爽多了,嘎嘎~
为什么不用ORM? 还是上一节说过的,尽量不用过度封装的包。
Package - Redis操作
最初我们使用了redigo【github.com/garyburd/redigo/redis】,使用上倒是没有什么不爽的,但是在压测的时候发现一个问题,即连接池的使用。
1funcfactory(name string)*redis.Pool{
2conf := config.Get( "redis."+ name).(*toml.TomlTree)
3host := conf.Get( "host").( string)
4port := conf.Get( "port").( string)
5password := conf.GetDefault( "passwd", "").( string)
6fmt.Printf( "conf-redis: %s:%s - %srn", host, port, password)
7
8pool := &redis.Pool{
9IdleTimeout: idleTimeout,
10MaxIdle: maxIdle,
11MaxActive: maxActive,
12Dial: func()(redis.Conn, error){
13address := fmt.Sprintf( "%s:%s", host, port)
14c, err := redis.Dial( "tcp", address,
15redis.DialPassword(password),
16)
17iferr != nil{
18exception.Catch(err)
19returnnil, err
20}
21
22returnc, nil
23},
24}
25returnpool
26}
27
28/**
29* 获取连接
30*/
31funcgetRedis(name string)redis.Conn{
32returnredisPool[name].Get()
33}
34
35/**
36* 获取master连接
37*/
38funcMaster(db int)RedisClient{
39client := RedisClient{ "master", db}
40returnclient
41}
42
43/**
44* 获取slave连接
45*/
46funcSlave(db int)RedisClient{
47client := RedisClient{ "slave", db}
48returnclient
49}
以上是定义了一个连接池,这里就产生了一个问题,在redigo中执行redis命令时是需要自行从连接池中获取连接,而在使用后还需要自己将连接放回连接池。最初我们就是没有将连接放回去,导致压测的时候一直压不上去。
那么有没有更好的包呢,答案当然是肯定的 —— gopkg.in/redis.v5
1funcfactory(name string)*redis.Client{
2conf := config.Get( "redis."+ name).(*toml.TomlTree)
3host := conf.Get( "host").( string)
4port := conf.Get( "port").( string)
5password := conf.GetDefault( "passwd", "").( string)
6fmt.Printf( "conf-redis: %s:%s - %srn", host, port, password)
7
8address := fmt.Sprintf( "%s:%s", host, port)
9returnredis.NewClient(&redis.Options{
10Addr: address,
11Password: password,
12DB: 0,
13PoolSize: maxActive,
14})
15}
16
17/**
18* 获取连接
19*/
20funcgetRedis(name string)*redis.Client{
21returnfactory(name)
22}
23
24/**
25* 获取master连接
26*/
27funcMaster()*redis.Client{
28returngetRedis( "master")
29}
30
31/**
32* 获取slave连接
33*/
34funcSlave()*redis.Client{
35returngetRedis( "slave")
36}
可以看到,这个包就是直接返回需要的连接了。
那么我们去看一下他的源码,连接有没有放回去呢。
1func(c *baseClient)conn()(*pool.Conn, bool, error){
2cn, isNew, err := c.connPool.Get()
3iferr != nil{
4returnnil, false, err
5}
6if!cn.Inited {
7iferr := c.initConn(cn); err != nil{
8_ = c.connPool.Remove(cn, err)
9returnnil, false, err
10}
11}
12returncn, isNew, nil
13}
14
15func(c *baseClient)putConn(cn *pool.Conn, err error, allowTimeout bool)bool{
16ifinternal.IsBadConn(err, allowTimeout) {
17_ = c.connPool.Remove(cn, err)
18returnfalse
19}
20
21_ = c.connPool.Put(cn)
22returntrue
23}
24
25func(c *baseClient)defaultProcess(cmd Cmder)error{
26fori := 0; i <= c.opt.MaxRetries; i++ {
27cn, _, err := c.conn()
28iferr != nil{
29cmd.setErr(err)
30returnerr
31}
32
33cn.SetWriteTimeout(c.opt.WriteTimeout)
34iferr := writeCmd(cn, cmd); err != nil{
35c.putConn(cn, err, false)
36cmd.setErr(err)
37iferr != nil&& internal.IsRetryableError(err) {
38continue
39}
40returnerr
41}
42
43cn.SetReadTimeout(c.cmdTimeout(cmd))
44err = cmd.readReply(cn)
45c.putConn(cn, err, false)
46iferr != nil&& internal.IsRetryableError(err) {
47continue
48}
49
50returnerr
51}
52
53returncmd.Err()
54}
可以看到,在这个包中的底层操作会先去connPool中Get一个连接,用完之后又执行了putConn方法将连接放回connPool。
结束语
1packagemain
2
3import(
4"github.com/gin-gonic/gin"
5
6"gosapi/application/library/initd"
7"gosapi/application/routers"
8)
9
10funcmain(){
11env := initd.ConfTree.Get( "ENVIRONMENT").( string)
12gin.SetMode(env)
13
14router := gin.New()
15routers.Register(router)
16
17router.Run( ":7321") // listen and serve on 0.0.0.0:7321
18}
3月21日开始写main,现在已经上线一个星期了,暂时还没发现什么问题。
经过压测对比,在性能上提升了大概四倍左右。原先响应时间在70毫秒左右,现在是10毫秒左右。原先的吞吐量大概在1200左右,现在是3300左右。
虽然Go很棒,但是我还是想说:PHP是最好的语言!(这句虽是原话,但小编持保留意见哈哈哈哈~)
ID:Golangweb