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

1. 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 实体。

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

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

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

// 正确示例
var a = 1

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

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

// 错误示例
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)
   }
}

4. 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}]
}

5. 在为 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
}

7. 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: <nil>" ,原因就是在 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)
   }
}

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

结构体小写字母开头的私有字段无法被外部直接访问到,因此在进行 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}
}