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?