21

Go实践笔记

 3 years ago
source link: https://studygolang.com/articles/29679
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go实践笔记

目前在用go写同步对战服务器这块。这门编程语言很久以前虽然学过,不过时间久了也用的较少,时间久了忘了很多。在写服务器过程中,可以享受到go对高性能并发的内置支持,但同时也会遇到一些坑,这里针对go的一些要点和问题,记录一些笔记和坑的解决办法。

1.go语言的异常处理机制不够完善。

(1)go的异常处理通过 defer recover 来实现,不支持函数局部代码块异常处理。

(2)go的异常不像java那样区分类型,包括官方的标准库也是,更多的是通过运行时错误码来判断。处理异常的时候,代码维护和可读方面没有java那么直观。

针对这一点,我思考了下,实际上因为go支持闭包 可以通过闭包来代替代码块处理局部异常;其次 go panic 抛出的异常可以是任意类型的,而go支持通过反射来动态判断类型,所以可以通过这两点模拟go版的区分类型的 try catch 机制。

2.go语言内置的一些数据类型都不是线程安全的。

go语言内置的一些数据类型支持,包括 slicemaplist 等都存在线程安全问题。对于大部分通用场景来说,使用go内置的 读写锁 即可满足需求;不过也可以使用 channel 代替锁,通过 channel 协调读写线程,实现线程安全。

参考相关文章: https://www.jianshu.com/p/df973e890663

3.go语言不支持宏,不支持泛型,也不支持重载

go语言为了加快编译速度,目前都没有支持宏和泛型,甚至不支持重载,而内置的数学库也只是支持float64型,对于复杂的业务场景来说,非常不便。

4.数据类型转换不方便

由于go语言不支持泛型,而go对于非基本数据类型的强制类型转换也没有提供专门的支持,在转换非基本数据类型时,显得很繁琐。例如下面这个例子,正确的转换很冗长,也无法使用宏简化(因为go也不支持宏):

` type _Int32 int32

var a *[]_Int32
var b *[]int32=(*[]int32)(a) // 语法报错
var d *[]int32=(*[]int32)(unsafe.Pointer(a)) //能正确转换

`

同样的,结构体的串行化也不方面,显得冗长,存在性能消耗:

`

type Struct struct{
    a int64
    b int32
}

sample:=Struct{}

size:=int(unsafe.Sizeof(&sample))
sh := &reflect.SliceHeader{
    Data: uintptr(unsafe.Pointer(&sample)),
    Len:  size,
    Cap:  size,
}

// 结构体->[]byte
data := *(*[]byte)(unsafe.Pointer(sh))

// []byte->结构体
originStruct:=(*Struct)(unsafe.Pointer(&data));

`

5.对goroutine调度控制较弱,需要注意安排好不同goroutine之间的依赖关系

6.关于 go版本的protobuf的性能问题

最近对比了messagepack和protobuf在go上面的性能表现。

序列化耗时 反序列化耗时 序列化数据大小 原始数据 \ \ 180 protobuf 100 120 45 messagepack 200 240 210

protobuf在性能上确实非常优越,速度快,体积小;但是也存在一些明显的缺陷:

1.protobuf导出的数据类型内部如果存在嵌套结构,那么嵌套结构都是使用指针关联的,一方面在反序列化过程中会发生大量内存分配操作,容易产生大量内存碎片,对于内存相对较小的机器非常不利,另一方面不利于结构体深拷贝操作,强行深拷贝会存在性能问题。

2.对于所有没有赋值或者赋了默认值的字段,在反序列化过程中都会初始化为空指针或者零值,会存在两个隐患:

  • 1.一旦产生字段赋值操作,很容易出现空指针异常。
  • 2.未赋值的数据结构指针(空指针)依旧可以正常使用,只不过获取的字段值都是空指针或零值,容易误导开发者以为该结构体实例是存在实体的。

7.go中所有的对象传递全部都是值传递,包括所谓的指针引用,实际上也是基于值传递实现的。go初学者稍不注意就会因此引发结构体副本之间的状态同步bug或者产生对一些第三方库(包括官方库)的误用。

比如socket编程中:

`

listener, err := net.Listen("tcp", "127.0.0.1:8001")

if err != nil {
        fmt.Println("err = ", err)
        return
}
defer listener.Close()
for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("err = ", err)
                return
     }
    // 处理用户请求, 新建一个协程
    // 为什么conn不使用指针传递呢,不会产生状态同步问题吗?
    // 这是因为net.Conn类型的数据实际上只保存了一个指针变量 `fd *netFD`,而所有的操作实现都是通过该指针来间接访问,所以无论conn拷贝几份,内部的fd指针值都一样。
    // 这应该是官方为了方便使用特意这样设计的
    // 但这用设计同时会引发另一个问题,就是 (&conn) 指针无法直接使用,虽然目前这不是什么大问题
    // 的
     go HandleConn(conn)

}

`

8.uintptr和gc

使用 uintptr 类型的值保存原始对象指针不会阻止该对象gc,需要结合 runtime.KeepAlive() 来使用,要尽量避免这种用法。

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK