why your golang sucks:每个人都会踩的go的五十个坑(21-30)

why your golang sucks:每个人都会踩的go的五十个坑(21-30)

1.本文是我在翻GO文章时发现的,译者主要完成了1-22条的翻译(有不少问题。),没了后续,本系列文章将会先转载并修订他最初的翻译,然后完成第23-50条的翻译

2.由于本篇文章最初写自2015年,而GO也在这两年间发生了较大变化,以下的50个坑将会重新实验修正,将会与原文稍微有些区别

3.本文翻译自 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs

4.http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.html

Go语言是一个简单却蕴含深意的语言。但是,即便号称是最简单的C语言,都能总结出一本《C陷阱与缺陷》,更何况Go语言呢。Go语言中的许多坑其实并不是因为Go自身的问题。一些错误你再别的语言中也会犯,例如作用域,一些错误就是对因为 Go 语言的特性不了解而导致的,例如 range。

其实如果你在学习Go语言的时候去认真地阅读官方文档,百科,邮件列表或者其他的类似 Rob Pike 的名人博客,报告,那么本文中提到的许多坑都可以避免。但是不是每个人都会从基础学起,例如译者就喜欢简单粗暴地直接用Go语言写程序。如果你也像译者一样,那么你应该读一下这篇文章:这样可以避免在调试程序时浪费过多时间。

本文将50个坑按照使用使用范围和难易程度分为以下三个级别:“新手入门级”,“新手深入级”,“新手进阶级”。

21.在Slice、Array、Map的多行书写最后的逗号不可省略

级别:新手入门级

出错代码:

package main

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

错误信息:

feiqianyousadeMacBook-Pro:go yousa$ go build 50demo.go
# command-line-arguments
./50demo.go:46: syntax error: unexpected semicolon or newline, expecting comma or }

修正代码:

扫描二维码关注公众号,回复: 4694153 查看本文章
package main

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

    y := []int{3,4,} //no error, 当然,如果把这些东西写在一行中,可以不加最后的逗号。
    y = y

    z := []int{3,4}
    z = z 
}

22.log.Fatal和log.Panic不仅仅是Log(打印日志)

级别:新手入门级

Logging libraries often provide different log levels. Unlike those logging libraries, the log package in Go does more than log if you call its Fatal*() and Panic*() functions. When your app calls those functions Go will also terminate your app :-)

通常,日志库会提供不同级别的日志打印。但Go原生的日志库log与他们有些不同,如果你直接使用了前缀是Fatal或者是Panic的日志接口,它们不仅仅会打印一条日志,也会使当前进程退出。

package main

import "log"

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

日志打印:

feiqianyousadeMacBook-Pro:go yousa$ ./50demo
2018/06/24 15:51:33 Fatal Level: log entry
// Panic is equivalent to l.Print() followed by a call to panic().
// 从这里可以看出Panic开头的接口是在打印日志之后会抛出一个panic,若不recover,则进程会退出
func (l *Logger) Panic(v ...interface{}) {
    s := fmt.Sprint(v...)
    l.Output(2, s)
    panic(s)
}

// Fatal is equivalent to l.Print() followed by a call to os.Exit(1).
// 从这里可以看出Fatal开头的接口是在打印完日志之后,直接调用Exit接口,会直接退出进程
func (l *Logger) Fatal(v ...interface{}) {
    l.Output(2, fmt.Sprint(v...))
    os.Exit(1)
}

23.GO内置的数据结构不是协程安全的

级别:新手入门级

Even though Go has a number of features to support concurrency natively, concurrency safe data collections are not one them :-) It’s your responsibility to ensure the data collection updates are atomic. Goroutines and channels are the recommended way to implement those atomic operations, but you can also leverage the “sync” package if it makes sense for your application.

虽然 Go 语言天生高并发,但是 Go 并没有考虑到数据安全。为了实现“原子化”的数据操作,开发者需要自己对数据操作进行加锁。当然, goroutine + channel 以及 sync 都是手动加锁的好方案。

这里以常量参数自增、多协程操作Slice和Map为例

常量参数自增示例代码:

import (
    "fmt"
    "sync"
)

var gSum int = 0
var wg sync.WaitGroup

func main() {

    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go sum()
    }

    wg.Wait()
    fmt.Println(gSum)
}

func sum() {
    defer wg.Done()
    gSum++
}

执行结果:

feiqianyousadeMacBook-Pro:go yousa$ ./plusplussafe
8936
feiqianyousadeMacBook-Pro:go yousa$ ./plusplussafe
8922
feiqianyousadeMacBook-Pro:go yousa$ ./plusplussafe
8972
feiqianyousadeMacBook-Pro:go yousa$ ./plusplussafe
8935
feiqianyousadeMacBook-Pro:go yousa$ ./plusplussafe
9007

多协程操作Slice和Map示例代码:

TODO

24.“range”语句在String中的迭代值

级别:新手入门级

一个字符,也可以有多个rune组成。需要处理字符,尽量使用golang.org/x/text/unicode/norm包。

for range他们会尝试将字符串解析成utf8的文本,若string中有无法解析的字符,它会返回oxfffd的rune字符value。

因此,任何包含非utf8字符的文本,一定要先将其转换成字符切片([]byte),再使用range进行处理。

示例代码:

package main

import "fmt"

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

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

25.“range”语句在Map中的迭代值

级别:新手入门级

每次的map迭代每次结果不会一定相同。从这里可以窥得Map的实现是基于哈希表

Go的runtime会尝试将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)
    }
}

打印输出:

feiqianyousadeMacBook-Pro:go yousa$ ./50demo
one 1
two 2
three 3
four 4
feiqianyousadeMacBook-Pro:go yousa$
feiqianyousadeMacBook-Pro:go yousa$ ./50demo
two 2
three 3
four 4
one 1
feiqianyousadeMacBook-Pro:go yousa$ ./50demo
one 1
two 2
three 3
four 4
feiqianyousadeMacBook-Pro:go yousa$ ./50demo
one 1
two 2
three 3
four 4
feiqianyousadeMacBook-Pro:go yousa$ ./50demo
two 2
three 3
four 4
one 1

26.”switch”声明中的case结束继续执行下一个case的失效行为

级别:新手入门级

在很多语言中,若在switch case结束不声明break的话,会自动执行下一个case。而Go不一样,在“switch”声明语句中的“case”语句块在默认情况下会break。

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)
}

你可以通过声明”fallthrough”来让case结束后继续执行下一个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)
}

27.自增和自减

级别:新手入门级

Many languages have increment and decrement operators. Unlike other languages, Go doesn’t support the prefix version of the operations. You also can’t use these two operators in expressions.

GO只有后置自增、后置自减,不存在前置自增、前置自减;另外,Go自增自减且无法嵌套在表达式中,只能独立成行,单独执行(这样可以减少了C/C++中复杂的自增自减嵌套,强制降低代码阅读难度,同时也减少了代码出错的几率)

出错代码:

package main

import "fmt"

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

错误信息:

feiqianyousadeMacBook-Pro:go yousa$ go build 50demo.go
# command-line-arguments
./50demo.go:48: syntax error: unexpected ++, expecting }

feiqianyousadeMacBook-Pro:go yousa$ go build 50demo.go
# command-line-arguments
./50demo.go:49: syntax error: unexpected ++, expecting :

修正代码:

package main

import "fmt"

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

28.按位NOT操作

许多语言的位操作是’~’符号,但是Go的NOT操作复用了XOR操作符。

出错代码:

package main

import "fmt"

func main() {  
    fmt.Println(~2) //error
}

错误信息:

feiqianyousadeMacBook-Pro:go yousa$ go build 50demo.go
# command-line-arguments
./50demo.go:46: bitwise complement operator is ^

修正代码:

package main

import "fmt"

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

在Go里,’^’依然是XOR符号,这可能对一些人来说有些迷惑。但考虑这个场景,0x00 ^ 0x2d和 ^0x2d其实是一样的,所以单符号操作^a也可以理解成a和0取异或,这样理解也是能够接受。

但Go的’AND NOT’操作,它的操作符是’&^’,这让人更迷惑。。。

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))
}

29.运算符优先级区别

Aside from the “bit clear” operators (&^) Go has a set of standard operators shared by many other languages. The operator precedence is not always the same though.

位运算(与、或、异或、取反)优先级高于四则运算(加、减、乘、除、取余),有别于C语言(C语言是四则运算优先级高于位运算)。

示例代码:

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
}

30.非导出字段在序列化时不会被encode和decode

The struct fields starting with lowercase letters will not be (json, xml, gob, etc.) encoded, so when you decode the structure you’ll end up with zero values in those unexported fields.

结构体在序列化时非导出字段(以小写字母开头的字段名)不会被encode,另外在decode时这些非导出字段的值为”0值”(整型为0,字符串为“”)

示例代码:

package main

import (  
    "fmt"
    "encoding/json"
)

type MyData struct {  
    One int
    two string
}

func main() {  
    in := MyData{1,"two"}
    fmt.Printf("%#v\n",in) //prints main.MyData{One:1, two:"two"}

    encoded,_ := json.Marshal(in)
    fmt.Println(string(encoded)) //prints {"One":1}

    var out MyData
    json.Unmarshal(encoded,&out)

    fmt.Printf("%#v\n",out) //prints main.MyData{One:1, two:""}
}

输出打印:

feiqianyousadeMacBook-Pro:go yousa$ go run 50demo.go
main.MyData{One:1, two:"two"}
{"One":1}
main.MyData{One:1, two:""}

那么,如果我想使上述代码中One的值json序列化后转换为key是one或者one_one该怎么办呢?one或者one_one开头不是大写字母,如果直接将One命名改为one或者one_one是不可行的。

解决这个问题,就使用到了Go语言的tag功能,在tag上声明该struct导出字段所对应序列化后的key值。

示例代码如下:

package main

import (  
    "fmt"
    "encoding/json"
)

type MyData struct {  
    One int             `json:"one_one"`
    two string
}

func main() {  
    in := MyData{1,"two"}
    fmt.Printf("%#v\n",in) 

    encoded,_ := json.Marshal(in)
    fmt.Println(string(encoded)) 

    var out MyData
    json.Unmarshal(encoded,&out)

    fmt.Printf("%#v\n",out) 
}

这里的json:"one_one"即是One字段的tag值,它表明在json序列化时,将其key值设置为one_one

输出打印:

feiqianyousadeMacBook-Pro:go yousa$ go run 50demo.go
main.MyData{One:1, two:"two"}
{"one_one":1}
main.MyData{One:1, two:""}

猜你喜欢

转载自blog.csdn.net/qq_15437667/article/details/80792797
Why
今日推荐