Go中有一些容易让人掉进去的陷阱,本篇列出一些常见的,教你如何避免。
Nil切片与空切片
var a []int //nil切片
b := []int{} //空切片
两者都没有元素,但行为上不同,例如,当检查长度或追加元素时:
fmt.Println(len(a), cap(a)) //0 0
fmt.Println(len(b), cap(b)) //0 0
但是当某些操作时,比如JSON的序列化,两者不一样:
dataA, _ := json.Marshal(a)
dataB, _ := json.Marshal(b)
fmt.Println(string(dataA)) // null
fmt.Println(string(dataB)) // []
注意:先明确需求,若表示“不存在的切片”,使用 nil ;若需要一个空的集合,则使用空切片。
不初始化的Map
在Go中,map必须使用 make 函数初始化,否则将是 nil 。
var m map[string]int
m["key"] = 42 // panic: assignment to entry in nil map
注意:始终要用 make
初始化map。
m := make(map[string]int)
m["key"] = 42
并发Map读写
Go的map在并发读写时是不安全的。
m := make(map[string]int)
go func() {
for {
m["key"] = 42
}
}()
go func() {
for {
_ = m["key"]
}
}()
上述代码会产生panic,需要使用 sync.RWMutex
进行锁定或考虑使用 sync.Map
。
闭包中的循环变量
package main
import "fmt"
import "sync"
func main() {
wg := sync.WaitGroup{}
for _, v := range []int{1, 2, 3, 4} {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v)
}()
}
wg.Wait()
fmt.Println("main done")
}
可能希望输出 1,2,3,4,但实际上 v 的值在每个 goroutine 中都可能是 4 。
解决方式:
package main
import "fmt"
import "sync"
func main() {
wg := sync.WaitGroup{}
for _, v := range []int{1, 2, 3, 4} {
wg.Add(1)
go func(v int) {
defer wg.Done()
fmt.Println(v)
}(v)
}
wg.Wait()
fmt.Println("main done")
}
将循环变量作为参数传递给闭包。
Error不被检查
Go没有其他语言那样的异常机制,它使用 error 来传递错误。
f, _ := os.Open("filename.ext")
上述代码,错误被忽略,可能导致后续操作在文件上失败。
解决方法:
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
始终检查错误并处理。
不正确的String转换
直接将 byte 切片转换为 string 会导致不可预期的结果,特别是当 byte 切片中包含非 UTF-8 字符时。
解决方式: 使用正确的转换方法,如 utf8.Valid 检查是否为有效的 UTF-8。
失效的Goroutine
若主goroutine结束,其他goroutine也会立即停止。
package main
import (
"fmt"
"time"
)
func main() {
go func() {
time.Sleep(2 * time.Second)
fmt.Println("This will never get printed")
}()
fmt.Println("main done")
}
输出:
main done
解决方法:使用 sync.WaitGroup
或其他同步机制来确保所有goroutine完成。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(2 * time.Second)
fmt.Println("This will never get printed")
}()
wg.Wait()
fmt.Println("main done")
}
输出:
This will never get printed
main done
变量阴影
可能会不小心创建了一个同名的局部变量,从而“阴影”了外部的变量。
x := 5
func f() {
x, err := doSomething() // 这里 unintentionally 阴影了外部的 x
if err != nil {
log.Fatal(err)
}
fmt.Println(x) // 这可能不是你期望的 x 的值
}
注意:始终注意变量的作用域,使用 go vet 来识别此类问题。
使用 == 比较复杂的数组和结构
Go允许使用 == 来比较数组和结构,但若它们本身包含不可比较的字段(如map或函数),则会导致编译错误。
package main
import "fmt"
type Person struct {
Name string
Ages map[string]int
}
func main() {
p1 := Person{Name: "Alice", Ages: map[string]int{"Bob": 30}}
p2 := Person{Name: "Alice", Ages: map[string]int{"Bob": 30}}
fmt.Println(p1 == p2) // 编译错误
}
解决:为复杂的数据结构实现自己的比较函数或者使用第三方库。
package main
import "fmt"
import "reflect"
type Person struct {
Name string
Ages map[string]int
}
func main() {
p1 := Person{Name: "Alice", Ages: map[string]int{"Bob": 30}}
p2 := Person{Name: "Alice", Ages: map[string]int{"Bob": 30}}
fmt.Println(reflect.DeepEqual(p1,p2)) // 正确比较
}
非显式的接口实现
Go 使用非显式的接口实现,这意味着只要类型实现了接口的所有方法,它就被认为实现了该接口。这可能导致意外实现了某个接口。
解决方法:虽然这是 Go 的特性,但是在可能的情况下,为类型提供明确的文档,说明它实现了哪些接口。
小心defer使用
虽然 defer 是处理资源清理的好方法,但使用它时仍然需要注意。
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
}
可能期望它在循环的每次迭代后立即输出,但实际上它会在函数返回时输出,并且顺序是反的。
解决: 理解 defer 的行为和用途,并仔细考虑何时使用它。
容易被忽视的零值
Go 的每种类型都有一个零值,例如,数值类型为 0,bool 类型为 false,string 为 “"。但这有时可能导致意外的行为,尤其是当结构体有复杂的嵌套时。
解决方法:在初始化结构时,明确指定所有期望的值,避免依赖零值。
指针和零值
指针的零值是 nil ,而不是指向零初始化的值。
var p *int
fmt.Println(*p) // 这会引发 panic,因为 p 是 nil,没有值可以解引用
解决: 始终确保指针已初始化并指向有效的内存地址。
在切片上的原位修改
切片其实是底层数组的一个视图。在一个切片上的修改可能影响其他引用相同底层数组的切片。
package main
import "fmt"
func main() {
original := []int{1, 2, 3, 4, 5}
subset := original[1:4]
subset[0] = 9
fmt.Println(original) // 输出:[1 9 3 4 5]
}
注意:如果不想修改原始数据,要使用 copy 创建一个新的切片副本。
错误地使用接口类型
Go 中的接口是静态类型的,但它们可以持有任何值。很多新手尝试将接口值直接转换为另一种类型,而不是使用类型断言。
func MyFunc(a interface{}) {
str := a.(string)
fmt.Println(str)
}
调用 MyFunc(123) 会引发panic。
解决:在使用类型断言前,要检查是否成功。
package main
import "fmt"
func main() {
MyFunc("123")
MyFunc(123)
}
func MyFunc(a interface{}) {
str := a.(string)
fmt.Println(str)
}
忽略Goroutine泄漏
Goroutines 是轻量级的,但它们并非完全没有成本。未正确终止的 Goroutines 会导致内存泄漏。
func leakyFunc() {
for {
go func() {
time.Sleep(time.Hour)
}()
}
}
解决: 确保为 Goroutines 设计合适的退出策略,例如使用 context 或 select 和 channel 机制。
没有初始化的结构体字段
在 Go 中,如果结构体的字段没有显式初始化,它们将被赋予零值。这可能导致意外的行为,尤其是当你期望它们有一个非零的默认值时。
type Config struct {
Timeout time.Duration
}
c := Config{}
fmt.Println(c.Timeout) // 输出:0s,可能不是你期望的默认超时值
解决: 创建返回正确初始化的结构体的工厂函数或构造函数。
忽略channel的返回值
当从已关闭的 channel 读取时,你将接收该类型的零值,而不会得到任何明确的“channel 已关闭”的错误。
ch := make(chan int)
close(ch)
val := <-ch
fmt.Println(val) // 输出:0,可能误导你认为 channel 传递了一个 0
解决: 使用两个返回值的形式来检查 channel 是否已关闭:
val, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
}
package main
import "fmt"
func main() {
ch := make(chan int)
close(ch)
val := <-ch
fmt.Println(val) // 输出:0,可能误导你认为 channel 传递了一个 0
ch1 := make(chan int)
close(ch1)
val1, ok := <-ch
if !ok {
fmt.Println("Channel is closed") // 检查 channel 是否已关闭
} else {
fmt.Println(val1) // 输出正确的值
}
}
非法的字符串操作
Go 的字符串是不可变的,尝试修改字符串会导致编译错误。
str := "hello"
str[0] = 'H' // 编译错误
解决: 使用切片和 string 转换函数来实现字符串的变化。
range循环的陷阱
在 for … range 循环中,范围变量的值在每次迭代中都会被重用,这可能会导致意外的闭包行为。
package main
import "fmt"
func main() {
funcs := []func(){}
for _, val := range []int{1, 2, 3} {
funcs = append(funcs, func() {
fmt.Println(val)
})
}
for _, f := range funcs {
f() // 会三次打印 3,而不是 1, 2, 3
}
}
正确的写法:
package main
import "fmt"
func main() {
funcs := []func(){}
for _, val := range []int{1, 2, 3} {
v := val
funcs = append(funcs, func() {
fmt.Println(v)
})
}
for _, f := range funcs {
f() // 会三次打印 3,而不是 1, 2, 3
}
}
解决: 在循环体内部为范围变量创建一个新的局部变量。
对nil切片和nil map的误解
虽然 nil 切片和 nil map 都表示“没有元素”的集合,但它们在使用时的行为是不同的。
var s []int
var m map[string]int
s = append(s, 1) // 正确的,你可以 append 到 nil 切片
m["key"] = 1 // 运行时 panic,不能直接赋值给 nil map
解决:在使用 map 之前,确保使用 make 初始化它。
小心map并发访问
Go的map不是并发安全的,多个goroutine同时读写map时可能导致运行时错误。
package main
import "fmt"
import "sync"
func main() {
m := make(map[string]int)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
m["key"] = 1
fmt.Printf("1: %v \n",m)
}()
wg.Add(1)
go func() {
defer wg.Done()
m["key"] = 2
fmt.Printf("2: %v \n",m)
}()
wg.Wait()
fmt.Println(m)
}
输出:
2: map[key:2]
1: map[key:1]
map[key:1]
解决:使用互斥锁(如:sync.Mutex)来同步对map的访问,或考虑使用并发安全的数据结构,如 sync.Map 。
方法和指针接收者
当定义方法时,区分值接收者和指针接收者是很重要的,特别是当你想修改接收者中的值时。
package main
import "fmt"
type Counter struct {
count int
}
func (c Counter) Add() {
c.count++
}
func (c *Counter) AddPtr() {
c.count++
}
func main() {
counter := Counter{}
counter.Add()
fmt.Println(counter.count) // 输出 0,因为 Add() 没有改变原值
counter.AddPtr()
fmt.Println(counter.count) // 输出 1,因为 AddPtr() 修改了原值
}