1. 程式人生 > >go語言初體驗

go語言初體驗

println add 數據 duration 頭文件 原來 ket mil turn

背景

目前後臺業務系統的大部分接口都是以同步阻塞式的方式工作,資源利用率低,單機qps有限。因為go語言原生支持協程,能夠同時滿足開發效率和程序性能,於是決定引入go語言進行改造。
主要是分享以下三點心得:

  • C/C++庫的封裝
  • map內部成員賦值,以及protobuf協議的支持
  • 網絡I/O超時處理

C/C++庫的封裝

環境搭建,語法這些就不贅述了。閱讀 A tour of go 可以很好的認識go語言。
引入go語言,碰到的第一個問題是如何復用已有的經過了長期線上檢驗的C/C++基礎庫,所幸go語言通過Cgo可以很方便的調用C庫中函數。具體方法見Command cgo。在使用過程中發現,Cgo可以支持C,但無法完美支持C++,在import C關鍵字上面include進來的頭文件中,如果有包含C++的頭文件時例如string,Cgo將會報錯,另外C++的namespace也會讓我們無法用go語言訪問namespace下的函數或變量。解決辦法是用C來包裹C++,通過C來調用C++,go來調用C解決。如我有一個用C++編寫的庫libtest.a和test.h,其中test.h中有包含C++中的string,這時候按照Command cgo中的方法,在test.go文件中引入:

packege test
// #cgo LDFLAGS: -L/usr/local/comm/lib -ltest -lstdc++
// #cgo CPPFLAGS: -I/usr/local/comm/include
// #include "test.h"

編譯過程中會報錯。註意,上面使用的都是絕對路徑,這是因為當我們在build go項目的時候,相對路徑是以執行build命令時的路徑來作為起點的,否則一旦build時的目錄改變,就會導致Cgo封裝失敗。
這時候,為了調用test庫,我們只好再寫一個t.h和t.cpp(隨便取的)
其中t.h的內容

#ifdef __cplusplus
extern "C" {
#endif
void C_test();//我們通過該函數來調用test庫中的函數,這裏的命名都是隨便取的 #ifdef __cplusplus } #endif

在t.cpp文件的內容:

#include "t.h"
#include "test.h"

#ifdef __cplusplus
extern "C" {
#endif

void C_test(){
    以C_test函數來封裝test庫中的函數
    ...
}

#ifdef __cplusplus
}
#endif

通過將t.cpp編譯成目標文件:

g++ -c t.cpp -Iabcd -Lefg -ltest

有兩種方式,一是將t.o打包進原來的libtest.a中,第二種當然就是將其打包為獨立的一個靜態庫,這裏推崇第二種方式:

ar -r libt.a t.o

這時候在對應的go文件中引入t.h和t.a即可完成go對C++庫的調用。

packege test
// #cgo LDFLAGS: -L./ -lt -L/usr/local/comm/lib -ltest -lstdc++
// #cgo CPPFLAGS: -I./ -I/usr/local/comm/include
// #include "t.h"

map內部成員賦值,以及protobuf協議的支持

在go語言的使用過程中,發現map結構中的value為自定義類型時,無法對該自定義類型內部的成員進行進行"寫"操作(map結構中返回的value不可對其成員尋址),如運行以下代碼:

package main
import "fmt"
type A struct{
    T int
}
func main(){
    m := make(map[int]A)
    a := A{1}
    m[1] = a
    m[1].T = 2
    fmt.Println(m)
}

編譯器會返回不可對m[1].T賦值的錯誤。但是當map中的value為指針時即可,如將第7,8,9行代碼換成如下:

    m := make(map[int]*A)
    a := A{1}
    m[1] = &a

這時即可完成對m[1].T的賦值。另外在map結構中的value類型為go的原生類型時則不存在這個問題(即byte/int8/16/32/64 float32/64,string,map,slice等等),如:

    m := make(map[int]map[int]int)
    ma := make(map[int]int)
    ma[1] = 1
    m[1] = ma
    m[1][1] = -1
    fmt.Println(m)

這時讀寫都沒問題。但是在slice中卻不存在這個問題。
在go中,將protobuf協議文件生成相應的go文件之後,對於每個字段生成的變量都是指針類型。protobuf協議在git上有golang開源的protobuf協議(畢竟都是google的東西),詳細使用可以參見鏈接這裏簡單談一下對於用指針的理解,使用指針可以更大程度的達到節流的目的,(而這裏的節流帶來的好處是更快的編解碼速度,數據包交互完成速度(因為要傳輸的數據量少了),節省帶寬),我們通過判斷指針值是否為nil,為nil時則直接跳過,不為nil時說明有數據才將其序列化。

網絡I/O超時處理

由於復雜的網絡環境,我們必須考慮對來自客戶端的每個請求的收包和回包以及系統內部調用鏈之間的網絡請求設置超時處理,否則系統可能會存在大量無法釋放的連接。

1 http server與client之間的超時設置

對於http服務器一般都是使用go標準庫中的http包,只需要簡單幾行代碼即可創建一個http server,如:

http.HandleFunc("/hello",func(w http.ResponseWriter,r *http.Request){
        io.WriteString(w,"hello world")
    });
http.ListenAndServe()

這裏其實是使用了內部的變量DefaultServeMux,但是它默認並不支持I/O超時,因此我們需要自己創建http.Server來提供服務:

svr := &http.Server{
    Addr:         "0.0.0.0:8080",
    ReadTimeout:  4 * time.Second,
    WriteTimeout: 4 * time.Second,
}
svr.ListenAndServe()

其中ReadTimeOut是從Accept請求後到RequestBody完全讀取的時間,WriteTimeOut是從RequstBody開始讀取到完整回包的時間。

2 系統內部調用鏈的超時設置

系統內部的服務之間經常需要協同服務才能完成對外的請求,因此通信無法避免。通過給建立好的連接對象net.Connd調用SetDeadline,SetReadDeadline,SetWriteDeadline三個方法設置Deadline可以達到對每次I/O進行超時控制,一旦超時後對該連接對象進行Close操作。顧名思義,就是分別對連接對象設置讀寫超時,讀超時和寫超時的函數,他們的設置是永久生效而不是只作用於一次I/O,一旦超時後將返回超時錯誤,因此每次I/O操作前都需要調用它們,這裏貼一個簡單的例子:

func SendOnePacket(conn net.Conn, packet []byte, timeout int64) error {
    conn.SetWriteDeadline(time.Now().Add(time.Duration(int64(time.Millisecond) * timeout)))
    for {
        n, err := conn.Write(packet)
        if err != nil {
            return errors.New("Write error:" + err.Error())
        }
        if n == len(packet) {
            return nil
        }
        packet = packet[n:]
    }
    return nil
}

func RecvOnePacket(conn net.Conn, timeout int64) ([]byte, error) {
    conn.SetReadDeadline(time.Now().Add(time.Duration(int64(time.Millisecond)*timeout)))
    recvBuf := bytes.NewBuffer(nil)
    var msgLen int = 0
    var buf [4096]byte
    for {
        n, err := conn.Read(buf[0:])
        recvBuf.Write(buf[0:n])
        if err != nil && err != io.EOF {
            str := string(recvBuf.Bytes())
            return nil, errors.New("Read Error:" + err.Error()+ " data:"+str)
        }
        msgLen = CheckPacket(recvBuf)
        if msgLen > 0 {
            break
        }
        if msgLen < 0 {
            return nil,errors.New("CheckPacket error,ret:" + strconv.FormatInt(int64(msgLen),10))
        }
    }
    packet := recvBuf.Bytes()
    return packet[:msgLen], nil
}

CheckPacket函數根據自己的通信協議來實現,用於檢查包是否完整,返回負數代表出錯,為0表示不完整,大於0表示是接收完成,且該值為該數據包的完整長度。其中RecvOnePacket中的buf數組其實非常講究,它會決定每一次的系統調用---Read函數,最多讀取多少個字節(如果傳入Read函數的切片長度為0的話直接就返回了),這個示例並為對數據緩存,因此出現粘包的話就雪崩了。

總結

目前小組內部也是剛引入go語言,對go的特性,深層次的一些實現理解還很淺。

go語言初體驗