Festival & Fuck, Coding, Inner depth, Sister and Others.

某些文章会提到《为什么Go语言这么不受待见》,《真的没必要浪费心思在 Go 语言上》,《我为什么放弃Go语言》,《Why worse is better》等话题。经常重温这些话题,每次都会有新发现。最忌手里有了一个语言,心里便容不下另一个语言。

忽略细节、语法或者设计,Go语言各种好用。考虑到这些因素,Go被喷出翔都不为过。

本文不打算在细节、语法或者设计上扯淡,只举些例子,说一说如何用Go语言写出还凑合的代码。

类、对象、属性,可能还夹杂着一点设计模式

//代码来自 https://github.com/xgdapg/xconn/blob/master/xconn.go,已验证

//Conn 对应一个tcp连接
type Conn struct {
        //原生TCP连接
    conn         net.Conn
        //发送数据的channel(类似队列)
    send         chan *MsgData
        //消息处理方法
    msgHandler   MsgHandler
        //tcp缓冲区
    recvBuffer   []byte
        //消息一次封装,后续会进行二次封装
    msgPacker    MsgPacker
        //健康检查周期
    pingInterval uint
        //健康检查方法
    pingHandler  PingHandler
        //停止健康检查channel
    pingStop     chan bool
        //自定义关闭连接时所调用方法
    closeHandler CloseHandler
}

//MsgHandler 为自定义处理消息
type MsgHandler interface {
    HandleMsg(*MsgData)
}

//MsgPacker 为自定义拆包解包方式,为了性能可以直接将此段与recvLoop端合并
type MsgPacker interface {
    PackMsg(*MsgData) []byte
    UnpackMsg([]byte) *MsgData
}

//MsgData 自定义消息类型
type MsgData struct {
    Data []byte
    Ext  interface{}
}

//PingHandler 为自定义心跳命令
type PingHandler interface {
    HandlePing()
}

//CloseHandler 为连接断开时的自定义操作
type CloseHandler interface {
    HandleClose()
}

//NewConn 为处理新连接方式:启动两个协程,一个只负责读,一个只负责写,
//也可以认为开启了三个协程,第三个协程负责进行定时ping操作
func NewConn(conn net.Conn) *Conn {
    c := &Conn{
        conn:         conn,
        send:         make(chan *MsgData, 64),
        msgHandler:   nil,
        recvBuffer:   []byte{},
        msgPacker:    nil,
        pingInterval: 0,
        pingHandler:  nil,
        pingStop:     nil,
        closeHandler: nil,
    }

    go c.recvLoop()
    go c.sendLoop()

    return c
}

//recoverPanic 程序panic情况下的处理方法(例如向已经关闭的tcp连接写数据会造成panic)
func recoverPanic() {
    if err := recover(); err != nil {
        //fmt.Println(err)
    }
}

//SetMsgHandler 为 Getter Setter 方法
func (this *Conn) SetMsgHandler(hdlr MsgHandler) {
    this.msgHandler = hdlr
}

//SetMsgPacker 为 Getter Setter 方法
func (this *Conn) SetMsgPacker(packer MsgPacker) {
    this.msgPacker = packer
}

//SetPing 为 Getter Setter 方法
func (this *Conn) SetPing(sec uint, hdlr PingHandler) {
    this.pingInterval = sec
    this.pingHandler = hdlr
    if this.pingStop == nil {
        this.pingStop = make(chan bool)
    }
    if sec > 0 {
        go this.pingLoop()
    }
}

//SetCloseHandler 为 Getter Setter 方法
func (this *Conn) SetCloseHandler(hdlr CloseHandler) {
    this.closeHandler = hdlr
}

//pingLoop 为定时健康检查操作
func (this *Conn) pingLoop() {
    defer recoverPanic()
    for {
        select {
        case <-this.pingStop:
            return
        case <-time.After(time.Duration(this.pingInterval) * time.Second):
            this.Ping()
        }
    }
}

//RawConn 返回原始的tcp连接
func (this *Conn) RawConn() net.Conn {
    return this.conn
}

//recvLoop 用于处理接收到的tcp包,并进行拆包等操作,然后调用recvMsg方法进行处理
func (this *Conn) recvLoop() {
    defer recoverPanic()
    defer this.Close()
    buffer := make([]byte, 2048)
        //一次封包协议:四个字节(int32)表示包长度,根据包长度截取消息长度作为包。
    for {
        bytesRead, err := this.conn.Read(buffer)
        if err != nil {
            return
        }

        this.recvBuffer = append(this.recvBuffer, buffer[0:bytesRead]...)
        for len(this.recvBuffer) > 4 {
            length := binary.BigEndian.Uint32(this.recvBuffer[0:4])
            readToPtr := length + 4
            if uint32(len(this.recvBuffer)) < readToPtr {
                break
            }
            if length == 0 {
                if this.pingHandler != nil {
                    this.pingHandler.HandlePing()
                }
            } else {
                buf := this.recvBuffer[4:readToPtr]
                go this.recvMsg(buf)
            }
            this.recvBuffer = this.recvBuffer[readToPtr:]
        }
    }
}

//recvMsg 为代理,实际执行的是后台的HandleMsg方法。
func (this *Conn) recvMsg(data []byte) {
    defer recoverPanic()
    msg := &MsgData{
        Data: data,
        Ext:  nil,
    }
        //调用UnackMsg对信息进行二次解包
    if this.msgPacker != nil {
        msg = this.msgPacker.UnpackMsg(data)
    }
    if this.msgHandler != nil {
        this.msgHandler.HandleMsg(msg)
    }
}

//sendLoop 用于发送数据包
func (this *Conn) sendLoop() {
    defer recoverPanic()
    for {
        msg, ok := <-this.send
        if !ok {
            break
        }

        go this.sendMsg(msg)
    }
}

//sendMsg 用于发送数据包,实际先调用PackMsg进行信息持久化,然后二次封包,转换为本框架能接受的形式
func (this *Conn) sendMsg(msg *MsgData) {
    defer recoverPanic()
    sendBytes := make([]byte, 4)
    if msg != nil {
        data := msg.Data
        if this.msgPacker != nil {
            data = this.msgPacker.PackMsg(msg)
        }
        length := len(data)
        binary.BigEndian.PutUint32(sendBytes, uint32(length))
        sendBytes = append(sendBytes, data...)
    }
    this.conn.Write(sendBytes)
}

//Close 关闭连接
func (this *Conn) Close() {
    defer recoverPanic()
    this.conn.Close()
    close(this.send)
    if this.pingStop != nil {
        close(this.pingStop)
    }
    if this.closeHandler != nil {
        this.closeHandler.HandleClose()
    }
}

//SendMsg 用于发送数据
func (this *Conn) SendMsg(msg *MsgData) {
    this.send <- msg
}

//SendData 用于发送数据
func (this *Conn) SendData(data []byte) {
    this.SendMsg(&MsgData{Data: data, Ext: nil})
}

//Ping 用于健康监测
func (this *Conn) Ping() {
    go this.sendMsg(nil)
}

作为一个专用于处理TCP链接的框架,实际上xconn(上文中的代码)进行了两次封装,连消息发送、信息拆包封包、甚至接收信息都进行了二次封装。

实际代码中,可以进行简化操作,将二次的部分简化为一次。

将代码写得和上面一样工整,便已经超越大部分猿了。

上个例子中作者用到了recover,用的很克制,却又恰到好处。

至于defer和panic

Go语言的try catch?

import (
    "fmt"
    "github.com/manucorporat/try"
)

func main() {
    try.This(func() {
        panic("my panic")

    }).Finally(func() {
        fmt.Println("this must be printed after the catch")

    }).Catch(func(e try.E) {
        // Print crash
        fmt.Println(e)
    })
}

以上代码纯属搞笑,个人不建议工程项目中使用如此写法,但是这种做法可以借鉴。

工程代码:(用于Go与数据库Transaction)

//代码来自《Go语言游戏项目应用情况汇报》

func (db *Database) Transaction(work func()) {
    db.lock.Lock()
    defer db.lock.UnLock()
    //事务控制
    defer func() {
        if err := recover; err == nil {
            db.commit(info)
        } else {
            db.rollback()
            //选择性抛出panic
            panic(TransError{err})
        }
    }()
    //执行传入的函数
    work()
}

《Go语言游戏项目应用情况汇报》是我所能找到的,为数不多的几个敢开放部分工程代码的分享。整体代码比较整洁,适于新手学习。

以上对err的处理方法写法,和《Errors are values》有异曲同工之妙。

err的一种处理方式

示范代码:来自《Errors are vales》

//这种写法强烈不推荐!!!!这就是许多人说的Go程序一大半都在check error
_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on
//重要的事情说两遍:不推荐,但是个人小项目这样写,完全没问题。

推荐如下写法:

var err error
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

也推荐如下写法:

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

其他:

正名:为什么选择Go语言。

答:因为简单,并且也不会别的。

建议:尽量选择Go1.6及以上版本,避免GC造成程序STW。

至于GC性能,可以参考 Go1.6中的gc pause已经完全超越JVM了吗?

原文禁止转载,因此提炼出关键字:从效果看XXX,但XXX;并且,XXX,XXX。

So, Why worse is better?