您的当前位置:首页正文

Go踩坑记录分享

2024-11-25 来源:个人技术集锦

废话不多说直接上使用错误使用示例

示例1

有时我们为了避免切片底层数据扩展带来的开销,会提前指定make的cap大小

func Example1() {
	persons := make([]*Person, 1000)
	for i := 0; i < 10000; i++ {
		persons = append(persons, &Person{
			Name: "example1",
			Age:  i,
		})
	}
}

// 正确方法:声明lens为0,就可以使用append将item逐一加到切片中
type Person struct {
	Name string
	Age int
}

func Example1() {
	persons := make([]*Person, 0, 1000)

	for i := 0; i < 1000; i++ {
		persons = append(persons, &Person{
			Name: "example1",
			Age: i,
		})
	}
}

// 正确方法2:使用切片逐个赋值的方式
type Person struct {
	Name string
	Age int
}

func Example1() {
	persons := make([]*Person, 1000)

	for i := 0; i < 1000; i++ {
		persons[i] =  &Person{
			Name: "example1",
			Age: i,
		}
	}
}

错误分析

虽然我们指定了make的cap大小,但是我们并没有指定make的length字段的大小,这个时候length字段的大小等于cap的大小,参照下面切片用法

补充:切片正确初始化、创建方法

使用 Golang 内置的 make() 函数创建切片,此时需要传入一个参数来指定切片的长度:

// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := make([]int, 5)

此时只指定了切片的长度,那么切片的容量和长度相等。也可以分别指定长度和容量:

// 创建一个整型切片
// 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)

分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能
访问所有的数组元素。 

示例2

1. 向nil map中添加元素导致panic

func Example2() {
	var m1 map[string]string

	m1["a"] = "b"
  …
}

// 解决方案:给map初始化

2. 函数返回值为map时,在使用前也应该初始化,否则会panic

func Example3() (m1 map[string]string) {
	m1["a"] = "b"
	return m1
}
// 解决方案:给map初始化

3. map有嵌套,嵌套的map也要初始化,否则会panic

func Example2() {
	m1 := make(map[string]map[int]int)
	m1["go"][0] = 0
}

// 正确方法
// 先初始化,再使用
func Example6() {
	m1 := make(map[string]map[int]int)
	m1["go"] = make(map[int]int)
	m1["go"][0] = 0
}

示例3

对于带for循环的select,break并不能打破for循环,会永远循环下去

func Example3(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			break
		default:
			...
		}
	}
}

正确方法1:采用break + 标签
func Example3(ctx context.Context) {
	//LOOP 是标签
	LOOP:
	for {
		select {
		case <-ctx.Done():
			break LOOP
		default:
			...
		}
	}
}

正确方法2:采用goto + 标签
func Example3(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			goto LOOP
		default:
			...
		}
	}
  //END 是标签
	END:
}

示例4

当panic发生后,虽然有recover,但是并没有将error信息传递到上层,可能会导致后续逻辑异常

func Example4(ctx context.Context) {
	err := DoExample4(ctx)
	if err != nil {
		return
	}
	...
}

func DoExample4() error {
	defer func(ctx context.Context) {
		if err := recover(); err != nil {
			log.ErrorContextf(ctx, "panic: %v", err)
		}
	}()
	
	panic("DoExample4")
}

// 正确做法:及时处理返回异常信息
func Example4(ctx context.Context) {
	err := DoExample4(ctx)
	if err != nil {
		return
	}
	...
}

func DoExample4() (err error) {
	defer func() {
		if panicErr := recover(); panicErr != nil {
			err = fmt.Errorf("panic %v", panicErr)
		}
	}()

	panic("DoExample4")
}


示例5

Go协程启动的函数,没有recover,painc可能导致整个程序panic

func Example5() {
	go func() {
		doSomething()
	}()
}

func doSomething() {
	println("this is a good day!")
	panic("but it panic")
}

// 正确方法:Go协程启动的函数,一定要有recover方法
func Example5() {
	go func() {
		defer func() {
			// 捕获错误并做相应的处理
			if err := recover(); err != nil {
				println("the program is err = %s", err)
			}
		}()
		doSomething()
	}()
}

func doSomething() {
	println("this is a good day!")
	panic("but it panic")
}

示例6

Go协程中,采用闭包的形式使用请求传入的context,可能会导致context canceled错误

func Example6(ctx context.Context) {
	go func() {
		defer func() {
			if err := recover(); err != nil {
				...
			}
		}()

		doSomething(ctx)
	}()
}

func doSomething(ctx context.Context) {
	...
}


// 解决方法:我们可以克隆一个context,例如使用trpc包中提供多个CloneContext方法
func Example6(ctx context.Context) {
	go func(ctx context.Context) {
		defer func() {
			if err := recover(); err != nil {
				...
			}
		}()

		doSomething(ctx)
	}(trpc.CloneContext(ctx))
}

func doSomething(ctx context.Context) {
	...
}

示例7

使用gorm查询时,用Take, First, Last、Find函数查询数据库时,当传入函数的参结果集的变量只能为Struct类型或Slice类型,当传入变量为Struc类型时,如果检索出来的数据为0条,会抛出ErrRecordNotFound错误,当传入变量为Slice类型时,任何条件下均不会抛出ErrRecordNotFound错误

具体分析:

// 查不到数据返回ErrRecordNotFound
func Example7(db *gorm.DB) error {
	person := model.Person{}
	return db.Find(&person, -1).Error
}


// 查不到数据返回nil
func Example7(db *gorm.DB) error {
	person := []model.Person{}
	return db.Find(&person, -1).Error
}

示例8

当gorm更新使用struct时,会有零值更新问题,就是对于零值字段,会忽略更新

type Person struct {
	Name string
	Age int
}

// 更新数据库时忽略结构体中为零值的字段
db.Model(&person).Updates(Person{Name: "test", Age: 0})


// 解决方法:使用map可以不忽略零值字段
// 当然也可以在定义数据时避免使用零值字段
type Person struct {
	Name string
	Age int
}


db.Model(&person).Updates(map[string]interface{}{“Name": "test", "Age": 0})

示例9

闭包使用for…range声明的变量,导致非预期结果

func Example9() {
	s := []string{"a", "b", "c"}
	for _, v := range s {
		go func() {
			fmt.Println(v)
		}()
	}
	select {}
}

// 正确方法1:通过复制变量,避免地址操作带来的影响
func Example9() {
	s := []string{"a", "b", "c"}
	for _, v := range s {
		v1 := v
		go func() {
			fmt.Println(v1)
		}()
	}
	select {}
}


// 正确方法2:通过函数传参深拷贝来解决,地址操作带来的影响
func Example9() {
	s := []string{"a", "b", "c"}
	for _, v := range s {
		v1 := v
		go func() {
			fmt.Println(v1)
		}()
	}
	select {}
}

示例10

map并发读写,会直接panic掉

func Example10() {
   m := make(map[int]int)
   go func() {
      for i := 0; i < 10000; i++ {
         m[i] = i
      }
   }()
   go func() {
      for i := 0; i < 10000; i++ {
         fmt.Println(m[i])
      }
   }()
   time.Sleep(time.Second * 5)   // fatal error: concurrent map read and map write
}


解决方法:
加锁
sync.Map
改成协程内局部变量

示例11

slice并发写不会出现panic,能正常跑,但数据通常不是预期结果

func Example11() {
   var arr []int
   for i := 0; i < 1000; i++ {
      go func(v int) {
         arr = append(arr, v)
      }(i)
   }
   time.Sleep(time.Second)
   for i, v := range arr {
      fmt.Println(i, ": ", v) // 数量,非预期的1000个
   }
}


解决方法:加锁控制并发写入的并发覆盖问题
func Example11() {
	lock := sync.Mutex{}
	var arr []int
	for i := 0; i < 1000; i++ {
		go func(v int) {
			lock.Lock()
			arr = append(arr, v)
			lock.Unlock()
		}(i)
	}
	time.Sleep(time.Second)
	for i, v := range arr {
		fmt.Println(i, ": ", v) // 数量预1000个
	}
}

显示全文