1、可变参数是空接口类型
func main() {
var a = []interface{}{1, 2, 3}
fmt.Println(a)
fmt.Println(a...)
}
不管是否展开,编译器都无法发现错误,但是输出是不同的:
[1 2 3]
1 2 3
2、数组是值传递
在函数调用参数中,数组是值传递,无法通过修改数组类型的参数返回结果,因此必要时使用切片
3、切片的函数传参
Go语言中,如果以切片为参数调用函数时,有时候会给人一种参数采用了传引用的方式的假象:因为在被调用函数内部可以修改传入的切片的元素。其实,任何可以通过函数参数修改调用参数的情形,都是因为函数参数中显式或隐式传入了指针参数。函数参数传值的规范更准确说是只针对数据结构中固定的部分传值,例如字符串或切片对应结构体中的指针和字符串长度结构体传值,但是并不包含指针间接指 向的内容。将切片类型的参数替换为类似 reflect.SliceHeader 结构体就很好理解切片传值的含义了:
func twice(x []int) {
for i := range x {
x[i] *= 2
}
}
type IntSliceHeader struct {
Data []int
Len int
Cap int
}
func twice(x IntSliceHeader) {
for i := 0; i < x.Len; i++ {
x.Data[i] *= 2
}
}
如果只是修改切片中的元素值,那么函数参数直接传切片即可,但如果要修改切片的长度,要增加或者减少切片中的成员,则需要传入切片的指针或者通过返回值返回修改后的切片
func modifySlice(array *[]int, elem int) {
*array = append(*array, elem)
}
func modifySlice(array []int, elem int) []int {
array = append(array, elem)
return array
}
因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。除了数据之外,切片结构还包含了切片长度和切片容量信息,这2个信息也是传值的。如果被调用函数中修改了 Len 或 Cap 信息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切片。这也是为何内置的 append 必须要返回一个切片的原因。
4、map遍历顺序不固定
map是一种hash表的实现,每次遍历的顺序可能都不一样
5、返回值被屏蔽
在局部作用域中,命名的返回值会被同名的局部变量屏蔽
func Foo() (err error) {
if err := Bar(); err != nil {
return
}
return
}
6、recover必须在defer函数中运行
recover捕获的是祖父级调用时的异常,直接调用时无效:
func main() {
recover()
panic(1)
}
直接defer调用也是无效的:
func main() {
defer recover()
panic(1)
}
defer调用时多层嵌套依然无效:
func main() {
defer func() {
func() { recover() }()
}()
panic(1)
}
7、main函数提前退出
后台goroutine无法保证完成任务
func main() {
go println("hello")
}
8、通过sleep来回避并发中的问题
休眠并不能保证输出完整的字符串
func main() {
go println("hello")
time.Sleep(time.Second)
}
9、独占CPU导致其他goroutine饿死
goroutine是协作式抢占调度,不会主动放弃CPU
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}()
for {} // 占用CPU
}
解决的方法是在for循环中加入runtime.Gosched()调度函数
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}()
for {
runtime.Gosched()
} // 占用CPU
}
或者是通过阻塞方式避免cpu占用
func main() {
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
os.Exit(0)
}()
select{}
}
10、不同Goroutine之间不满足顺序一致性内存模型
因为在不同的Goroutine中,main函数无法保证能打印出hello,world:
var msg string
var done bool
func setup() {
msg = "hello, world"
done = true
}
func main() {
go setup()
for !done {
}
println(msg)
}
解决的办法是显示同步:
var msg string
var done = make(chan bool)
func setup() {
msg = "hello, world"
done <- true
}
func main() {
go setup()
<-done
println(msg)
}
msg的写入是在channel发送之前,所以能保证打印hello,world
11、闭包错误引用同一个变量
这个很容易踩中
func main() {
for i := 0; i < 5; i++ {
defer func() {
println(i)
}()
}
}
改进的方法是在每次迭代中生成一个局部变量
func main() {
for i := 0; i < 5; i++ {
tmp := i
defer func() {
println(tmp)
}() //或者通过函数参数传入
}
}
12、在循环内部执行defer语句
defer在函数退出时才能执行,在for执行defer会导致资源延迟释放:
func main() {
for i := 0; i < 5; i++ {
f, err := os.Open("/path/to/file")
if err != nil {
log.Fatal(err)
}
defer f.Close()
}
}
解决的方法可以在for中构造一个局部函数,在局部函数内部执行defer:
func main() {
for i := 0; i < 5; i++ {
func(){
f, err := os.Open("/path/to/file")
if err != nil {
log.Fatal(err)
}
defer f.Close()
}
}
}
13、切片会导致整个底层数组被锁定
切片会导致整个底层数组被锁定,底层数组无法释放内存。如果底层数组较大会对 内存产生很大的压力。
func main() {
headerMap := make(map[string][]byte)
for i := 0; i < 5; i++ {
name := "/path/to/file"
data, err := ioutil.ReadFile(name)
if err != nil {
log.Fatal(err)
}
headerMap[name] = data[:1]
}
// do some thing
}
解决的方法是将结果克隆一份,这样可以释放底层的数组:
func main() {
headerMap := make(map[string][]byte)
for i := 0; i < 5; i++ {
name := "/path/to/file"
data, err := ioutil.ReadFile(name)
if err != nil {
log.Fatal(err)
}
headerMap[name] = append([]byte{}, data[:1]...)
}
// do some thing
}
14、空指针和空接口不等价
比如返回了一个错误指针,但是并不是空的error接口
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
}
15、内存地址会变化
Go语言中对象的地址可能发生变化,因此指针不能从其它非指针类型的值生成:
func main() {
var x int = 42
var p uintptr = uintptr(unsafe.Pointer(&x))
runtime.GC()
var px *int = (*int)(unsafe.Pointer(p))
println(*px)
}
当内存发送变化的时候,相关的指针会同步更新,但是非指针类型的uintptr不会做 同步更新。
同理CGO中也不能保存Go对象地址。
16、Goroutine泄露
Go语言自带垃圾回收,因此内存一般不会泄露。但是Goroutine却存在泄露的情况,导致Goroutine引用的内存同样无法被回收
func main() {
ch := func() <-chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ {
ch <- i
}
} ()
return ch
}()
for v := range ch {
fmt.Println(v)
if v == 5 {
break
}
}
}
上面的程序中后台Goroutine向管道输入自然数序列,main函数中输出序列。但是 当break跳出for循环的时候,后台Goroutine就处于无法被回收的状态了。 我们可以通过context包来避免这个问题:
func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := func(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ {
select {
case <-ctx.Done():
return
case ch <- i:
}
}
}()
return ch
}(ctx)
for v := range ch {
fmt.Println(v)
if v == 5 {
cancel()
break
}
}
}
当main函数在break跳出循环时,通过调用 cancel() 来通知后台Goroutine退出,这样就避免了Goroutine的泄漏
本文详细介绍了Go语言中的一些常见陷阱,包括可变参数、数组与切片的传递方式、map遍历顺序的不确定性、返回值屏蔽、recover的使用场景、main函数提前退出、并发问题、闭包引用、循环内defer、切片与内存管理、空指针与空接口的区别、内存地址变化、Goroutine泄露以及内存泄漏。通过这些陷阱,帮助开发者更好地理解和避免在Go编程中遇到的问题。
659

被折叠的 条评论
为什么被折叠?



