今天在作数据库数据读取时, 首先经过多个 goroutine 将从数据库读取的数据写入 channel, 同时经过另外一个 goroutine 从 channel 中读取数据进行分析.golang
就是这么简单的一个功能, 在读取数据的时候不按期的会出以下错误:数据库
[signal SIGSEGV: segmentation violation code=0x1 addr=0x7f2227fe004d pc=0x52eb6f]
数据库是 boltdb, 错误的位置老是出在 json.Unmarshal 的地方:json
1 for v := range outCh { 2 var data OmsData 3 if err := json.Unmarshal(v, &data); err != nil { 4 log.Fatalf("json unmarshal error: %v\n", err) 5 } 6 }
outCh 中就是从数据库读取的数据. 刚开始觉得是数据中的数据有错误, 后来发现 err 也捕获不到, 每次都是 panic 错误.code
因而, 就分析了下整个过程, 读取数据的 goroutine 代码大体以下:内存
1 func readOneDB(db *bolt.DB, outCh chan []byte) { 2 defer db.Close() 3 4 // 获取 db 中的全部 bucket 5 bucketNames := getAllBucketNames(db) 6 7 err := db.View(func(tx *bolt.Tx) error { 8 9 for _, bName := range bucketNames { 10 11 bucket := tx.Bucket([]byte(bName)) 12 13 bucket.ForEach(func(_ []byte, v []byte) error { 14 // 把 bucket 中的value 写入 channel 15 outCh <- v 16 return nil 17 }) 18 } 19 20 return nil 21 }) 22 23 if err != nil { 24 log.Fatal(err) 25 } 26 }
读取数据的代码也很简单, 没有明显的问题.get
读写 channel 的代码就是上面那么简单, 一眼就能看明白, 为何会 panic? 我进行了屡次实验, 发现以下现象:io
基于上面的分析, 我当时就以为是否是 db.Close() 以后, 把写入 channel 的一些数据也释放了.class
因而, 我尝试在写入 channel 以前, 把数据复制一份, 改造 readOneDB 以下:引用
1 func readOneDB(db *bolt.DB, outCh chan []byte) { 2 defer db.Close() 3 4 bucketNames := getAllBucketNames(db) 5 6 err := db.View(func(tx *bolt.Tx) error { 7 8 for _, bName := range bucketNames { 9 10 bucket := tx.Bucket([]byte(bName)) 11 12 bucket.ForEach(func(_ []byte, v []byte) error { 13 // ** 改造的部分 ** 14 // 改造的方式就是把 bucket 中的数据copy一份放入channel 15 // 而不是像以前那样, 直接把 v 放入 channel 16 nb := make([]byte, len(v)) 17 copy(nb, v) 18 outCh <- nb 19 return nil 20 }) 21 } 22 23 return nil 24 }) 25 26 if err != nil { 27 log.Fatal(err) 28 } 29 }
这样改造以后, 就再也没有出现内存错误了!channel
golang 的 channel 中写入数据的时候, 若是写入的是引用类型, 那么应该写入的是数据的地址, 而不是完整的数据, 若是该地址对应的数据被 GC 回收的话, 在使用数据的地方就会致使 内存错误(panic)
这种问题很隐蔽, 由于 GC 的回收时机没法控制, 咱们能作的就是在代码层面保证要用的数据不会被回收.