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