Golang踩坑记录-1
平时 Golang 开发中会遇到的“坑点”,总结一下,避免重复踩坑
Interface
看代码,答问题
代码语言:javascript复制func main() {
var i interface{}
fmt.Println(i == nil)
}
结果:
代码语言:javascript复制true
再看如下代码:
代码语言:javascript复制func main() {
var p map[string]string
var i interface{} = p
fmt.Println(i == nil)
}
结果:
代码语言:javascript复制false
代码语言:javascript复制package main
import (
"code.byted.org/live/utils/errors"
"fmt"
)
func Test() *errors.LiveError {
// ...
// if xxx {
return nil
// }
//...
}
func main() {
var err error
err = Test()
if err != nil {
//
fmt.Println("err is not nil")
}
}
分析
在Go语言中,一个interface{}
类型的变量包含两个指针,一个指向其类型,另一个指向真正的值。只有当类型和值都是nil的时候,才等于nil。当我们将一个具体类型的值赋值给一个interface类型的变量的时候,就同时把类型和值都赋值给了interface里的两个指针。如果这个具体类型的值是nil的话,interface变量依然会存储对应的类型指针和值指针。这个时候拿这个interface变量去和nil常量进行比较的话就会返回false。实战的踩坑 网上的实战例子,详细参考及详解 https://studygolang.com/articles/10635 这是我们在GoWorld分布式游戏服务器的开发中,碰到的一个实际的bug。由于GoWorld支持多种不同的数据库(包括MongoDB,Redis等)来保存服务端对象,因此GoWorld在上层提供了一个统一的对象存储接口定义,而不同的对象数据库实现只需要实现EntityStorage接口所提供的函数即可。
// EntityStorage defines the interface of entity storage backendstype
EntityStorage interface {
List(typeName string) ([]common.EntityID, error)
Write(typeName string, entityID common.EntityID, data interface{}) error
Read(typeName string, entityID common.EntityID) (interface{}, error)
Exists(typeName string, entityID common.EntityID) (bool, error)
Close()
IsEOF(err error) bool
}
以一个使用Redis作为对象数据库的实现为例,函数OpenRedis连接Redis数据库并最终返回一个redisEntityStorage对象的指针。
代码语言:javascript复制// OpenRedis opens redis as entity storagefunc
OpenRedis(url string, dbindex int) *redisEntityStorage {
c, err := redis.DialURL(url)
if err != nil {
return nil
}
if dbindex >= 0 {
if _, err := c.Do("SELECT", dbindex); err != nil {
return nil
}
}
es := &redisEntityStorage{
c: c,
}
return es
}
在上层逻辑中,我们使用OpenRedis函数连接Redis数据库,并将返回的redisEntityStorage指针赋值个一个EntityStorage接口变量,因为redisEntityStorage对象实现了EntityStorage接口所定义的所有函数。
代码语言:javascript复制var storageEngine StorageEngine // 这是一个全局变量
storageEngine = OpenRedis(cfg.Url, dbindex)
if storageEngine != nil {
// 连接成功
...} else {
// 连接失败
...}
上面的代码看起来都很正常,OpenRedis在连接Redis数据库失败的时候会返回nil,然后调用者将返回值和nil进行比较,来判断是否连接成功。这个就是Go语言少有的几个深坑之一,因为不管OpenRedis函数是否连接Redis成功,都会运行连接成功的逻辑。
想要避开这个Go语言的坑,我们要做的就是避免将一个有可能为nil的具体类型的值赋值给interface变量。以上述的OpenRedis为例,一种方法是先对OpenRedis返回的结果进行非-nil检查,然后再赋值给interface变量,如下所示。
代码语言:javascript复制var storageEngine StorageEngine // 这是一个全局变量
redis := OpenRedis(cfg.Url, dbindex)if redis != nil {
// 连接成功
storageEngine = redis // 确定redis不是nil之后再赋值给interface变量} else {
// 连接失败
...}
另外一种方法是让OpenRedis函数直接返回EntityStorage接口类型的值,这样就可以把OpenRedis的返回值直接正确赋值给EntityStorage接口变量。
代码语言:javascript复制// OpenRedis opens redis as entity storagefunc
OpenRedis(url string, dbindex int) EntityStorage {
c, err := redis.DialURL(url)
if err != nil {
return nil
}
if dbindex >= 0 {
if _, err := c.Do("SELECT", dbindex); err != nil {
return nil
}
}
es := &redisEntityStorage{
c: c,
}
return es
}
避雷
想要避开这个Go语言的坑,我们要做的就是避免将一个有可能为nil的具体类型的值赋值给interface变量。
参考
- https://studygolang.com/articles/10635
Json反序列化
看代码,答问题
代码语言:javascript复制package main
import (
"encoding/json"
"fmt"
)
func main() {
originPid := int64(1234567890123456789)
jsonStr := fmt.Sprintf("{"userID":1,"config":{"pid":%v,"target_type":1}}", originPid)
var result map[string]interface{}
_ = json.Unmarshal([]byte(jsonStr), &result)
finalPid := result["config"].(map[string]interface{})["pid"]
fmt.Printf("PID类型:t%T nPID值:t%fn", finalPid, finalPid)
fmt.Printf("Int64:%d", int64(finalPid.(float64)))
}
分析 To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:
JSON | Interface |
---|---|
booleans | bool |
numbers | float64 |
strings | string |
arrays | []interface{} |
objects | map[string]interface{} |
null | nil |
由于float64类型所支持的精度问题,我们会损失一丢丢精度……就如上面的转换所示,最后两位已然面目全非……
避雷
既然 json.Unmarshal 处理较大的数会产生精度问题,那么不要让它处理数字就行。json.Decoder 就能实现这样的操作。json.Decoder 支持这样一个方法:UseNumber
它是这样说明的:
UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.
UseNumber使Decoder将数字作为json.Number解析到interface{}中而不是float64。而 json.Number 提供将其转换为 Int64 类型的方法。所以,我们可以这样写:
代码语言:javascript复制package main
import (
"bytes"
"encoding/json"
"fmt"
)
func main() {
originPid := int64(1234567890123456789)
jsonStr := fmt.Sprintf("{"userID":1,"config":{"pid":%v,"target_type":1}}", originPid)
var result map[string]interface{}
decoder := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
decoder.UseNumber()
_ = decoder.Decode(&result)
finalPid := result["config"].(map[string]interface{})["pid"]
fmt.Printf("PID类型:t%T nPID值:t%fn", finalPid, finalPid)
pidValue, _ := finalPid.(json.Number).Int64()
fmt.Printf("Int64:%d", pidValue)
}
参考
https://blog.moonlightwatch.me/2019/03/01/golang-json-unmarshal-number/
Json序列化
看代码,答问题
代码语言:javascript复制package main
import (
"encoding/json"
"fmt"
)
type MyData struct {
One int
two string
}
func main() {
in := MyData{1, "two"}
fmt.Printf("%#vn", in)
//prints main.MyData{One:1, two:"two"}
encoded, _ := json.Marshal(in)
fmt.Println(string(encoded))
//prints 是 {"One":1,"Two":"two"} ??
}
分析
代码语言:javascript复制package member
type Member struct {
id int
name string
email string
gender int
age int
}
package main
import "test/member"
func main() {
_ = member.Member{1, "小明", "xiaoming@163.com", 1, 18}
//会引发panic错误???
}
我们定义结构体字段名首字母是小写的,这意味着这些字段在包外不可见,因而无法在其他包中被访问,只允许包内访问。
下面的例子中,我们将Member声明在member包中,而后在main包中创建一个变量,但由于结构体的字段包外不可见,因此无法为字段赋初始值,无法按字段还是按索引赋值,都会引发panic错误。
参考
- https://www.liuin.cn/2018/08/23/Go-易采坑总结/
- https://juejin.im/post/5ca2f37ce51d4502a27f0539