Go编程陷阱

Thursday, September 14, 2023

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() 修改了原值
}
Golang修炼

Go读取配置文件的方式

Go中的JSON序列化与反序列化

comments powered by Disqus