【Go入门可能会踩的坑-02】

Go语言是一门简单有趣的编程语言,与其他语言一样,在使用时不免会遇到很多坑,不过它们大多不是Go本身的设计缺陷。如果你刚从其他语言转到Go,那这篇文章可能会帮到你。旨在帮助你跳过这些坑,能减少大量调试代码的时间。

Go入门可能会踩的坑-01 中我们总结了一部分,现在我们学习另一部分。

16.string 类型的值是常量,不可更改

尝试使用索引遍历字符串,来更新字符串中个别字符,是不允许的

string类型的值是只读的二进制 byte slice,如果要修改字符串中的字符,将string转为[]byte 修改后,在转为string即可。

// 错误示例
func main() {
    x := "text"
    x[0] = "T"		// error : cannot assign to x[0]
    fmt.Println(x)
}

// 修改示例
func main() {
    x := "text"
    xBytes := []byte(x)
    xBytes[0] = "T" 		// 此时 T 是 rune 类型
    x = string(xBytes)
    fmt.Println(x)
}

注意:上面的示例并不是更新字符串的正确姿势,因为一个UTF8编码的字符汇能会占多个字节,比如汉字需要3-4个字节存储,此时更新其中一个字节是错误的。正确方式是:将string转为 rune slice(此时一个rune 可能会占多个byte),直接更新rune中的字符。

func main() {
    x := "text"
    xBytes := []rune(x)
    xBytes[0] = "你" 		
    x = string(xBytes)
    fmt.Println(x)		// 你ext
}

17. string 与 byte slice 之间的互转

当进行string 和byte slice 相互转换时,参与转换的是拷贝的原始值。这种转换的过程,与其他编程语言的强制类型转换不同,也和新slice和旧slice共享底层数组不同。

Go在string 与 byte slice 相互转换上优化了两点,避免了额外的内存你分配:

  • 在 map[string] 中查找key时,使用了对应的 []byte,避免做m[string(key)]的内存分配
  • 使用 for range 迭代string 转换为[]byte 的迭代: for i,v := range []byte(str){}

18. string 与索引操作符

对字符串使用索引访问返回的不是字符,而是一个byte值。这种处理方式和其他语言一样。

func main() {
    x := "ascii"
    fmt.Println(x[0])  		// 97
    fmt.Printf("%T", x[0])	// unit8
}

19.字符串并不都是UTF8文本

string的值不必是UTF8文本,可以包含任意值。只有字符串是文字字面值时才是UTF8文本, 字符串可以通过转义来包含其他数据。判断字符串是否是UTF8文本,可以使用“unicode/utf8” 包中的 ValidString()函数、

func main() {
    str1 := "ABC"
    fmt.Println(utf8.ValidString(str1)) // true
    
    str2 := "A\xdw"
    fmt.Println(utf8.ValidString(str2))  // false
    
    str3 := "A\\ssc"
    fmt.Println(utf8.ValidString(str3))	// true   把转义字符转义成字面值
}

20.字符串的长度

Go的内置函数 len() 返回的是字符串的 byte 数量,如果要得到字符串的字符数,可使用 “unicode/utf8” 包中的 RuneCountInString(str string) (n int)

func main() {
    char := "心"
    fmt.Println(utf8.RuneCountInString(char))  
}

// 注意: RuneCountInString  并不总是返回看到的字符数,因为有的字符会占用两个 rune

func main() {
    char := "é"
    fmt.Println(len(char))    // 3
    fmt.Println(utf8.RuneCountInString(char))    // 2
    fmt.Println("cafe\u0301")    // café    // 法文的 cafe,实际上是两个 rune 的组合
}

21. 在多行array、slice、map语句中缺少 , 号

func main() {
    x := []int{
        1, 
        2 	// syntax error : unexceted newline, expecting comma or }
    }
    
    y := []int{1,2}
    z := []int{1,2}
}

声明语句中 } 折叠到单行后,尾部的 , 不是必须的。

22. log.Fatal 和 log.Panic 不只是log

log标准库提供了不同的日志记录等级,与其他语言的日志库不同,Go的log包在调用 Fatal()、 Panic()时能做更多日志外的事,如中断程序的执行等

func main() {
    log.Fatal("fatal level log : log entry")  	// 输出信息后,程序终止执行
    log.Println("panic level log : log entry")
}

23. 对內建数据结构的操作不是同步的

尽管Go本身有大量的特性来支持并发,但并不保证并发的数据安全,用户需自己保证变量等数据以原子操作更新。

24. range 迭代 map

如果你希望已特定的顺序(比如按key排序)来迭代map,要注意每次迭代都可能产生不一样的结果。

Go的运行时是有意打乱迭代顺序的,所以如果得到的迭代结果可能不一致。但也不总会打乱,得到连续相同的结果也是可能的,例如:

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

25. switch 中的fallthrough 语句

Switch 语句中 case 代码块会默认带上break, 但可以使用 fallthrough 来强制执行下一个case 代码块

func main() {
    isSpace := func(char byte) bool {
        switch char {
        case ' ' :			// 空格符会直接break  返回false  
            // fallthrough
        case '\t' :
        	return true    
        }
        return false
    }
    
    
    fmt.Println(isSpace('\t'))   // true
    fmt.Println(isSpace(' '))    // false
}

也可以改写为case 多条件判断:

func main() {
    isSpace := func(char byte) bool {
        switch char {
        case ' ', '\t' :			
        	return true
        }
        return false
    }
    
    
    fmt.Println(isSpace('\t'))   // true
    fmt.Println(isSpace(' '))    // false
}

26. 自增和自减运算

很多编程语言都自带前置后置的 ++ – 运算。 但Go特立独行,去掉了前置操作, 同时 ++ – 值作为运算符而非表达式。

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

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

27.按位取反

很多编程语言使用 ~ 作为一元按位取反(NOT)操作符,Go重用^ XOR操作符来按位取反

// 错误示例
func main() {
    fmt.Println(~2)   // bitwise complement operator is ^
}

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

同时 ^ 也是按位异或(XOR)操作符。

28.运算符的优先级

除了位清除(bit clear) 操作符,Go也有很多和其他语言一样的位操作符,但是优先级另当别论。

优先级列表:

Precedence           Operator
5						* / % << >> & &^
4						+ - | ^
3						== != < <= > >=
2						&&
1						||

29.不导出的struct 字段无法被encode

以小写字母开头的字段成员是无法被外部直接访问的,所以struct 在进行json、xml、gob等格式的encode操作时,这些私有字段会被忽略,到处是得到零值。

func main() {
    type MyData struct{
        One int `json:"One"`
        two string `json:"two"`  
    }
    
    in := MyData{1, "two"}
    fmt.Printf("%#v\n", in)			// main.MyData{One:1, two:"two"}
	
    encode, _ := json.Marshal(in)
    fmt.Println(string(encode))     // {"One":1}
	
    var out MyData
    json.Unmarshal(encod, &out)
    fmt.Println("%#v\n")			// main.MyData{One:1, two:""}
}

30. 程序退出是还有goroutine 在执行

程序默认不等所有goroutine 都执行完才退出,这点需要特别注意:

// 主程序会直接退出
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)        // 模拟 goroutine 正在执行
    fmt.Printf("[%v] is done\n", workerID)
}

31. 向无缓冲的channel发送数据,只要receiver 准备好了就会立刻返回

只有在数据被receiver处理时,sender才会阻塞。因运行环境而异,在sender发送完数据后,receiver 的goroutine可能没有足够的时间处理下一个数据。

func main() {
    ch := make(chan string)
    
    go func() {
        for m := range ch {
            fmt.Println("processed:", m)
            time.Sleep(1 * time.Second)
        }  
    }()
    
    ch <- "cmd.1"
    ch <- "cmd.2"		
}

32.向已经关闭的channel发送数据会造成panic

从已经关闭的channel接收数据是安全的。接收状态值 ok 是 false时表明channel中已经没有数据可以接收了。类似的,从有缓冲的channel中接收数据,缓存的数据获取完再没有数据可取时,状态值也是false。

向已经关闭的channel中发送数据会造成panic:

func main() {
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func(idx int) {
           ch <- idx 
        }(i)
    }
    
    fmt.Println(<-ch)
    close(ch)
    time.Sleep(2 * time.Second)
}

image-20211228212432783

33.使用了值为nil的channel

在一个值为nil的channel上发送和接收数据将永久阻塞

func main() {
    var ch chan int    // 未初始化, 值为nil
    
    for i := 0; i<3; i++{
        go func(i int){
           ch <- i 
        }(i)
    }
    
    fmt.Println("result:", <- ch)
    time.Sleep(1 * time.Second)
}

34.若函数 receiver 传参是传值方式,则无法修改参数的原有值

方法receiver 的参数与一般函数的参数类似:如果声明为值,那方法体得到的是一份参数的值拷贝,此时对参数的任何修改都不会对原有值产生影响。除非receiver 参数是map或slice类型的变量,并且以指针方式更新map中的字段,slice中的元素,才会更新原有值。

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

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

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

func main() {
    key := "key1"

    d := data{1, &key, make(map[string]bool)}
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

    d.pointerFunc()    // 修改 num 的值为 7
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)

    d.valueFunc()    // 修改 key 和 items 的值
    fmt.Printf("num=%v  key=%v  items=%v\n", d.num, *d.key, d.items)
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值