Golang踩坑记录

2023-07-08 16:01:20 浏览数 (1)

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接口所提供的函数即可。

代码语言:javascript复制
// 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

0 人点赞