|
作者 百度小程序团队introduction本文收集一些使用Go开发过程中非常容易踩坑的case,所有的case都有具体的代码示例,以及针对的代码修复方法,以避免大家再次踩坑。通常这些坑的特点就是代码正常能编译,但运行结果不及预期或是引入内存漏洞的风险。全文7866字,预计阅读时间20分钟。GEEK TALK01参数传递误用1.1 误对指针计算Sizeof对任何指针进行unsafe.Sizeof计算,返回的结果都是 8 (64位平台下)。稍不注意就会引发错误。错误示例:func TestSizeofPtrBug(t *testing.T) {????type?CodeLocation?struct?{ LineNo int64 ColNo int64 } cl := &CodeLocation{10, 20} size := unsafe.Sizeof(cl) fmt.Println(size) // always return 8 for point size}建议使用示例:单独编写一个只处理值大小的函数 ValueSizeof。func TestSizeofPtrWithoutBug(t *testing.T) { type CodeLocation struct { LineNo int64 ColNo int64 } cl := &CodeLocation{10, 20} size := ValueSizeof(cl) fmt.Println(size) // 16}func ValueSizeof(v any) uintptr { typ := reflect.TypeOf(v) if typ.Kind() == reflect.Pointer { return typ.Elem().Size() } return typ.Size()}1.2?可变参数为any类型时,误传切片对象当参数的可变参数是any类型时,传入切片对象时一定要用展开方式。 appendAnyF := func(t []any, toAppend ...any) []any { ret := append(t, toAppend...) return ret } emptySlice := []any{} slice2 := []any{"hello", "world"} // bug append slice as a element emptySlice = appendAnyF(emptySlice, slice2) fmt.Println(emptySlice) // only 1 element [[hello world]] emptySlice = []any{} emptySlice = appendAnyF(emptySlice, slice2...) fmt.Println(emptySlice) // [hello world]1.3?数组是值传递数组在函数或方法中入参传递是值复制的方式,不能用入参的方式进函数或方法内修改数组内容进行返回的。示例代码如下: arr := [3]int{0, 1, 2} f := func(v [3]int) { v[0] = 100 } f(arr) // no modify to arr fmt.Println(arr) // [0 1 2]1.4?切片扩容后会新申请内存,不再与内存引用有任何关联这里坑在,如果从一个数组中引入一个切片,一旦这个切片引发扩容后,则与原来的引用内容没有任何关系。 arr := []int{0, 1, 2} f := func(v []int) { v[0] = 100// can modify origin array v = append(v, 4) // new memory allocated v[0] = 50// no modify to origin array } f(arr) fmt.Println(arr) // [100 1 2]上面的示例代码,扩容切片前对内容的修改可以影响到arr数组,说明是共享内存地址引用的,一旦扩容后,则是重新申请了内存,与数组不再是一个内存引用了。1.5?返回参数尽量避免使用共享数据的切片对象,容易导致原始数据污染这种场景就是如果通过函数返回值方式从一个大数组获取部分内部,尽量不要用切片共享的方式,可以使用copy的方式来替换。下面的代码,通过ReadUnsafe读取切片后,修改内容同步影响原始的内容。type Queue struct { content []byte pos int}func (q *Queue) ReadUnsafe(size int) []byte { if q.pos+size >= len(q.content) { return nil } pos := q.pos q.pos = q.pos + size return q.content[pos:q.pos]}func TestReadUnsafe(t *testing.T) { c := [200]byte{} q := &Queue{content: c[:]} v := q.ReadUnsafe(10) v[0] = 1 fmt.Println(q.content[0]) // 1 q.content值已经被修改}正确的修改如下,使用copy创建一份新内存:func (q *Queue) ReadSafe(size int) []byte { if q.pos+size >= len(q.content) { return nil } pos := q.pos q.pos = q.pos + size ret := make([]byte, size) copy(ret, q.content[pos:q.pos]) return ret}func TestReadSafe(t *testing.T) { c := [200]byte{} q := &Queue{content: c[:]} v := q.ReadSafe(10) v[0] = 1 fmt.Println(q.content[0]) // 0 q.content值安全}GEEK TALK02指针相关使用的坑2.1?误保存uintptr值uintptr保存的当前地址的一个整型值,它一旦被获取后,是不会被编译器感知的,也就是它就是一个普通变量,不会追溯内存真实地址变化。 slice := []int{0, 1, 2} ptr := unsafe.Pointer(&slice[0]) // get array element:0 pointer slice = append(slice, 3) // allocate new memory ptr2 := unsafe.Pointer(&slice[0]) // ptr is 824633770392, ptr2 is 824633762896, ptr==ptr2 result is false????fmt.Println(fmt.Sprintf("ptr?is?%d,?ptr2?is?%d,?ptr==ptr2?result?is?%v",?ptr,?ptr2,?ptr?==?ptr2))2.2?len与cap 对空指针nil与空值返回相同针对切片, 用len与cap操作时,空值与nil都是返回0, 针对map, 用len操作时,空值与nil都是返回0。 var slice []int = nil fmt.Println(len(slice), cap(slice)) // 0 0 var slice2 []int = []int{} fmt.Println(len(slice2), cap(slice2)) // 0 0 var mp map[int]int = nil fmt.Println(len(mp)) // 0 var mp2 map[int]int = map[int]int{} fmt.Println(len(mp2)) // 02.3?用new对map类型进行初始化用new对map进行创建,编译器不会报错,但是无法对map进行赋值操作的。正确应使用make进行内存分配。 mp := new(map[int]int) f := func(m map[int]int) { m[10] = 10 } f(*mp) // assignment to entry in nil map2.4?空指针和空接口不等价对于接口类型是可以用nil赋值的,但如果对于接口指针类型,其值对应的并不一个空接口。Go语言编译器似乎在这个处理,会特殊处理。// MyErr just for demotype MyErr struct{}func (e *MyErr) Error() string { return""}func TestInterfacePointBug(t *testing.T) { var e *MyErr = nil var e2 error = e // e2 will never be nil. fmt.Println(e2 == nil)}GEEK TALK03函数,方法与控制流相关3.1 循环中使用闭包错误引用同一个变量原因分析:闭包捕获外部变量,它不关心这些捕获的变量或常量是否超出作用域,只要闭包在使用,这些变量就会一直存在。 type S struct { A string B string C string } typ := reflect.TypeOf(S{}) funcArr := make([]func() string, typ.NumField()) for i := 0; i
|
|