3

大家好,我是煎鱼。

前一次给大家分享了《你会犯这些 Go 编码错误吗(一)?》,不知道大家吸收的怎么样,还有再踩到类似的坑吗?

今天继续来第二弹,跟煎鱼上车。

Go 常见错误

6. 同名变量的作用域

问题

我们在编写程序时,由于各种临时变量,会常用变量名 n、i、err 等。有时候会遇到一些问题,如下代码:

func main() {
    n := 0
    if true {
        n := 1
        n++
    }
    fmt.Println(n)
}

程序的输出结果是什么。n 是 1,还是 2?

输出结果:

0

解决方法

上述代码的 n := 1 又重新声明了一个新变量,他是同名变量,同时很关键的,他包含同名局部变量。

我们的 n++ 影响的是 if 区块里的变量 n,而不是外部的同名变量 n。如果要正确影响,应当修改为:

func main() {
    n := 0
    if true {
        n = 1
        n++
    }
    fmt.Println(n)
}

输出结果:

2

这一个案例虽然单一提出来看并不复杂,但在许多 Go 初学者刚入门时经常会在应用程序中遇到类似的问题,然后问为什么不行...

据鱼的 7s 记忆,在全局 DB 句柄中等场景中遇到这个问题,来问煎鱼的应该有 10 次以上,是一个比较高频的 ”坑“ 了。

7. 循环中的临时变量

问题

相信不少同学在业务代码中做过类似的事情,那就是:边循环处理业务数据,边变更值内容。

如下代码:

s := []int{1, 1, 1}
for _, n := range s {
    n += 1
}
fmt.Println(s)

程序输出的结果是什么,i 成功均都 +1 了吗?

不,真正的输出结果是:[1 1 1]

解决方法

实际上在循环中,我们所引用的变量是临时变量,你去修改他是没有任何意义的,修改的根本不是你的原数据的结构。

我们需要定位到原数据,根据索引去定位修改。如下代码:

s := []int{1, 1, 1}
for i := range s {
    s[i] += 1
}
fmt.Println(s)

输出结果:

[2 2 2]

这很常见,一个不注意就会手抖。

8. JSON 转换和输出为空

问题

在做对外接口的数据对接和转换时,我们经常需要对 JSON 数据进行处理。

如下代码:

type T struct {
    name string
    age  int
}

func main() {
    p := T{"煎鱼", 18}
    jsonData, _ := json.Marshal(p)
    fmt.Println(string(jsonData))
}

输出结果是什么,能把姓名和年龄正常输出吗?

输出结果:

{}

你没有看错,程序的输出结果是空,没有转换到任何东西。

解决方法

原因是 JSON 的输出,只会输出公开(导出)字段,也就是首字母必须为大写。

我们需要进行如下改造:

type T struct {
    Name string
    Age  int
}

func main() {
    p := T{"煎鱼", 18}
    jsonData, _ := json.Marshal(p)
    ...
}

输出结果:

{"Name":"煎鱼","Age":18}

又或是显式指定 JSON 的标签:

type T struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

现在 IDE 能够很方便的直接生成 JSON 标签了,建议大家可以习惯性补上,确保字段规范。

真的可以避免不少的对接时的拉扯和误差。

9. 以为 recover 是万能的

问题

在 Go 中,goroutine + panic + recover 是天作之合,用起来很方便。常常会有同学以为他是万能的。

如下代码:

func gohead() {
    go func() {
        panic("煎鱼下班了")
    }()
}

func main() {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println(r)
            }
        }()

        gohead()
    }()

    time.Sleep(time.Second)
}

你认为输出结果是什么。程序被中断,还是成功 recover 了?

输出结果:

panic: 煎鱼下班了

goroutine 17 [running]:
main.gohead.func1()
        /Users/eddycjy/awesomeProject/main.go:10 +0x39
created by main.gohead
        /Users/eddycjy/awesomeProject/main.go:9 +0x35

你以为 recover 是万能的?并不。

解决方法

Go1 现阶段没有万能解决方法,只能遵守 Go 的规范(Go 就是不能跨 goroutine recover,是正确的逻辑)。

Goroutine 建议每个 goroutine 都需要有 recover 兜底,否则一旦出现 panic 就会导致应用中断,容器会重启。

另外 Go 底层主动抛出的致命错误 throw,是没有办法使用 recover 拦截的,请务必注意。

注:有同学一直认为 recover 可以拦截到 throw,特此说明。

10. nil 不是 nil

问题

我们在做程序的逻辑处理时,经常要对接口(interface)值进行判断。

如下代码:

func Foo() error {
    var err *os.PathError = nil
    return err
}

func main() {
    err := Foo()
    fmt.Println(err)
    fmt.Println(err == nil)
}

你认为输出结果是什么,是 nil 和 true 吗?

输出结果:

<nil>
false

表面看起来的 nil ,它并不等于 nil。

解决方法

接口值,是特殊的。只有当它的值和动态类型都为 nil 时,接口值才等于 nil。

在前面的问题代码中,实际上函数 Foo 返回的是 [nil, *os.PathError],我们将其与 [nil, nil] 进行比较,所以是 false。

如果要准确判断,要进行如下转换:

fmt.Println(err == (*os.PathError)(nil))

输出结果就会为 true。

另外就是尽量使用 error 类型,又或是避免与 interface 进行比较,这是比较危险的行为(有不少人不知道这一现象)。

总结

在今天这篇文章中,我们开始了 Go 常见编码错误的第二节,共涉及 5 个案例:

  1. 同名变量的作用域。
  2. 循环中的临时变量。
  3. JSON 转换和输出为空。
  4. 以为 recover 是万能的。
  5. nil 不是 nil。

这依然是非常常见的案例,你有没有遇到过?或是有其它的新的案例呢?

欢迎大家一起交流和沟通。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

推荐阅读

参考


煎鱼
8.4k 声望12.8k 粉丝