Golang踩坑集锦

Golang踩坑集锦

Golang 是一门简单高效的编程语言,学习曲线平缓,开发效率高。不过,和使用其它语言一样,在编码的过程中也难免会踩到一些坑,这些坑一般都是开发者不熟悉 Golang 的一些特性导致的。本篇文章总结了一些常见的坑,希望大家在遇到类似情况的时候能够绕过这些坑,提高开发和联调效率。

Golang 中的对象

type Student struct {
}

func (s *Student) printName() {
   fmt.Println("Tom") // Tom
}

func main() {
   var s *Student
   fmt.Println("s == nil?", s == nil) // s == nil? true
   s.printName()
}

上述代码是可以正常输出的,这在 Java 等面向对象语言中是不可思议的。Golang 不是真正意义上的面向对象语言,Golang 中的对象其实是 struct 实体。

简短声明的变量不能用在函数外部

// 错误示例
a := 1 // syntax error: non-declaration statement outside function body

func foo() {
    fmt.Println(a)
}

// 正确示例
var a = 1

func foo() {
    fmt.Println(a)
}

简短声明不能用于设置结构体的字段

// 错误示例
type response struct {
   code int
}

func foo() (int, error) {
   return 3, nil
}

func main() {
   var resp response
   resp.code, err := foo() // non-name resp.code on left side of :=
   if err != nil {
      fmt.Println(err)
   }
}

// 正确示例
type response struct {
   code int
}

func foo() (int, error) {
   return 3, nil
}

func main() {
   var resp response
   var err error
   resp.code, err = foo()
   if err != nil {
      fmt.Println(err)
   }
}

for range 迭代 slice, array 时更新元素

在 for range 迭代中,遍历的值是元素的值拷贝,更新拷贝并不会更新原始的元素。

type Student struct {
   name  string
   score int
}

func main() {
   students := []Student{{"Tom", 58}, {"Lucy", 59}}
   for _, s := range students {
      s.score = 60
   }
   fmt.Println("students:", students) // students: [{Tom 58} {Lucy 59}]
}

上述代码,变量 s 是值拷贝,所以导致更新 s 的 score 字段并没有对原值的 score 字段生效。正确更改原始元素的 score 字段,需要用索引访问到原始元素进行更改。

type Student struct {
   name  string
   score int
}

func main() {
   students := []Student{{"Tom", 58}, {"Lucy", 59}}
   for index := range students {
      students[index].score = 60
   }
   fmt.Println("students:", students) // students: [{Tom 60} {Lucy 60}]
}

在为 nil 的 channel 上发送和接收数据将会永远阻塞

func main() {
   var ch chan int
   fmt.Println("ch == nil?", ch == nil) // ch == nil? true
   go func() {
      ch <- 1
   }()
   fmt.Println("element of ch: ", <-ch)
}

上述代码会报如下的死锁错误,而不是 NPE 异常:

fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]
goroutine 5 [chan send (nil chan)] 

6. defer 函数的传参

对于 defer 延迟执行的函数,传参在声明的时候就会求出具体值,而不是在执行时才求值。

func foo(x int) {
   fmt.Println("x in foo:", x) // x in foo: 1
}

func main() {
   x := 1
   defer foo(x)
   x += 1
   fmt.Println("x in main:", x) // x in main: 2
}

interface 变量和 nil 比较

很多人把 interface 误以为是指针类型,但是其实它不是。如果将 interface 变量和 nil 进行比较,只有类型和值都为 nil 时,两者才相等。

func main() {
   var x *int
   var i interface{}

   fmt.Printf("%v %v\n", x, x == nil) // <nil> true
   fmt.Printf("%T %v %v\n", i, i, i == nil) // <nil> <nil> true

   i = x

   fmt.Printf("%T %v %v\n", i, i, i == nil) // *int <nil> false
}

如果函数的返回值是 interface 类型,则要格外小心这个坑。

type response struct {
   code int
}

func foo(x int) interface{} {
   var resp *response = nil
   if x < 0 {
      return resp
   }

   return &response{0}
}

func main() {
   if resp := foo(-1); resp == nil {
      fmt.Println("invalid parameter, resp:", resp)
   } else {
      fmt.Println("work fine, resp:", resp) // work fine, resp: <nil>
   }
}

上述代码,main 函数进入了 else 逻辑打印出了 "work fine, resp: ",原因就是在 foo 函数中 if 逻辑返回的 resp 变量虽然值是 nil,但是类型并非 nil。如果想改正上述代码,则需要在 if 逻辑中明确返回 nil。

 type response struct {
   code int
}

func foo(x int) interface{} {
   if x < 0 {
      return nil
   }

   return &response{0}
}

func main() {
   if resp := foo(-1); resp == nil {
      fmt.Println("invalid parameter, resp:", resp) // invalid parameter, resp: <nil>
   } else {
      fmt.Println("work fine, resp:", resp)
   }
}

结构体的私有字段无法被序列化

结构体小写字母开头的私有字段无法被外部直接访问到,因此在进行 json, xml 等格式的序列化操作时,这些字段将会被忽略,相应地在反序列化时得到的是零值。

type Student struct {
   Name  string
   score int // 私有字段
}

func main() {
   s1 := Student{"Tom", 90}
   buf, _ := json.Marshal(s1)
   fmt.Println(string(buf)) // {"Name":"Tom"}

   var s2 Student
   json.Unmarshal(buf, &s2)
   fmt.Printf("%+v\n", s2) // {Name:Tom score:0}
}

数组类型做为函数传参

在 Python 和 C/C++ 等语言中,数组类型做为函数传参时,相当于传递了数组内存地址的引用,在函数内可以更改原数组的值。但是在 Golang 中,数组类型做为函数传参时是进行的值拷贝,在函数内部无法更改原数组的值。

func foo(a [3]int) {
   a[0] = 10
}

func main() {
   a := [3]int{1, 2, 3}
   foo(a)
   fmt.Println("a:", a) // a: [1 2 3]
}

如果用的是 slice,虽然在传参是也是值拷贝,但是拷贝的是引用,指向的还是相同的一片内存。因此可以更新原 slice 的值。

func foo(a []int) {
   a[0] = 10
}

func main() {
   a := []int{1, 2, 3}
   foo(a)
   fmt.Println("a:", a) // a: [10 2 3]
}

slice, map 中元素是值拷贝

将元素放到 slice, map 中是进行的值拷贝,这意味着更改原始元素不会影响 slice, map 中的元素。

 type Student struct {
   Name  string
   Score int
}

func main() {
   s := Student{"Tom", 59}
   students := []Student{s}
   s.Score = 60
   fmt.Printf("s: %+v\n", s) // s: {Name:Tom Score:60}
   fmt.Printf("students[0]: %+v\n", students[0]) // students[0]: {Name:Tom Score:59}
}

float64 精度丢失

func main() {
   f := 19.9
   fmt.Printf("%T\n", f) // float64
   fmt.Println(f) // 19.9
   fmt.Println(f * 100) // 1989.9999999999998
}

可借助 github.com/shopspring/decimal 包来实现更加高的精度

 func main() {
   decimalValue := decimal.NewFromFloat(19.9)
   decimalValue = decimalValue.Mul(decimal.NewFromInt(100))

   f, _ := decimalValue.Float64()
   fmt.Println(f) // 1990
}

map 中元素不可寻址

map 中元素并不是一个变量,而是一个值。因此,不能对 map 中元素进行取址操作。那么,如果 map 中元素是个结构体类型,则无法直接更新该结构体的单个字段。

type Student struct {
   Name  string
   Score int
}

func main() {
   students := map[string]Student{
      "Tom": {"Tom", 59},
   }
   fmt.Printf("%p\n", &students["Tom"]) // 编译失败,Cannot take the address of 's["Tom"]'
   students["Tom"].Score = 90 // 编译失败,Cannot assign to 'students["Tom"].Score'
}

for range 迭代变量与闭包函数

for range 迭代中迭代变量在每次迭代中都会被重用。那么在 for 循环中创建的闭包函数接收到的参数始终是同一个变量。

func main() {
   a := []int{1, 2, 3}
   for _, x := range a {
      go func() {
         fmt.Println(x)
      }()
   }
   time.Sleep(3 * time.Second)
}

上述代码将依次输出 3 个 3,如果想依次输出数组 a 里面的 1,2,3,可以有两种方法:1. 将迭代值保存成局部变量;2. 直接将迭代值以参数形式传递给匿名函数。

func main() {
   a := []int{1, 2, 3}
   for _, x := range a {
      x := x
      go func() {
         fmt.Println(x)
      }()
   }
   time.Sleep(3 * time.Second)
}

func main() {
   a := []int{1, 2, 3}
   for _, x := range a {
      go func(x int) {
         fmt.Println(x)
      }(x)
   }
   time.Sleep(3 * time.Second)
}

defer 可以读取有名返回值

func foo() (x int) {
   defer func() {
      x++
   }()
   return 1
}

func main() {
   fmt.Println(foo()) // 2
}

上述代码中,当 defer 函数返回时,能读取到有名的返回值 x=1,进行 defer 里面的 x++,所以返回输出是 2,而不是 1。

结构体方法的值拷贝

type Student struct {
   Name  string
   Score int
}

func (s Student) setScore(score int) {
   fmt.Printf("s in setScore: %p\n", &s) // s in setScore: 0xc000088040
   s.Score = score
}

func main() {
   s := Student{"Tom", 59}
   fmt.Printf("s in main: %p\n", &s) // s in main: 0xc000088020
   s.setScore(60)
   fmt.Printf("s: %+v\n", s) // s: {Name:Tom Score:59}
}

上述代码中,结构体方法 setScore 中的结构体变量 s 是值拷贝。所以,在 main 函数中调用 s.setScore 的时候,是拷贝了一个副本给函数 setScore,然后对副本进行 score 的设置,对 main 函数中的原始变量 s 没有发生影响。通过打印 main 函数和 setScore 函数中的变量 s 的地址能印证这一点。
如果将 setScore 方法前面的结构体改成结构体指针,则可将方法里面的变更对 main 函数中的原始变量 s 生效,因为传入指针是指向变量 s 内存的指针。

type Student struct {
   Name  string
   Score int
}

func (s *Student) setScore(score int) {
   fmt.Printf("s in setScore: %p\n", s) // s in setScore: 0xc00000a080
   s.Score = score
}

func main() {
   s := Student{"Tom", 59}
   fmt.Printf("s in main: %p\n", &s) // s in main: 0xc00000a080
   s.setScore(60)
   fmt.Printf("s: %+v\n", s) // s: {Name:Tom Score:60}
}

生成随机数前需要调用 Seed 函数

使用 math/rand 包生成随机数的时候,如果没有先调用 Seed 函数,虽然连续调用看起来生成的是随机数,但是每次运行的结果都是一样的。

func main() {
   fmt.Println(rand.Intn(100)) // 81
   fmt.Println(rand.Intn(100)) // 87
   fmt.Println(rand.Intn(100)) // 47
}

需要在生成随机数前调用 Seed 函数,并传入一个变化的值做为参数,比如 time.Now().Unix()。

func main() {
   rand.Seed(time.Now().Unix())
   fmt.Println(rand.Intn(100))
   fmt.Println(rand.Intn(100))
   fmt.Println(rand.Intn(100))
}

声明新类型与方法继承

由一个非 interface 类型创建新类型时,并不会继承原有的方法。

// sync.Mutex definition
//type Mutex struct {
//   state int32
//   sema  uint32
//}

type myMutex sync.Mutex

func main() {
   var mtx myMutex
   mtx.Lock() // 报错:mtx.Lock undefined (type myMutex has no field or method Lock)
   mtx.UnLock() // 报错:mtx.UnLock undefined (type myMutex has no field or method UnLock)
}

如果想需要使用原类型的方法,可将原类型以匿名字段的形式嵌到新定义的类型中。

type myLocker struct {
   sync.Mutex
}

func main() {
   var locker myLocker
   locker.Lock()
   locker.Unlock()
}

但是,由一个 interface 类型创建新类型时,是会继承原有的方法的。

// sync.Locker definition
//type Locker interface {
// Lock()
// Unlock()
//}

type myLocker sync.Locker

func main() {
   var locker myLocker
   locker.Lock()
   locker.Unlock()
}

json.Marshal 处理 map 类型

json.Marshal 在处理 map 类型时,key 可以是 string, int 等基本类型,但是不能是 interface{} 类型。

func main() {
   mp := map[interface{}]string{
      1: "1",
      2: "2",
   }
   buf, err := json.Marshal(mp)
   if err != nil {
      fmt.Println(err) // 报错:json: unsupported type: map[interface {}]string
      return
   }
   fmt.Println(string(buf))
}

数组可以用指定索引和对应值的方式初始化

和其它语言不同的是,golang 中数组可以用指定索引及其对应值的方式进行初始化,中间索引的初始值为默认值。

func main() {
   a := [...]int{0: 1, 2: 1, 3: 4}
   fmt.Println(a) // [1 0 1 4]
}

json 反序列数字到 interface{} 类型的值中时,默认解析为 float64 类型

func main() {
   str := `{"name": "Tom", "age": 20}`
   var mp map[string]interface{}

   json.Unmarshal([]byte(str), &mp)
   age := mp["Tom"].(int) // 报错:panic: interface conversion: interface {} is nil, not int
   fmt.Println(age)
}

使用 Golang 解析 json 格式数据时,若以 interface{} 接收数据,则会按照下列规则进行解析:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

:= 赋值导致变量屏蔽

:= 往往是用来声明局部变量,在多个变量赋值且有的值存在的情况下,:= 也可以用来赋值使用,例如:

msg := "hello wolrd"
msg, err := "hello", errors.New("xxx") // err 并不存在,是声明,msg 是赋值

但是一旦作用域发生改变的时候,有可能导致变量屏蔽,例如下列代码中,第4行代码是在 if 作用域里重新声明了一个变量 a,屏蔽了外部的变量 a,导致 if 作用域里面对 a 的操作都不会对外部的 a 造成任何影响。

func main() {
   a := 2
   if a > 1 {
      a, err := doDivision(a, 2)
      if err != nil {
         panic(err)
      }
      fmt.Println(a) // 1
   }
   fmt.Println(a) // 2
}

func doDivision(a, b int) (int, error) {
   if b == 0 {
      return 0, errors.New("input is invalid")
   }
   return a / b, nil
}

返回值被屏蔽

和上面的问题比较类似,:= 赋值可能会导致在局部作用域中,命名的返回值内同名的局部变量屏蔽:

func main() {
   fmt.Println(foo())
}

func foo() (err error) {
   if err := test(); err != nil {
      return // 报错:err is shadowed during return
   }
   return
}

func test() error {
   return errors.New("hello world")
}

上述代码在第7行会报错:err is shadowed during return,但是如果将这行代码改成: return err 就没有问题,并且打印出: hello world。

interface 断言时的内存拷贝

不少人将 golang 的 interface 看成 Java 的 Object 以及 C 语言的 void*。但是后者通过强转为某个类型获取指向原数据的一个目标类型的引用或指针,因此如果在这个引用或指针上进行修改操作,原数据也会被修改;但是 golang 的 interface 和具体类型之间的转换、赋值是将实际数据复制了一份进行操作的。例如下面代码,实际的过程是首先将 tom 指向的数据复制一份,然后转换为 Student 类型赋值给 s 变量,因此对 s 变量的修改不会影响到原变量。

type Student struct {
   name  string
   score int
}

func main() {
   var tom interface{} = Student{
      name: "Tom",
      score: 59,
   }
   s := tom.(Student)
   s.score = 60
   fmt.Println(tom.(Student).score) // 59
}

阻塞 channel 只有数据被 receiver 处理时才阻塞

向阻塞 channel 发送数据,只要 receiver 准备好了就立马返回。因此,sender 发送数据后,receiver 的 goroutine 可能没有时间处理下一个数据。

func main() {
   ch := make(chan string)

   go func() {
      for v := range ch {
         fmt.Println("Received:", v) // 只打印一行 a
         time.Sleep(1 * time.Second)
      }
   }()

   ch <- "a"
   ch <- "b" // 可能不会被打印出来。主协程退出后,子协程可能还没来得及输出,整个进程就结束了
}
0
比特币中运用到的密码学原理 认证、授权、鉴权、权限控制

没有评论

No comments yet

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注