Why your golang sucks:每个人都会踩的GO的五十个坑 (11-20)

Why your golang sucks:每个人都会踩的GO的五十个坑 (11-20)

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

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

11.字符串赋值为 nil

级别:新手入门级

go的string类型变量无法赋值为nil,也无法与nil进行比较操作。string对应的空值是”“

nil类似于C/C++中的NULL,另外,Objective C和JAVA的String是可以赋值为NULL的。

错误代码:

package main

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

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

错误信息:

# command-line-arguments
./50demo.go:28: cannot use nil as type string in assignment
./50demo.go:30: invalid operation: x == nil (mismatched types string and nil)

修正代码:

package main

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

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

12.数组被当做函数入参进行传递

级别:新手入门级

在C/C++中,入参如果是一个数组的话,那么实际上传递的是这个数组的指针,在函数内对该指针进行修改,是可以修改到原本数组的内容。

在GO语言中,数组Array作为函数入参是传该数组的一个拷贝,在函数内对数组成员进行修改并不会影响原有数组;切片Slice作为入参则会传指针那么数组对你而言就是指针,对切片进行修改会影响原有的切片成员。

PS:

a [3]int,这是一个数组
b []int,这是一个切片  
需要明辨数组和切片的区别
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]
}

或者可以使用 Slice,

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

13.使用 Slice 和 Array 的 range

级别:新手入门级

如果你对别的语言中的 for in 和 foreach 熟悉的话,那么 Go 中的 range 使用方法完全不一样。

GO语言中的range每次会返回两个值,第一个值是在 Slice 或 Array 中的编号,第二个是对应的数据的值。

错误代码:

package main

import "fmt"

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

    for v := range x { //因为只有一个值接收,虽然编译会通过,但是这个v是下标值,也可以理解为index
        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
    }
}

14.Slice 和 Array 维度是一维

级别:新手入门级

Go 看上去支持多维的 Array 和 Slice,但是其实不然。尽管可以创建 Array 的 Array,也可以创建 Slice 的 Slice。对于依赖多维 Array 的计算密集型的程序,无论是从性能还是复杂程度,Go 都不是最佳选择。

当然,如果你选择创建嵌套的 Array 与嵌套的 Slice,那么你就得自己负责进行索引、进行下表检查、以及 Array 增长时的内存分配。嵌套 Slice 分为两种,Slice 中嵌套独立的 Slice,或者 Slice 中嵌套共享数据的 Slice。

使用嵌套的独立 Slice 创建多维的 Array 需要两步。第一步,创建外围 Slice,然后分配每个内部的 Slice。内部的 Slice 是独立的,可以对每个单独的内部 Slice 进行缩放。

package main

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

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

使用嵌套、共享数据的 Slice 创建多维 Array 需要三步。第一,创建数据“容器”,第二部,创建外围 Slice,第三部,对内部的 Slice 进行初始化。

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

Go 语言也有对于支持多维 Array 和 Slice 的需求,不过不要期待太多。Go 语言官方将这些需求分在“低优先级”组中。

15.访问不存在的 Map Key

级别:新手入门级

先看第一种写法,通过查询某一个值,看其是否是空值来判断map中该key值是否存在。这个写法在如果该key值在map下对应的值时空值(nil、0或者”“)的时候,也会认为该key不存在,但实际上该key存在。

错误代码:

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

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

no entry
no entry

检测对应的“零值”可以用于确定map中的记录是否存在,但这并不总是可行。

检测给定map中的记录是否存在的最可信的方法是,通过map的访问操作,检查第二个返回的值。

修正代码:

package main

import "fmt"

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

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

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

16.String 不可变

级别:新手入门级

对于 String 中单个字符的操作会导致编译失败。如果想要对 String 操作的话,应当使用[]byte,而不是将它转换为 String 类型。

错误信息:

package main

import "fmt"

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

    fmt.Println(x)
}

错误信息:

# command-line-arguments
./50demo.go:30: 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
}

需要注意的是:这并不是在文字string中更新字符的正确方式,因为给定的字符可能会存储在多个byte中。如果你确实需要更新一个文字string,先把它转换为一个rune slice([]rune)。即使使用rune slice,单个字符也可能会占据多个rune,比如当你的字符有特定的重音符号时就是这种情况。这种复杂又模糊的“字符”本质是Go字符串使用byte序列表示的原因。

我们将会在下面提到。

string不可变的好处

  1. 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现(String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

  2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

  3. 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

  4. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

  5. 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

17.String 与 Byte Slice 的转换

级别:新手入门级

当你把一个字符串转换为[]byte(或者反之)时,你就得到了一个原始数据的完整拷贝。这和其他语言中cast操作不同,也和新的slice变量指向原始[]byte使用的相同数组时的重新slice操作不同。

Go在[]byte到string和string到[]byte的转换中确实使用了一些优化来避免额外的内存分配(当然在golang的todo列表中有更多的优化还需要做)。

举个例子

一个优化是避免了当[]byte keys用于在map[string]集合中查询时的额外内存分配:

m[string(key)]

另一个优化避免了字符串转换为[]byte后在for range语句中的额外内存分配:

for i,v := range []byte(str) {...}

18.String 与下标

级别:新手入门级

对字符串的索引操作是返回一个byte值,而不是一个字符char(和其他语言中的做法一样)。

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

如果你需要访问特定编码的字符串中的“字符”(unicode编码的points/runes),使用for range。官方的“unicode/utf8”包和实验中的utf8string包(golang.org/x/exp/utf8string)也可以用。utf8string包中包含了一个很方便的At()方法。把字符串转换为rune的切片也是一个选项。

19.String并不一定存储的是UTF8格式字符

级别:新手入门级

String 类型不一定是 UTF8 编码,String 中也可以包含自定义的文字/字节。只有需要将字符串显示出来的时候才需要用 UTF8 格式,其他情况下可以随便用转义来表示任意字符。

可以使用 unicode/utf8 包中的 ValidString() 方法判断是否是 UTF8 类型的文本。

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
}

20.String 的长度

级别:新手入门级

Python 代码:

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

转换成类似的 Go 代码如下:

package main

import "fmt"

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

Go 的 len() 方法和 Python 的并不相同,和 Python 的 len 方法等价的 Go 方法是 RuneCountInString。

package main

import (  
    "fmt"
    "unicode/utf8"
)

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

当然有些情况(例如法语)的情况,RuneCountInString 也并不能完全返回字符数目,因为有些字符是使用多个rune的方式进行存储的。

package main

import (  
    "fmt"
    "unicode/utf8"
)

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

猜你喜欢

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