go语言初体验

因为go语言原生支持协程,能够同时满足开发效率和程序性能,于是决定引入go语言进行改造。

本文由腾讯WeTest授权发布,未经许可不得转载。

 

背景

目前后台业务系统的大部分接口都是以同步阻塞式的方式工作,资源利用率低,单机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文件中引入:

c
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的内容

c

void C_test();//我们通过该函数来调用test库中的函数,这里的命名都是随便取的

在t.cpp文件的内容:

c
#include "t.h"
#include "test.h"
void C_test(){
    以C_test函数来封装test库中的函数
    ...
}

通过将t.cpp编译成目标文件:

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

有两种方式,一是将t.o打包进原来的libtest.a中,第二种当然就是将其打包为独立的一个静态库,这里推崇第二种方式:

c
ar -r libt.a t.o

这时候在对应的go文件中引入t.h和t.a即可完成go对C++库的调用。

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的特性,深层次的一些实现理解还很浅。另外,由于刚把第一批接口编码完成,下周估计才能上线,因此具体表现如何目前也还未可知。

 

关于腾讯WeTest (wetest.qq.com)

 

腾讯WeTest是腾讯游戏官方推出的一站式游戏测试平台,用十年腾讯游戏测试经验帮助广大开发者对游戏开发全生命周期进行质量保障。腾讯WeTest提供:适配兼容测试;云端真机调试;安全测试;耗电量测试;服务器性能测试;舆情监控等服务。

最新文章
1重磅上新:Android 15开发者预览版云真机,尽享全新特性 Android 15 预览版计划从 2024 年 2 月开始启动,到向 AOSP 和 OEM 提供最终的公开版本时结束,最终版本预计将在今年年底发布。
2免费加码!超值的新人福利来了! 为了向客户提供更优质的服务,释放技术红利,腾讯WeTest新增赠送很多安全类产品额度。平台用户在完成个人认证和企业创建能进一步享有更多服务礼包。
4跨越界限!PerfDog Evo(v10.0)版,打破游戏与APP性能测试壁垒! 跨越界限!PerfDog Evo(v10.0)版,打破游戏与APP性能测试壁垒!
5腾讯WeTest :为用户开新篇,八周年惠享巨献! “腾讯WeTest八周年惠享巨献”活动盛大开启!热门产品低至1折2,购买核心服务立享满减优惠,等您来参与!
购买
客服
反馈