初学者常犯的错误

初学者常犯的错误 #

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

索引运算符和字符串 #

字符串上的 index 方法 (运算符) 返回一个字节值,而不是一个字符类型(就像在其他语言中一样)。

package main

import "fmt"

func main() {  
    x := "text"
    fmt.Println(x[0]) //print 116
    fmt.Printf("%T",x[0]) //prints uint8
}

如果需要访问特定字符串 “characters”(unicode 代码点 / 运行符),请使用 for range 语句。官方的 “unicode/utf8” 包和基础的 utf8string 包 (golang.org/x/exp/utf8string) 也很有用。utf8string 包有一个方便的 At() 方法,将字符串转换为切片也是一种选择。

使用 「for range」子句遍历 Map #

  • level:初学者

如果你希望 Map 每项数据按照顺序排列 (例如,按键值顺序),这是不可能的,每次 Map 迭代会输出不一样的结果。GO 运行时可能会随机分配迭代顺序,因此你可能会得到几次相同的 Map 迭代结果也不用惊讶。

package main

import "fmt"

func main() {  
    m := map[string]int{"one":1,"two":2,"three":3,"four":4}
    for k,v := range m {
        fmt.Println(k,v)
    }
}

而且,如果你使用 Go Playground ( play.golang.org/) 运行这段代码,将始终得到相同的迭代结果,因为除非进行更改代码,否则它不会重新编译代码。

增量和减量 #

  • 级别:初学者

许多语言都有递增和递减运算符。与其他语言不同,Go 不支持操作的前缀版本。你也不能在表达式中使用这两个运算符。

失败:

package main

import "fmt"

func main(){
    data := []int{1,2,3}
    i := 0
    ++i //错误
    fmt.Println(data [i++])//错误
}

编译错误:

/tmp/sandbox101231828/main.go:8:语法错误:意外的 ++ /tmp/sandbox101231828/main.go:9:语法错误:意外的 ++,期望:

作品:

package main

import "fmt"

func main(){
    data := []int{1,2,3}
    i := 0
    i++
    fmt.Println(data[i])
}

按位 NOT 运算符 #

  • 级别:初学者

许多语言都使用作为一元 NOT 运算符 (也称为按位补码),但是 Go 为此重用了 XOR 运算符 (^)。

失败:

package main

import "fmt"

func main(){
    fmt.Println(2)//错误
}

编译错误:

/tmp/sandbox965529189/main.go:6:按位补码运算符是 ^

作品:

package main

import "fmt"

func main(){
    var d uint8 = 2
    fmt.Printf(“%08b \ n”,^ d)
}

Go 仍然使用 ^ 作为 XOR 运算符,这可能会使某些人感到困惑。

如果你愿意,你可以用二进制的 XOR 操作 (例如,' NOT 0x02 ‘) 来表示一个单目的 NOT 操作 (例如,’ 0x02 XOR 0xff ‘)。这可以解释为什么 ^ 被重用于表示一元 NOT 操作。

Go 还具有一个特殊的 ‘AND NOT’ 按位运算符 (&^),这增加了 NOT 运算符的困惑。看起来像一个特性 / 黑客,不需要括号就可以支持 A AND (NOT B)

package main

import "fmt"

func main() {  
    var a uint8 = 0x82
    var b uint8 = 0x02
    fmt.Printf("%08b [A]\n",a)
    fmt.Printf("%08b [B]\n",b)

    fmt.Printf("%08b (NOT B)\n",^b)
    fmt.Printf("%08b ^ %08b = %08b [B XOR 0xff]\n",b,0xff,b ^ 0xff)

    fmt.Printf("%08b ^ %08b = %08b [A XOR B]\n",a,b,a ^ b)
    fmt.Printf("%08b & %08b = %08b [A AND B]\n",a,b,a & b)
    fmt.Printf("%08b &^%08b = %08b [A 'AND NOT' B]\n",a,b,a &^ b)
    fmt.Printf("%08b&(^%08b)= %08b [A AND (NOT B)]\n",a,b,a & (^b))
}

运算符优先级差异 #

  • 级别:初学者

除了「位清除」运算符 (&^) 之外,Go 还有许多其他语言共享的一组标准运算符。但是,运算符优先级并不总是相同。

package main

import "fmt"

func main() {  
    fmt.Printf("0x2 & 0x2 + 0x4 -> %#x\n",0x2 & 0x2 + 0x4)
    //prints: 0x2 & 0x2 + 0x4 -> 0x6
    //Go:    (0x2 & 0x2) + 0x4
    //C++:    0x2 & (0x2 + 0x4) -> 0x2

    fmt.Printf("0x2 + 0x2 << 0x1 -> %#x\n",0x2 + 0x2 << 0x1)
    //prints: 0x2 + 0x2 << 0x1 -> 0x6
    //Go:     0x2 + (0x2 << 0x1)
    //C++:   (0x2 + 0x2) << 0x1 -> 0x8

    fmt.Printf("0xf | 0x2 ^ 0x2 -> %#x\n",0xf | 0x2 ^ 0x2)
    //prints: 0xf | 0x2 ^ 0x2 -> 0xd
    //Go:    (0xf | 0x2) ^ 0x2
    //C++:    0xf | (0x2 ^ 0x2) -> 0xf
}

应用退出与活动的 Goroutines #

  • 级别:初学者

应用程序不会等待你的所有 goroutine 完成。对于一般的初学者来说,这是一个常见的错误。每个人都从某个地方开始,所以在犯菜鸟错误时不要觉得丢脸

package main

import (  
    "fmt"
    "time"
)

func main() {  
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        go doit(i)
    }
    time.Sleep(1 * time.Second)
    fmt.Println("all done!")
}

func doit(workerId int) {  
    fmt.Printf("[%v] is running\n",workerId)
    time.Sleep(3 * time.Second)
    fmt.Printf("[%v] is done\n",workerId)
}

你会看到的:

[0] 正在运行
[1] 正在运行
全部完成!

最常见的解决方案之一是使用 “WaitGroup” 变量。它将允许主 goroutine 等待直到所有工作程序 goroutine 完成。如果你的应用程序具有长时间运行的消息处理循环,则你还需要一种方法向那些 goroutine 发出退出信号的信号。你可以向每个工作人员发送 “杀死” 消息。另一个选择是关闭所有工作人员正在接收的渠道。这是一次发出所有 goroutine 信号的简单方法。

package main

import (  
    "fmt"
    "sync"
)

func main() {  
    var wg sync.WaitGroup
    done := make(chan struct{})
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doit(i,done,wg)
    }

    close(done)
    wg.Wait()
    fmt.Println("all done!")
}

func doit(workerId int,done <-chan struct{},wg sync.WaitGroup) {  
    fmt.Printf("[%v] is running\n",workerId)
    defer wg.Done()
    <- done
    fmt.Printf("[%v] is done\n",workerId)
}

如果你运行此应用,将会看到:

[0] is running
[0] is done
[1] is running
[1] is done

看起来 worker 在主 goroutine 退出之前已经完成。这太棒了!但是 1,你还会看到这样的情况:

fatal error: all goroutines are asleep - deadlock!

这不太好 发生了什么?为什么会出现死锁?当 worker 离开时,它们执行了 wg.Done()。应用程序应该是可以工作的。

发生死锁是因为每个 Worker 都会获得原始「WaitGroup」变量的副本。当工人执行 wg.Done() 时,它不会影响主 goroutine 中 的「WaitGroup」变量。

package main

import (  
    "fmt"
    "sync"
)

func main() {  
    var wg sync.WaitGroup
    done := make(chan struct{})
    wq := make(chan interface{})
    workerCount := 2

    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go doit(i,wq,done,&wg)
    }

    for i := 0; i < workerCount; i++ {
        wq <- i
    }

    close(done)
    wg.Wait()
    fmt.Println("all done!")
}

func doit(workerId int, wq <-chan interface{},done <-chan struct{},wg *sync.WaitGroup) {  
    fmt.Printf("[%v] is running\n",workerId)
    defer wg.Done()
    for {
        select {
        case m := <- wq:
            fmt.Printf("[%v] m => %v\n",workerId,m)
        case <- done:
            fmt.Printf("[%v] is done\n",workerId)
            return
        }
    }
}

现在它可以按预期工作了

“nil” 使用 “nil” 通道 #

Send and receive operations on a nil channel block forver. It’s a well documented behavior, but it can be a surprise for new Go developers.

package main

import (  
    "fmt"
    "time"
)

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

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

如果运行你的代码,你会看到这样的报错:
fatal error: all goroutines are asleep - deadlock!

出现这样的错误是因为你在 select 语句中 case 块中动态启用和禁用了管道。

package main

import "fmt"  
import "time"

func main() {  
    inch := make(chan int)
    outch := make(chan int)

    go func() {
        var in <- chan int = inch
        var out chan <- int
        var val int
        for {
            select {
            case out <- val:
                out = nil
                in = inch
            case val = <- in:
                out = outch
                in = nil
            }
        }
    }()

    go func() {
        for r := range outch {
            fmt.Println("result:",r)
        }
    }()

    time.Sleep(0)
    inch <- 1
    inch <- 2
    time.Sleep(3 * time.Second)
}

方法中的接受者不能修改原始值 #

  • 级别:初学者

方法接收者就像常规函数参数一样。如果声明为值,那么你的函数 / 方法将获得接收器参数的副本。这意味着对接收者进行更改不会影响原始值,除非你的接收者是映射或切片变量,并且你要更新集合中的项,或者你要在接收者中更新的字段是指针。

package main

import "fmt"

type data struct {  
    num int
    key *string
    items map[string]bool
}

func (this *data) pmethod() {  
    this.num = 7
}

func (this data) vmethod() {  
    this.num = 8
    *this.key = "v.key"
    this.items["vmethod"] = true
}

func main() {  
    key := "key.1"
    d := data{1,&key,make(map[string]bool)}

    fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
    //prints num=1 key=key.1 items=map[]

    d.pmethod()
    fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items) 
    //prints num=7 key=key.1 items=map[]

    d.vmethod()
    fmt.Printf("num=%v key=%v items=%v\n",d.num,*d.key,d.items)
    //prints num=7 key=v.key items=map[vmethod:true]
}


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