Golang 防掉坑笔记(二)

本文深入探讨了Go语言编程中的常见陷阱,包括指针、接口、类型定义、字符串操作、赋值规则、变量作用域、循环特性、goroutine行为、切片底层机制、常量组特性、方法调用限制、指针值复制、Mutex锁使用、defer函数执行时机等关键概念,旨在帮助开发者避免编程错误,提升代码质量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 永远不要使用一个指针指向一个接口类型,因为它已经是一个指针

 函数参数为 interface{} 时可以接收任何类型的参数,包括用户自定义类型等

type S struct {
}

func fa(x interface{}) {
}

func g(x *interface{}) {
}
func TestFour01(t *testing.T) {
	s := S{}
	p := &s
	fa(s)
	g(s)
	fa(p)
	g(p)
}

2. 基于类型创建的方法必须定义在同一个包内

3. golang 的字符串类型是不能赋值 nil 的,也不能跟 nil 比较

4. 对于类似 X = Y的赋值操作,必须知道 X 的地址,才能够将 Y 的值赋给 X

var m = map[string]Math{
	//"foo": Math{2, 3},

	// fix2.修改数据结构
	//"foo": &Math{2, 3},
}

// go 中的 map 的 value 本身是不可寻址的
// map[key]struct 中 struct 是不可寻址的
func TestFour07(t *testing.T) {
	//m["foo"].x = 4

	// fix1.使用临时变量
	tmp := m["foo"]
	tmp.x = 4
	m["foo"] = tmp
	fmt.Println(m["foo"].x)
}

5.  对于使用:=定义的变量,如果新变量与同名已定义的变量不在同一个作用域中,那么 Go 会新定义这个变量

var p3 *int
func foo() (*int, error) {
	var i int = 5
	return &i, nil
}
func bar() {
	//use p
	fmt.Println(*p3)
}
func TestFour08(t *testing.T) {
	// 函数里的 p3 是新定义的变量,会遮住全局变量 p3,
	// 导致执行到bar()时程序,全局变量 p3 依然还是 nil,程序随即 Crash
	p3, err := foo()
	if err != nil {
		fmt.Println(err)
		return
	}
	bar()
	fmt.Println(*p3)
}

6. 循环次数在循环开始前就已经确定,循环内改变切片的长度,不影响循环次数

func TestFour09(t *testing.T) {
	v := []int{1, 2, 3}
	for i := range v {
		v = append(v, i)
	}
	println(v)
}

7. goroutine 中输出的 i、v 值都是 for range 循环结束后的 i、v 最终值, 而不是goroutine启动时的i, v值。

// 可以理解为闭包引用,使用的是上下文环境的值。
func TestFour10(t *testing.T) {
	var m = [...]int{1, 2, 3}
	for i, v := range m {
		go func() {
			fmt.Println(i, v)
		}()
	}

	// 	fix1:使用函数传递
	for i, v := range m {
		go func(i, v int) {
			fmt.Println(i, v)
		}(i, v)
	}

	// fix2:使用临时变量保留当前值
	for i, v := range m {
		i := i // 这里的 := 会重新声明变量,而不是重用
		v := v
		go func() {
			fmt.Println(i, v)
		}()
	}

	time.Sleep(time.Second * 1)
}

8. 切片在 go 的内部结构有一个指向底层数组的指针

当 range 表达式发生复制时,副本的指针依旧指向原底层数组,所以对切片的修改都会反应到底层数组上,所以通过 v 可以获得修改后的数组元素

func change1(s ...int) {
	s = append(s, 3)
}

// 第一次调用 change() 时,append() 操作使切片底层数组发生了扩容,原 slice 的底层数组不会改变;
// 第二次调用change() 函数时,使用了操作符[i,j]获得一个新的切片,
// 假定为 slice1,它的底层数组和原切片底层数组是重合的,
// 不过 slice1 的长度、容量分别是 2、5,所以在 change() 函数中对 slice1 底层数组的修改会影响到原切片。
func TestFour13(t *testing.T) {
	slice := make([]int, 5, 5)
	slice[0] = 1
	slice[1] = 2
	change1(slice...)
	fmt.Println(slice)
	change1(slice[0:2]...)
	fmt.Println(slice)

	var a = []int{1, 2, 3, 4, 5}
	var r [5]int

	for i, v := range a {
		if i == 0 {
			a[1] = 12
			a[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r = ", r)
	fmt.Println("a = ", a)
}


9. 常量组中如不指定类型和初始化值,则与上一行非空常量右值相同

func TestFour15(t *testing.T) {
	const (
		x uint16 = 120
		y
		s = "abc"
		z
	)

	fmt.Printf("%T %v\n", y, y)
	fmt.Printf("%T %v\n", z, z)

	const x1 = 123
	const y1 = 1.23
	fmt.Println(x1 + y1)
}

10. 在方法中,指针类型的接收者必须是合法指针(包括 nil),或能获取实例地址;不可寻址的结构体不能调用带结构体指针接收者的方法

type X struct{}
func (x *X) test() {
	println(x)
}

type T struct {
	n int
}
func (t *T) Set(n int) {
	t.n = n
}
func getT() T {
	return T{}
}

// X{} 是不可寻址的,不能直接调用方法。
func TestFour16(t *testing.T) {
	var a *X
	a.test()
	//X{}.test()
	// fix1
	var x = X{}
	x.test()

        //getT().Set(1)
	// fix2
	i := getT()
	i.Set(2)
}

11. 当指针值赋值给变量或者作为函数参数传递时, 会立即计算并复制该方法执行所需的接收者对象,与其绑定,以便在稍后执行时,能隐式第传入接收者参数

当目标方法的接收者是指针类型时,那么被复制的就是指针

type N int
func (n N) test() {
	fmt.Println(n)
}
func TestFour18(t *testing.T) {
	var n N = 10
	p := &n

	n++
	f1 := n.test
	n++
	f2 := p.test
	n++
	fmt.Println(n)

	f1()
	f2()
}

12. 截取符号 [i:j],如果 j 省略,默认是原切片或者数组的长度

// x 的长度是 2,小于起始下标 6
func TestFour20(t *testing.T) {
	x := make([]int, 2, 10)
	_ = x[6:10]
	_ = x[6:] //panic
	_ = x[2:]
}

13. 将 Mutex 作为匿名字段时,相关的方法必须使用指针接收者,否则会导致锁机制失效

type data struct {
	sync.Mutex

	// fix2
	//*sync.Mutex
}

func (d data) test(s string) {
	// fix1
	//func (d *data) test(s string) {
	d.Lock()
	defer d.Unlock()

	for i := 0; i < 5; i++ {
		fmt.Println(s, i)
		time.Sleep(time.Second)
	}
}
func TestFour21(t *testing.T) {
	var wg sync.WaitGroup
	wg.Add(2)
	var d data

	// fix2
	//d:=data{new(sync.Mutex)}
	go func() {
		defer wg.Done()
		d.test("read")
	}()

	go func() {
		defer wg.Done()
		d.test("write")
	}()

	wg.Wait()
}

14. defer() 后面的函数如果带参数,会优先计算参数, 并将结果存储在栈中,到真正执行 defer() 的时候取出

func TestFour24(t *testing.T) {
	f := F(5)
	defer func() {
		fmt.Println("1+==", f())
	}()
	defer fmt.Println("2+==", f())
	i := f()
	fmt.Println("4+==", i)
}

// 在调用 defer() 时,便会计算函数的参数并压入栈中,
// 所以执行a1处代码时,此时便会捕获 panic(2);此后的 panic(1),会被上一层的 recover() 捕获
func TestFour25(t *testing.T) {
        // a1
	defer func() {
		fmt.Print(recover())
	}()
	defer func() {
		defer fmt.Print(recover())
		panic(1)
	}()
	// recover() 必须在 defer() 函数中调用才有效
	defer recover() //无效的
	panic(2)
}

文章部分知识点出自:5 年 Gopher 都不知道的 defer 细节,你别再掉进坑里!Go代码断行规则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值