我的golang易错笔记

812 阅读4分钟

1. JSON 四状态问题

在Go中通过标准库解析JSON时,我们经常要对单个字段区分四个状态,分别是:

  • 存在且 非0 值
  • 存在为 0 值
  • 存在为 null
  • 缺失 四个状态中,null 和 缺失一般可以视为同一个状态,但是0值缺失值由于类型安全的语言特性使得不得不填充0值。这对于一些默认状态为0值的服务来说可能造成判断失效。举个栗子,在Go中一般定义一个字段为int, 当进行一个http body 解析后,如果原始json中不存在 该字段,该字段也会被解析成 0 值。但是由于传统习惯以code=0 作为成功的响应,所以很容易造成对错误的误判导致出现bug. 一个较为通常的做法是,将 字段定义为*int 类型,这样在字段值不存在时将会被填充为 nil,区分了0值与缺失状态。

2. JSON String Number 问题

在JavaScript中,Number类型的最大值是 2 的 53 次方,超过这个数字会造成精度丢失,一般的做法是转为String。这时只需要将 字段对应的tag标记为支持从String转换为int64.示例如下:

type Foo struct {
	A int64 `json:"a,string"`
}

func main(){
	rawString := `{"a": "1"}`
	var foo Foo
	if err := json.Unmarshal([]byte(rawString), &foo); err != nil {
		panic(err)
	}
	fmt.Printf("int: %+v\n", foo)
}

3. JSON int类型 断言问题

如果你得到了这样一个JSON: {"foo": 1} 但是你不想写个Struct来专门解析,你想用转成 map[string]interface{}, 然后将foo的值断言为 int值,这时候容易错的是,json.Unmarshal 库会将number 值默认转为的是 float64 类型, 从而造成断言失败。

4. map 扩缩容问题

我们知道map初始化时可以通过make函数来预先开辟内存空间,并在map中新增key时会进行自动的扩容.但是,截止目前为止(go 1.16),go map 中开辟的bucket占用的空间不会自动缩容,也不会被GC,这甚至可能引起OOM。 详细可以参考 #20135 。以下这段代码将打印各个阶段占用的内存:

func main() {
	v := struct{}{}
	a := make(map[int]struct{})
	for i := 0; i < 100000; i++ {
		a[i] = v
	}
	runtime.GC()
	printMemStats("After Map Add 1000000")
	for i := 0; i < 100000-1; i++ {
		delete(a, i)
	}
	runtime.GC()
	printMemStats("After Map Delete 99999")
	for i := 0; i < 100000-1; i++ {
		a[i] = v
	}
	runtime.GC()
	printMemStats("After Map Add 99999 again")
	fmt.Printf("%d\n", len(a))
	a = nil
	runtime.GC()
	printMemStats("After Map Set nil")
}

func printMemStats(mag string) {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("%v:memory = %vKB, GC Times = %v\n", mag, m.Alloc/1024, m.NumGC)
}
After Map Add 1000000:memory = 1625KB, GC Times = 1
After Map Delete 99999:memory = 1628KB, GC Times = 2
After Map Add 99999 again:memory = 1628KB, GC Times = 3
100000
After Map Set nil:memory = 117KB, GC Times = 4

可以看到的是,删除并且GC后内存依然占用很高。这种情况在一些生命周期很长的Map使用过程中应该特别注意。对此,官方的推荐是,new一个新的。

5. nil 和 interface nil 空值判断问题

定义某个类型的空值的变量,将这个变量赋值给一个interface,你会神奇的发现: 空值判断失效了!这个问题出自: golang-nuts

type someType struct{ f1 string }

func main() {
	var v *someType
	var v2 interface{}
	v2 = v
	fmt.Printf("v2 == nil: %t\n", v2 == nil)
	fmt.Printf("v2 reflected val is nil: %t\n", reflect.ValueOf( v2 ).IsNil() )
}

对这个问题的解释是interface类型的实现是type和value,v2 == nil 判断的是 v2是否为空值,而这里interface并不是空值,而是该interface hold 了一个类型为 *someType 的 nil值,所以造成了判断失效。所以对interface的判断要特别注意,也要注意尽量不要将某类型的nil值传入一个interface中。

6. 默认时区问题

如果 一个 字符串中没有时区信息,默认转为UTC时区, 而time.Now 默认为 当前系统时区。如果你期望的是当前时区, 可能造成时间错误。

func main(){
	format := "2006-01-02 15:04"
	t, _ := time.Parse(format, "2021-05-10 14:00")
	fmt.Println(t)
	fmt.Println("现在是: ", time.Now())
}

输出结果:

2021-05-10 14:00:00 +0000 UTC
现在是:  2021-05-10 14:41:01.241168 +0800 CST m=+0.000088626

可以看到的是,指定的没有时区的字符串被解析成了 UTC时区,但是 now 生成的时间是 系统时区。需要注意对没有时区的字符串时间提高警惕。

总结

这是笔者日常的一些经验总结,如果有错误的地方欢迎大家指正。祝大家都能写出bug free的代码 !