目前在用go写同步对战服务器这块。这门编程语言很久以前虽然学过,不过时间久了也用的较少,时间久了忘了很多。在写服务器过程中,可以享受到go对高性能并发的内置支持,但同时也会遇到一些坑,这里针对go的一些问题,记录一些笔记,包括一些坑的解决办法。
(1)go的异常处理通过defer recover来实现,不支持函数局部代码块异常处理。
(2)go的异常不像java那样区分类型,包括官方的标准库也是,更多的是通过运行时错误码来判断。处理异常的时候,代码维护和可读方面没有java那么直观。
针对这一点,我思考了下,实际上因为go支持闭包 可以通过闭包来代替代码块处理局部异常;然后go panic抛出的异常可以是任意类型的,而go支持通过反射来动态判断类型,所以可以通过这两点模拟go版的区分类型的try catch机制。
go语言内置的一些数据类型支持,包括slice,map,list等都存在线程安全问题。对于大部分通用场景来说,使用go内置的读写锁即可满足需求;不过也可以使用channel代替锁,通过channel协调读写线程,实现线程安全。此外,也可以使用atomic库实现更灵活的功能。
参考相关文章:https://www.jianshu.com/p/df973e890663
go语言为了加快编译速度,目前都没有支持宏和泛型,甚至不支持重载,而内置的数学库也只是支持float64型,对于复杂的业务场景来说,非常不便。 所幸有些第三方库帮我们处理了一些脏活累活,比如数学扩展工具库 https://godoc.org/modernc.org/mathutil
由于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));特别常见的一点是当main函数退出之后,程序就会退出,不会等待其他线程退出。通常会适用wait.group来实现线程依赖等待。
最近对比了messagepack和protobuf在go上面的性能表现。
序列化耗时反序列化耗时序列化数据大小原始数据\\180protobuf10012045messagepack200240210protobuf在性能上确实非常优越,速度快,体积小;但是也存在一些明显的缺陷:
1.protobuf导出的数据类型内部如果存在嵌套结构,那么嵌套结构都是使用指针关联的,一方面在反序列化过程中会发生大量内存分配操作,容易产生大量内存碎片,对于内存相对较小的机器非常不利,另一方面不利于结构体深拷贝操作,强行深拷贝会存在性能问题。
2.对于所有没有赋值或者赋了默认值的字段,在反序列化过程中都会初始化为空指针或者零值,会存在两个隐患:
1.一旦产生字段赋值操作,很容易出现空指针异常。2.未赋值的数据结构指针(空指针)依旧可以正常使用,只不过获取的字段值都是空指针或零值,容易误导开发者以为该结构体实例是存在实体的。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) } // net.Conn的实际数据结构 type conn struct { fd *netFD }实践中,总结了一下指针引用和值引用的适用场景:
如果对象包含持续的状态变化,那么应当使用指针引用,避免产生副本导致状态不同步、锁失效等“离奇”问题。如果对象是简单的无状态数据: 如果对象非常小,那么适合使用值引用。如果对象较大,那么适合使用指针引用,避免数据拷贝导致执行效率降低、内存占用明显升高。使用uintptr类型的值保存原始对象指针不会阻止该对象gc,需要结合runtime.KeepAlive()来使用。要尽量避免这种用法。
下面这段代码会导致后续go线程中调用的handler是后面遍历到的其他handler
func HandleRequestParallalThreadUnsafe(jobInfo *HandleJobInfo) { for _, handler := range this.handlers { if handler != nil { go func() { defer func() { if err := recover(); err != nil { log.Println("HandleRequestParallalThreadUnsafe-error:", err) } }() handler(jobInfo) }() } } }正确写法:
func HandleRequestParallalThreadUnsafe(jobInfo *HandleJobInfo) { handlers := make([]RequestHandler, len(this.handlers)) //通过创建副本,避免this.handlers被动态修改出现遍历bug copy(handlers, this.handlers) for _, handler := range handlers { if handler != nil { //不使用闭包变量会导致后续go中调用的handler是后面遍历到的handler thandler := handler go func() { defer func() { if err := recover(); err != nil { log.Println("HandleRequestParallalThreadUnsafe-error:", err) } }() thandler(jobInfo) }() } } }go内置提供的锁通常是这样用的:
var lock sync.RWMutex func TestTreadLock1(){ lock.Lock() defer lock.Unlock() ... } func TestTreadLock2(){ lock.Lock() ... lock.Unlock() ... }这里其实可以封装改进一下,让锁的使用更严格,不容易出现死锁:
import "sync" type RWLock struct { lock sync.RWMutex } func (this *RWLock) LockWrite(call func()) { defer this.lock.Unlock() this.lock.Lock() call() } func (this *RWLock) LockRead(call func()) { defer this.lock.RUnlock() this.lock.RLock() call() } var lock RWLock{} func TestTreadLock3(){ lock.LockWrite(func() { ... }) lock.LockRead(func() { ... }) }在个人笔记本上并发测试,耗时如下,可以看出并发越高,atomic性能越不如lock。另外经过反复测试,发现atomic的耗时波动非常大,不如lock稳定。
func BenchmarkGenUniqueId64(b *testing.B) { var wg sync.WaitGroup count:=1 wg.Add(count) for k:=0;k<count;k++{ go func() { for i := 0; i < 100000; i++ { GenUniqueId64() } wg.Done() }() } wg.Wait() } func BenchmarkGenUniqueId64_2(b *testing.B) { var wg sync.WaitGroup count:=1 wg.Add(count) for k:=0;k<count;k++{ go func() { for i := 0; i < 100000; i++ { GenUniqueId64_2() } wg.Done() }() } wg.Wait() }测试结果:
count == 1:BenchmarkGenUniqueId640.00660 ns/opBenchmarkGenUniqueId64_20.00330 ns/op count == 2:BenchmarkGenUniqueId640.0240 ns/opBenchmarkGenUniqueId64_20.0280 ns/op count == 3:BenchmarkGenUniqueId640.0500 ns/opBenchmarkGenUniqueId64_20.0730 ns/op count == 4:BenchmarkGenUniqueId640.0790 ns/opBenchmarkGenUniqueId64_20.120 ns/op count == 6:BenchmarkGenUniqueId640.139 ns/opBenchmarkGenUniqueId64_20.201 ns/op count == 8:BenchmarkGenUniqueId640.197 ns/opBenchmarkGenUniqueId64_20.398 ns/opgo的单元测试推荐goland内置单元测试功能。goland内置提供生成表单驱动单元测试的功能,提供单元测试覆盖率统计。
参考 golang 单元测试(gotests、mockery自动生成)参考 java单元测试代码,自动生成这几天观察项目内存泄漏问题时,发现内存占用居高不下,以为有严重的内存泄漏问题,但是细查之后并没有什么发现,怀疑go内存回收方面可能有奇怪的设定。后来参考下文,发现确实如此,使用 debug.FreeOSMemory() 强制返还系统内存,内存占用立即降到 135MB,反复返回测试同样稳定在135MB。说明没有明显的内存泄漏问题。
参考 Go内存泄漏?不是那么简单!
