節操,程式碼,修養,妹子和其他(Go語言版)

大舒發表於2019-02-16

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?

相關文章