新手常犯的错误

新手常犯的错误 #

引用: Go 经典译文:50 个 Go 新手易犯的错误(2020版)

花括号不能放在单独的一行 #

大多数使用花括号的语言中,你可以选择放置花括号的位置。 但 Go 不一样。 Go 在编译时会自动注入分号,花括号单独一行会导致分号注入错误(无需自己书写分号)。 所以 Go 其实是有分号的

错误的范例:

package main

import "fmt"

func main()  
{ // 错误,不能将左大括号放在单独的行上
    fmt.Println("hello there!")
}

编译错误:

/tmp/sandbox826898458/main.go:6: 语法错误: { 前出现意外的分号或者新的一行

正确的写法:

package main

import "fmt"

func main() {  
    fmt.Println("works!")
}

未使用的变量 #

如果存在未使用的变量会导致编译失败。但是有一个例外, 只有在函数内部声明的变量未使用才会导致报错,如果你有未使用的全局变量是没问题的,也可以存在未使用的函数参数。

如果给变量赋值但是没有使用该变量值,你的代码仍将无法编译。你需要以某种方式使用变量值以使编译器通过。

错误的范例:

package main

var gvar int //not an error

func main() {  
    var one int   //error, unused variable
    two := 2      //error, unused variable
    var three int //error, even though it's assigned 3 on the next line
    three = 3

    func(unused string) {
        fmt.Println("Unused arg. No compile error")
    }("what?")
}

编译错误:

/tmp/sandbox473116179/main.go:6: one declared and not used /tmp/sandbox473116179/main.go:7: two declared and not used /tmp/sandbox473116179/main.go:8: three declared and not used

正确的写法:

package main

import "fmt"

func main() {  
    var one int
    _ = one

    two := 2 
    fmt.Println(two)

    var three int 
    three = 3
    one = three

    var four int
    four = four
}

另一种选择是注释掉或删除未使用的变量

未使用的导入 #

如果你导入一个包却没有使用它的任何导出函数,接口,结构体或变量,你的代码将会编译失败。

如果确实需要导入包,你可以使用空白标识符_作为其包名,以避免此编译失败。对于这些副作用,使用空标识符来导入包。

错误的范例:

package main

import (  
    "fmt"
    "log"
    "time"
)

func main() {  
}

编译错误:

/tmp/sandbox627475386/main.go:4:导入但未使用:“fmt” /tmp/sandbox627475386/main.go:5:导入但未使用:“ log” /tmp/sandbox627475386/main.go:6:导入但未使用:“time”

正确的写法:

package main

import (  
    _ "fmt"
    "log"
    "time"
)

var _ = log.Println

func main() {  
    _ = time.Now
}

另一个选择是删除或注释掉未使用的导入 goimports 工具可以为你提供帮助。

短变量声明只能在函数内部使用 #

错误的范例:

package main

myvar := 1 //error

func main() {  
}

编译错误:

/tmp/sandbox265716165/main.go:3: non-declaration statement outside function body

正确的写法:

package main

var myvar = 1

func main() {  
}

使用短变量声明重新声明变量 #

你不能在独立的语句中重新声明变量,但在至少声明一个新变量的多变量声明中允许这样做。

重新声明的变量必须位于同一块中,否则最终将得到隐藏变量。

错误的范例:

package main

func main() {  
    one := 0
    one := 1 //error
}

编译错误:

/tmp/sandbox706333626/main.go:5: no new variables on left side of :=

正确的写法:

package main

func main() {  
    one := 0
    one, two := 1,2

    one,two = two,one
}

不能使用短变量声明来设置字段值 #

错误的范例:

package main

import (  
  "fmt"
)

type info struct {  
  result int
}

func work() (int,error) {  
    return 13,nil  
  }

func main() {  
  var data info

  data.result, err := work() //error
  fmt.Printf("info: %+v\n",data)
}

编译错误:

prog.go:18: non-name data.result on left side of :=

尽管有解决这个问题的办法,但它不太可能改变,因为 Rob Pike 喜欢它「按原样」

使用临时变量或预先声明所有变量并使用标准赋值运算符。

正确的写法:

package main

import (  
  "fmt"
)

type info struct {  
  result int
}

func work() (int,error) {  
    return 13,nil  
  }

func main() {  
  var data info

  var err error
  data.result, err = work() //ok
  if err != nil {
    fmt.Println(err)
    return
  }

  fmt.Printf("info: %+v\n",data) //prints: info: {result:13}
}

偶然的变量隐藏 #

简短的变量声明语法非常方便 (特别是对于那些来自动态语言的变量),以至于可以像对待常规赋值操作一样轻松地对待它。如果你在新的代码块中犯了此错误,将不会有编译器错误,但你的应用程序将无法达到你的期望。

package main

import "fmt"

func main() {  
    x := 1
    fmt.Println(x)     //prints 1
    {
        fmt.Println(x) //prints 1
        x := 2
        fmt.Println(x) //prints 2
    }
    fmt.Println(x)     //prints 1 (bad if you need 2)
}

即使对于有经验的 Go 开发者来说,这也是一个非常常见的陷阱。这很容易出现,可能很难发现。

你可以使用 vet 命令来查找其中的一些问题。默认情况下,vet 将不执行任何隐藏变量的检查。确保使用 -shadow 标志:go tool vet -shadow your_file.go

注意,vet 命令不会报告所有的隐藏变量。使用 go-nyet 进行更全面的隐藏变量检查。

不能使用 「nil」来初始化没有显式类型的变量 #

「nil」标识符可以用作接口,函数,指针,映射,切片和通道的「零值」。如果不指定变量类型,则编译器将无法编译代码,因为它无法猜测类型。

错误的范例:

package main

func main() {  
    var x = nil //error

    _ = x
}

编译错误:

/tmp/sandbox188239583/main.go:4: use of untyped nil

正确的写法:

package main

func main() {  
    var x interface{} = nil

    _ = x
}

使用 「nil」 切片和映射 #

可以将数据添加到「nil」切片中,但是对映射执行相同操作会产生运行时崩溃 (runtime panic)。

正确的写法:

package main

func main() {  
    var s []int
    s = append(s,1)
}

错误的范例:

package main

func main() {  
    var m map[string]int
    m["one"] = 1 //error

}

映射容量 #

你可以在创建映射时指定映射的容量,但不能在映射中使用 cap() 函数。

错误的范例:

package main

func main() {  
    m := make(map[string]int,99)
    cap(m) //error
}

编译错误:

/tmp/sandbox326543983/main.go:5: invalid argument m (type map[string]int) for cap

字符串不能为「nil」 #

对于习惯于为字符串变量分配「nil」标识符的开发人员来说,这是一个陷阱。

错误的范例:

package main

func main() {  
    var x string = nil //error

    if x == nil { //error
        x = "default"
    }
}

编译错误:

/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment /tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)

正确的写法:

package main

func main() {  
    var x string //defaults to "" (zero value)

    if x == "" {
        x = "default"
    }
}

数组函数参数 #

如果你是 C 或 C++ 开发者,那么你的数组是指针。当你将数组传递给函数时,这些函数将引用相同的内存位置,因此它们可以更新原始数据。Go 中的数组是值,因此当你将数组传递给函数时,这些函数会获取原始数组数据的副本。如果你尝试更新数组数据,则可能会出现问题。

package main

import "fmt"

func main() {  
    x := [3]int{1,2,3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [1 2 3] (not ok if you need [7 2 3])
}

如果你需要更新原始数组数据,请使用数组指针类型。

package main

import "fmt"

func main() {  
    x := [3]int{1,2,3}

    func(arr *[3]int) {
        (*arr)[0] = 7
        fmt.Println(arr) //prints &[7 2 3]
    }(&x)

    fmt.Println(x) //prints [7 2 3]
}

另一种选择是使用切片。即使你的函数获得了切片变量的副本,它仍然引用原始数据。

package main

import "fmt"

func main() {  
    x := []int{1,2,3}

    func(arr []int) {
        arr[0] = 7
        fmt.Println(arr) //prints [7 2 3]
    }(x)

    fmt.Println(x) //prints [7 2 3]
}

切片和数组「range」子句下的意外值 #

如果你习惯于使用其他语言的「for-in」或 「foreach」语句,则可能发生这种情况。Go 中的「range」子句不同。它生成两个值:第一个值是索引,而第二个值是数据。

错误的范例:

package main

import "fmt"

func main() {  
    x := []string{"a","b","c"}

    for v := range x {
        fmt.Println(v) //prints 0, 1, 2
    }
}

正确的写法:

package main

import "fmt"

func main() {  
    x := []string{"a","b","c"}

    for _, v := range x {
        fmt.Println(v) //prints a, b, c
    }
}

切片和数组是一维的 #

Go 看起来它支持多维数组和切片,但它并不支持。创建数组的数组或切片的切片是可能的。对于依赖于动态多维数组的数值计算应用程序来说,在性能和复杂性方面远远不够理想。

你可以使用原始的一维数组,「独立」切片的切片以及「共享数据」切片的切片来构建动态多维数组。

如果使用的是原始一维数组,则需要在数组增长时负责索引,边界检查和内存重新分配。

使用「独立」切片的切片创建动态多维数组是一个两步过程。首先,你必须创建外部切片。然后,你必须分配每个内部切片。内部切片彼此独立。你可以扩大和缩小它们,而不会影响其他内部切片。

package main

func main() {  
    x := 2
    y := 4

    table := make([][]int,x)
    for i:= range table {
        table[i] = make([]int,y)
    }
}

使用 「共享数据」切片的切片创建动态多维数组是一个三步过程。首先,你必须创建保存原始数据的数据「容器」切片。然后,创建外部切片。最后,通过重新排列原始数据切片来初始化每个内部切片。

package main

import "fmt"

func main() {  
    h, w := 2, 4

    raw := make([]int,h*w)
    for i := range raw {
        raw[i] = i
    }
    fmt.Println(raw,&raw[4])
    //prints: [0 1 2 3 4 5 6 7] <ptr_addr_x>

    table := make([][]int,h)
    for i:= range table {
        table[i] = raw[i*w:i*w + w]
    }

    fmt.Println(table,&table[1][0])
    //prints: [[0 1 2 3] [4 5 6 7]] <ptr_addr_x>
}

对于多维数组和切片有一个规范 / 建议,但目前看来这是低优先级的功能。

访问不存在的映射键 #

对于希望获得「nil」标识符的开发人员来说这是一个陷阱 (就像其他语言一样)。如果相应数据类型的「零值」为「 nil」,则返回值将为「 nil」,但对于其他数据类型,返回值将不同。检查适当的「零值」可用于确定映射记录是否存在,但是并不总是可靠的 (例如,如果你的布尔值映射中「零值」为 false,你会怎么做)。知道给定映射记录是否存在的最可靠方法是检查由映射访问操作返回的第二个值。

错误的范例:

package main

import "fmt"

func main() {  
    x := map[string]string{"one":"a","two":"","three":"c"}

    if v := x["two"]; v == "" { //incorrect
        fmt.Println("no entry")
    }
}

正确的写法:

package main

import "fmt"

func main() {  
    x := map[string]string{"one":"a","two":"","three":"c"}

    if _,ok := x["two"]; !ok {
        fmt.Println("no entry")
    }
}

字符串是不可变的 #

尝试使用索引运算符更新字符串变量中的单个字符将导致失败。字符串是只读字节片 (具有一些其他属性)。如果确实需要更新字符串,则在必要时使用字节片而不是将其转换为字符串类型。

错误的范例:

package main

import "fmt"

func main() {  
    x := "text"
    x[0] = 'T'

    fmt.Println(x)
}

编译错误:

/tmp/sandbox305565531/main.go:7: cannot assign to x[0]

正确的用法:

package main

import "fmt"

func main() {  
    x := "text"
    xbytes := []byte(x)
    xbytes[0] = 'T'

    fmt.Println(string(xbytes)) //prints Text
}

请注意,这不是真正更新文本字符串中字符的正确方法,因为给定字符可以存储在多个字节中。如果确实需要更新文本字符串,请先将其转换为符文切片。即使使用符文切片,单个字符也可能跨越多个符文。例如,如果你的字符带有重音符号,则可能会发生这种情况。「字符」的这种复杂和模凌两可的性质是将 Go 字符串表示为字节序列的原因。

字符串和字节片之间的转换 #

当你将字符串转换为字节片 (反之亦然) 时,你将获得原始数据的完整副本。这不像其他语言中的强制转换操作,也不像在新切片变量指向原始字节片所使用的相同基础数组的切片一样。

Go 对于 []bytestring ,和 string[]byte 确实做了一些优化,以免转换额外分配 (在待办事项列表中还对此进行了更多的优化)

第一个优化避免了在 map[string] 获取 m[string(key)] 中使用 []byte 的 keys 查找条目时的额外分配。

第二个优化避免了在 for range 字符串被转换的语句 []byte: for i,v := range []byte(str) {...}.

字符串并不总是 UTF8 文本 #

  • 等级:新手

字符串的值不一定是 UTF8 文本。它们可以包含任意字节。只有在使用字符串字面值时,字符串才是 UTF8。即使这样,它们也可以使用转译序列包括其他数据。若要了解你是否具有 UTF8 文本字符串,请使用 「unicode/uft8」包中的函数 ValidString()

package main

import (  
    "fmt"
    "unicode/utf8"
)

func main() {  
    data1 := "ABC"
    fmt.Println(utf8.ValidString(data1)) //prints: true

    data2 := "A\xfeC"
    fmt.Println(utf8.ValidString(data2)) //prints: false
}

字符串长度 #

  • 等级:新手

假设你是 python 开发者,并且使用下面的代码:

data = u'♥'  
print(len(data)) #prints: 1  

当你将其转换为类似的 Go 代码时,你可能会感到惊讶。

package main

import "fmt"

func main() {  
    data := "♥"
    fmt.Println(len(data)) //prints: 3
}

内置的 len() 函数返回字节数而不是字符数,就像 Python 中对 unicode 字符串所做的那样。

要在 Go 中获得相同的结果,请使用 「unicode/utf8」包中的 RuneCountInString() 函数。

package main

import (  
    "fmt"
    "unicode/utf8"
)

func main() {  
    data := "♥"
    fmt.Println(utf8.RuneCountInString(data)) //prints: 1

从技术上讲, RuneCountInString() 函数不会返回字符数,因为单个字符可能跨越多个符文。

package main

import (  
    "fmt"
    "unicode/utf8"
)

func main() {  
    data := "é"
    fmt.Println(len(data))                    //prints: 3
    fmt.Println(utf8.RuneCountInString(data)) //prints: 2
}

在多行切片,数组和映射字面值中缺少逗号 #

  • 等级:新手

错误的范例:

package main

func main() {  
    x := []int{
    1,
    2 //error
    }
    _ = x
}

编译错误:

/tmp/sandbox367520156/main.go:6: syntax error: need trailing comma before newline in composite literal /tmp/sandbox367520156/main.go:8: non-declaration statement outside function body /tmp/sandbox367520156/main.go:9: syntax error: unexpected }

正确的写法:

package main

func main() {  
    x := []int{
    1,
    2,
    }
    x = x

    y := []int{3,4,} //no error
    y = y
}

如果在声明折叠为一行时留下逗号,则不会出现编译错误。

log.Fatal 与 log.Panic 比 Log 要做的更多 #

  • 级别:新手

日志库通常提供不同的日志级别。与那些日志记录库不同,Go 中的日志包的作用远不止于日志记录。如果在你的应用中调用 Go 的 Fatal *()Panic *() 函数,Go 将会终止你的应用

package main

import "log"

func main() {  
    log.Fatalln("Fatal Level: log entry") //app exits here
    log.Println("Normal Level: log entry")
}

内置数据结构操作不同步 #

  • 等级:新手

尽管 Go 有很多支持并发的原生特性,但是并发安全的数据集合不在这些特性中。开发者需要保证对这些数据集合的并发更新操作是原子性的,比如对 map 的并发更新。Go 推荐使用 channels 来实现对集合数据的原子性操作。当然如果「sync」包更适合你的应用也可以利用「sync」包来实现。

「range」语句对于字符串的操作 #

  • 等级:新手

「range」语句的第一个返回值是当前「字符」(该字符可能是 unicode 码点 /rune)的第一个字节在字符串中按字节的索引值(unicode 是多字节编码),「range」语句的第二个返回值是当前的「字符」。这是 Go 其他语言不同的地方,其他语言的迭代操作大多是返回当前字符的位置,但 Go「range」返回的并不是当前字符的位置。在实际的使用中一个字符可能是由多个 rune 表示的,所以当我们需要处理字符时强烈推荐使用「norm」包(golang.org/x/text/unicode/norm)。

带有字符串变量的 for range 子句将尝试把数据解释为 UTF8 文本。对于任何它无法理解的字节序列,它将返回 0xfffd runes (即 Unicode 替换字符),而不是实际数据。如果你在字符串变量中存储了任意 (非 UTF8 文本) 数据,请确保将其转换为字节切片,以按原样获取所有存储的数据。

package main

import "fmt"

func main() {  
    data := "A\xfe\x02\xff\x04"
    for _,v := range data {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfffd 0x2 0xfffd 0x4 (not ok)

    fmt.Println()
    for _,v := range []byte(data) {
        fmt.Printf("%#x ",v)
    }
    //prints: 0x41 0xfe 0x2 0xff 0x4 (good)
}

switch 语句中的 Fallthrough 行为 #

  • 级别:新手

在 “switch” 语句中的 “case” 块,其缺省行为是 break 出 “switch”。这一行为与其它语言不同,其它语言的缺省行为是,继续执行下一个 “case” 块。

package main

import "fmt"

func main() {  
    isSpace := func(ch byte) bool {
        switch(ch) {
        case ' ': //error
        case '\t':
            return true
        }
        return false
    }

    fmt.Println(isSpace('\t')) //prints true (ok)
    fmt.Println(isSpace(' '))  //prints false (not ok)
}

你可以通过在每个 “case” 块的最后加入 “fallthrough” 语句来迫使 “case” 块继续往下执行。你也可以重写你的 “switch” 语句,在 “case” 块中使用表达式列表来达到这一目的。

package main

import "fmt"

func main() {  
    isSpace := func(ch byte) bool {
        switch(ch) {
        case ' ', '\t':
            return true
        }
        return false
    }

    fmt.Println(isSpace('\t')) //prints true (ok)
    fmt.Println(isSpace(' '))  //prints true (ok)
}

发送到无缓冲通道的消息在目标接收器准备就绪后立即返回 #

  • 等级:新手

直到接收方处理完你的消息后,发送才会被阻止。根据运行代码的机器,接收方 goroutine 可能会或可能没有足够的时间在发送方继续执行之前处理消息。

package main

import "fmt"

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

    go func() {
        for m := range ch {
            fmt.Println("processed:",m)
        }
    }()

    ch <- ".1"
    ch <- ".2" //won't be processed
}

发送到关闭通道会引起崩溃 #

  • 等级:新手

从关闭的通道接收是安全的。接收语句中的 ok 返回值将设置为 false 表示未接收到任何数据。如果你是从缓冲通道接收到的数据,则将首先获取缓冲数据,一旦缓冲数据为空,返回的 ok 返回值将为 false

发送数据到一个已经关闭的 channel 会触发 panic。 这是一个不容争论的事实,但是对于一个 Go 开发新手来说这样的事实可能不太容易理解,可能会更期望发送行为像接收行为那样。

package main

import (  
    "fmt"
    "time"
)

func main() {  
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func(idx int) {
            ch <- (idx + 1) * 2
        }(i)
    }

    //获取第一个结果
    fmt.Println(<-ch)
    close(ch) //这样做很不好 (因为在协程中还有动作在向 channel 发送数据)
    //做些其他的事情
    time.Sleep(2 * time.Second)
}

根据你的应用程序,修复这样的程序将会有所不同。修改细微的代码不让 panic 中断程序是次要的,因为可能你更加需要修改程序的逻辑设计。无论哪种方式,你都需要确保你的应用程序不会在 channel 已经关闭的情况下发送数据给它。

可以通过使用特殊的取消渠道来通知剩余的工作人员不再需要他们的结果,从而解决该示例问题。

package main

import (  
    "fmt"
    "time"
)

func main() {  
    ch := make(chan int)
    done := make(chan struct{})
    for i := 0; i < 3; i++ {
        go func(idx int) {
            select {
            case ch <- (idx + 1) * 2: fmt.Println(idx,"sent result")
            case <- done: fmt.Println(idx,"exiting")
            }
        }(i)
    }

    //get first result
    fmt.Println("result:",<-ch)
    close(done)
    //do other work
    time.Sleep(3 * time.Second)
}


本图书由小熊©2021 版权所有,所有文章采用知识署名-非商业性使用-禁止演绎 4.0 国际进行许可。