语法规范类的坑
没有哪一种编程语言是完美和理想的。
短变量声明的坑
1、短变量声明不总是会声明一个新变量
Go 语言是静态编译型语言,因此在 Go 中使用变量之前一定要先声明变量。采用常规变量声明形式和段变量声明形式均可实现对新变量的声明:
var a int = 5
b, c := "hello", 3.1415
println(a, b, c) // 5 hello 3.1415
如果重复声明变量,Go 语言编译器会报错:
var a int = 5
a := 6 // no new variables on left side of :=
但是下面的代码却可以通过编译的检查:
a := 5
a, b := 6, 7
println(a, b) // 6 7
多变量声明语句并未重新声明一个新变量 a,它只是对之前已经声明的变量 a 进行了重新赋值。
2、短变量声明会导致难于发现的变量遮蔽
即便没有使用短变量声明,变量遮蔽也一样可能发生,如下:
var a int = 5
func main() {
println(a) // 5
var a int = 6
println(a) // 6
if a == 6 {
var a int = 7
println(a) // 7
}
}
我们再看一个例子:
func main() {
var err error
defer func() {
if err != nil {
println(err)
}
}()
a, err := someFunc()
if err != nil {
return
}
println(a)
if a == 1 {
b, err := touchError()
if err != nil {
return
}
}
}
func someFunc() (int, error) {
return 1, nil
}
func touchError() (int, error) {
return 2, errors.New("error")
}
在上面的代码中,我们在 main 函数中使用了短变量声明,但是在 if 语句中又使用了短变量声明。这样会导致在 if 语句中声明的变量 err 遮蔽了 main 函数中声明的变量 err,导致在 defer 语句中的 err 变量并不是 main 函数中声明的 err 变量。
所以结果为:
1
而不是预期中的:
1
error
修正这个问题的方法是使用常规变量声明:
func main() {
var err error
defer func() {
if err != nil {
println(err)
}
}()
a, err := someFunc()
if err != nil {
return
}
println(a)
if a == 1 {
var b int
b, err = touchError()
if err != nil {
return
}
}
}
nil 相关的坑
1、不要是所有以 nil 作为零值的类型都是零值可用的
- 以 nil 为零值的类型:根据 Go 语言规范,诸如切片(slice)、映射(map)、通道(channel)、接口(interface)、函数(function)和指针(pointer)的零值都是 nil。
- 零值可用的类型:常见的有 sync.Mutex 和 bytes.Buffer。 Go 原生的切片类型只在
append()
下才可以被划到零值可用的范畴。
var s []int = nil
s = append(s, 1)
println(s) // [1]
var strs []string = nil
strs[0] = "go" // panic: runtime error: index out of range
var m map[string]int = nil
m["one"] = 1 // panic: runtime error: assignment to entry in nil map
var ch chan int = nil
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
type MyInterface interface {
Method()
}
var i MyInterface = nil
i.Method() // panic: runtime error: invalid memory address or nil pointer dereference
2、值为 nil 的接口类型变量并不总等于 nil
我们来看一个例子:
type TxtReader struct {}
func (t *TxtReader) Read(p []byte) (n int, err error) {
// ...
return 0, nil
}
func NewTxtReader(path string) io.Reader {
var r *TxtReader
if strings.Contains(path, ".txt") {
r = new(TxtReader)
}
return r
}
func main() {
i := NewTxtReader("test.txt")
if i == nil {
println("fail to init txt reader")
return
}
println("init txt reader success")
}
在上面的代码中,我们定义了一个 TxtReader 类型,然后定义了一个 NewTxtReader 函数,该函数返回一个 io.Reader 类型的接口变量。在 main 函数中,我们调用 NewTxtReader 函数,然后判断返回的接口变量是否为 nil。但是实际上,返回的接口变量并不是 nil,因此会输出:
init txt reader success
为了便于理解,将上述例子简化为下面的代码:
var r *TxtReader = nil
var i io.Reader = r
println(i == nil) // false
for range 的坑
1、要注意得到的是序号值还是元素值
当使用 for range
针对切片、数组或字符串进行迭代操作时,迭代变量有两个,第一个是元素在迭代集合中的序号值(从 0 开始),第二个值才是元素值。
s := []int{1, 2, 3}
for i, v := range s {
println(i, v)
}
// 0 1
// 1 2
// 2 3
2、针对 string 的类型的 for range
迭代不是逐字节迭代
s := "go语言"
for i, v := range s {
println(i, v)
}
// 0 103
// 1 111
// 2 35821
// 5 35328
输出的结果是字符的 Unicode 编码值。也就是说在 Go 中对字符串运用 for range
操作,每次返回的是一个码点,而不是一个字节。
那么要想进行逐字节迭代,我们需要先将字符串转换为字节类型切片后再使用 for range
对字节类型切片进行迭代。
s := []byte("go语言")
for i, v := range s {
println(i, v)
}
3、对 map 类型内元素的迭代顺序是随机的
m := map[string]int{"one": 1, "two": 2, "three": 3}
for k, v := range m {
println(k, v)
}
将上面的代码运行多次,会发现输出的顺序是随机的。
要想有序迭代 map 内的元素,我们需要额外的数据结构支持,比如使用一个切片来有序保存 map 内元素的 key 值:
m := map[string]int{"one": 1, "two": 2, "three": 3}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
println(k, m[k])
}
4、在副本上进行迭代
func main() {
var s = []int{1, 2, 3}
var r = make([]int, 0)
fmt.Println(s)
for i, v := range s {
if i == 0 {
s = append(s, 4)
}
r = append(r, v)
}
fmt.Println(r)
fmt.Println(s)
}
// 输出
// [1 2 3]
// [1 2 3]
// [1 2 3 4]
我们发现 s 的长度增加了,但是 r 的长度没有增加,说明 for range 是在 s 的副本上进行的迭代。
5、迭代变量是重用的
在 for i, v := range xxx
这条语句中,i、v
都被称为迭代变量。迭代变量总是会参与到每次迭代的处理逻辑中,
func main() {
var a = []int{1, 2, 3, 4}
var wg sync.WaitGroup
for _, v := range a {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(v)
}()
}
wg.Wait()
}
运行结果都会是 4,因为迭代变量 v 是重用的,所以在 goroutine 执行的时候,v 的值已经是 4 了。
改进代码如下:
func main() {
var a = []int{1, 2, 3, 4}
var wg sync.WaitGroup
for _, v := range a {
wg.Add(1)
go func(v int) {
defer wg.Done()
fmt.Println(v)
}(v)
}
wg.Wait()
}
在 Go 1.22 版本之后,这个问题不会存在,官方修改了默认行为。
切片的坑
1、对内存的过多使用
基于已有切片创建的新切片与原切片共享底层存储,这样如果原切片占用较大内存,新切片的存在又使原切片内存无法得到释放,这样就会导致内存的过多使用。
func main() {
var s = make([]int, 1e6)
var r = s[:100]
fmt.Println(len(r), cap(r))
s = nil
fmt.Println(len(r), cap(r))
}
我们可以通过内建函数 copy
为新切片建立独立的存储空间以避免原切片共享底层存储
func main() {
var s = make([]int, 1e6)
var r = make([]int, 100)
copy(r, s[:100])
fmt.Println(len(r), cap(r))
s = nil
fmt.Println(len(r), cap(r))
}
2、隐匿数据的暴露与切片数据募改
我们来看一个例子:
func main() {
var b = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var b1 = b[3:7]
var b2 = b1[:6]
fmt.Println(b, len(b), cap(b))
fmt.Println(b1, len(b1), cap(b1))
fmt.Println(b2, len(b2), cap(b2))
}
// 输出
// [1 2 3 4 5 6 7 8 9 10] 10 10
// [4 5 6 7] 4 7
// [4 5 6 7 8 9] 6 7
我们期望 b2 的输出结果是 [4 5 6 7 0 0]
,但由于 b1 b2 b 三个切片共享底层存储,使得原先切片b 对 b1 的隐匿的数据在切片 b2 中暴露出来。
我们依然可以采用通过内建函数 copy
为新切片建立独立的存储空间以避免原切片共享底层存储。
func main() {
var b = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var b1 = b[3:7]
var b2 = make([]int, 6)
copy(b2, b1)
fmt.Println(b, len(b), cap(b))
fmt.Println(b1, len(b1), cap(b1))
fmt.Println(b2, len(b2), cap(b2))
}
3、新切片与原切片底层存储可能会分家
Go 中的切片支持自动扩容。当然扩容发生时,新切片与原切片底层存储便会出现分家。一旦发生分家,后续对新切片的任何操作都不会影响到原切片:
string 的坑
string 在 Go 中是原生类型,其长度为 string 类型底层数组的字节数量:
func main() {
var s = "go语言"
fmt.Println(len(s)) // 8
}
字符串长度并不等于该字符串中的字符个数:
func main() {
var s = "go语言"
fmt.Println(len(s)) // 8
fmt.Println(utf8.RuneCountInString(s)) // 4
}
sting 类型支持下标操作符[]
,我们可以通过下标操作符读取字符串中的每一字节:
func main() {
s := "hello"
for i := 0; i < len(s); i++ {
fmt.Printf("%c\n", s[i])
}
}
还有 string 类型是不可改变的,我们不能通过下标操作符对字符串进行修改:
func main() {
s := "hello"
s[0] = 'H' // cannot assign to s[0]
}
可以通过将 string 转换为 切片再修改的方案:
func main() {
s := "hello"
b := []byte(s)
b[0] = 'H'
s = string(b)
fmt.Println(s) // Hello
}
string 类型是零值是 ""
,而不是 nil
,因此判断一个字符串是否为空应该使用 len(s) == 0
或 s == ""
。
func main() {
var s string
fmt.Println(s == "") // true
fmt.Println(len(s) == 0) // true
fmt.Println(s == nil) // invalid operation: s == nil (mismatched types string and nil)
}
switch 的坑
Go 中的 switch 跟别的语言有一个区别,就是不需要显式地使用 break 语句来终止 case 的执行。当一个 case 的执行体执行完毕后,程序会自动终止 switch 语句的执行。
func main() {
var i = 1
switch i {
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
case 3:
fmt.Println("3")
}
}
// 输出
// 1
但是如果我们想要继续执行下一个 case 的执行体,我们可以使用 fallthrough
关键字:
func main() {
var i = 1
switch i {
case 1:
fmt.Println("1")
fallthrough
case 2:
fmt.Println("2")
case 3:
fmt.Println("3")
}
}
// 输出
// 1
// 2
goroutine 的坑
1、无法得到 goroutine 的退出状态
利用 channel,我们可以得到 goroutine 的退出状态:
func main() {
c := make(chan error, 1)
go func() {
// do something
c <- nil
}()
err := <-c
if err != nil {
fmt.Println("goroutine exit with error:", err)
}
}
2、程序随着 main goroutine 的退出而退出,不等待其他 goroutine
func main() {
println("start")
go func() {
time.Sleep(2*time.Second)
println("goroutine 1")
}()
go func() {
time.Sleep(3*time.Second)
println("goroutine 2")
}()
println("end")
}
// 输出
// start
// end
main goroutine 丝毫没有顾及正在运行的两个 goroutine,它在自己的任务结束后就立即退出了。我们可以通过 sync.WaitGroup
来解决这个问题:
func main() {
println("start")
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
time.Sleep(2*time.Second)
println("goroutine 1")
}()
go func() {
defer wg.Done()
time.Sleep(1*time.Second)
println("goroutine 2")
}()
wg.Wait()
println("end")
}
// 输出
// start
// goroutine 2
// goroutine 1
// end
3、任何一个 goroutine 的 panic ,如果没有及时捕获,那么整个程序都将退出
解决方法是采用防御性代码,即在每个 goroutine 的启动函数中加上对 panic 的捕获:
func safeRun(g func()) {
defer func() {
if err := recover(); err != nil {
fmt.Println("catch a panic:", err)
}
}()
g()
}
func main() {
println("start")
var wg sync.WaitGroup
wg.Add(1)
go safeRun(func() {
defer wg.Done()
panic("something wrong")
})
wg.Wait()
println("end")
}
channel 的坑
1、向已关闭的 channel 发送数据会 panic
func main() {
c := make(chan int)
close(c)
c <- 1 // panic: send on closed channel
}
2、从已关闭的 channel 接收数据不会 panic
func main() {
c := make(chan int)
close(c)
v, ok := <-c
fmt.Println(v, ok) // 0 false
}
3、从已关闭的 channel 接收数据会一直返回零值
func main() {
c := make(chan int)
close(c)
for i := 0; i < 3; i++ {
v := <-c
fmt.Println(v) // 0 0 0
}
}
方法的坑
1、使用值类型的 receiver 的方法无法改变类型实例的状态
Go 语言的方法(method)很独特,除了参数和返回值,它还拥有一个代表着类型实例的 receiver
参数。这个 receiver
参数可以是值类型或者指针类型。
而采用值类型的 receiver 的方法无法改变类型实例的状态。
我们来看一个例子:
type foo struct {
name string
age int
}
func (f foo) changeNameByValueReceiver(name string) {
f.name = name
}
func (p *foo) changeNameByPointReceiver(name int) {
p.name = name
}
func main() {
f := foo{"foo", 20}
f.changeNameByValueReceiver("bar")
fmt.Println(f) // {foo 20}
f.changeNameByPointReceiver("bar")
fmt.Println(f) // {bar 20}
}
之所以会这样,是因为方法本质上是一个以 receiver 为第一个参数的函数。当 receiver 为值类型时,方法内部对 receiver 的任何修改都是对 receiver 的拷贝进行的,不会影响到原 receiver。
2、值类型实例可以调用采用指针类型的 receiver 的方法,指针类型实例也可以调用采用值类型的 receiver 的方法
type foo struct {}
func (foo) methodWithValueReceiver() {
fmt.Println("value receiver")
}
func (*foo) methodWithPointerReceiver() {
fmt.Println("pointer receiver")
}
func main() {
f := foo{}
f.methodWithValueReceiver() // value receiver
f.methodWithPointerReceiver() // pointer receiver
p := &f
p.methodWithValueReceiver() // value receiver
p.methodWithPointerReceiver() // pointer receiver
}
这个语法糖的影响范围局限在类型实例调用方法的范畴。当我们将类型实例赋值给某个接口类型变量时,只有真正实现了该接口类型的实例类型才能赋值成功。
break 的坑
我们来看一个例子:
func breakWithForSwitch(b bool) {
for {
time.Sleep(1*time.Second)
fmt.Println("enter for-switch loop")
switch b {
case true:
break
case false:
fmt.Println("go on for-switch loop")
}
}
fmt.Println("exit for-switch loop")
}
func breakWithForSelect(c <-chan int) {
for {
time.Sleep(1*time.Second)
fmt.Println("enter for-select loop")
select {
case <-c:
break
default:
fmt.Println("go on for-select loop")
}
}
fmt.Println("exit for-select loop")
}
func main() {
go func() {
breakWithForSwitch(true)
}()
c := make(chan int, 1)
c <- 11
breakWithForSelect(c)
}
运行结果:
enter for-switch loop
enter for-select loop
enter for-select loop
go on for-select loop
enter for-switch loop
...
我们看到无论是 switch 还是 select,break 都无法终止最外层的 for 循环的执行,仅仅跳出了 switch 或 select 语句的执行体。 这就是 Go 语言 break 语句的原生语义:不接标签(label)的 break 语句会跳出最内层的 switch、select 或 for 语句的执行体。
如果要跳出最外层的 for 循环,我们可以使用标签(label):
func breakWithForSwitch(b bool) {
outerLoop:
for {
time.Sleep(1*time.Second)
fmt.Println("enter for-switch loop")
switch b {
case true:
break outerLoop
case false:
fmt.Println("go on for-switch loop")
}
}
fmt.Println("exit for-switch loop")
}