260×260

科学搜查官yuchanns

理想的生活是纯粹地热爱技术
  • Shenzhen, China
  • 后端开发工程师
Posted 9 months ago

go新手常见陷阱

节选自《50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs》,仅摘录一些笔者比较在意的片段。

关联仓库yuchanns/gobyexample(包含测试用例)

初级篇

未指定类型变量不能用nil初始化

支持nil初始化的变量类型有interfacefunctionpointermapslicechannel。所以使用nil初始化未指定类型的变量会导致编译器无法自动推断:

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

初始化为nil的map无法添加元素

应该使用make方法声明来对map进行实际的内存分配;slice可以使用append方法对值为nil追加元素。

当然,初始化slice时最好预估一个长度,节省重复扩容开销。

package main func main() { m := make(map[string]int) // var m map[string]int // 错误示范,初始化值为nil m["one"] = 1 // 如果对上述值为nil的map添加元素,会报错 var s []int s = append(s, 1) // 正确的slice追加元素用法 }

初始化string不能为nil

nil不支持string类型的初始化。它的初始值应为空字符串:

package main func main() { var s string // var s string = nil // 错误示范,cannot use nil as type string in assignment if s == "" { s = "default" } }

range遍历slice和array时的非预期值用法

使用rang进行遍历时,第一个值固定返回索引,第二个值固定返回值。

如果只想用值,在索引位置可用_来接收,节省复制开销。

在大数组中最好不使用range来遍历,因为range的本质是对索引和值的复制和再赋值,开销较大;推荐使用for i := 0; i < len(s); i++ {}的方式进行。

package main import "fmt" func main() { x := []string{"a", "b", "c"} for _, v := range x { // 索引不进行复制 fmt.Println(v) } }

使用独立的一维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,无法通过访问索引的方式更改个别字符。如果想要更改,需要转化成[]byte类型。

对于UTF8字符串,实际上应该转换为[]rune类型,避免出现字节更新错误。

package main import "fmt" func main() { x := "test" xbytes := []byte(x) xbytest[0] = 'T' y := "s界" yrunes := []rune(y) yrunes[0] = '世' fmt.Println(string(xbytes)) fmt.Println(string(yrunes)) }

判断字符串是否为utf8文本以及获取字符串长度

字符串的内容并不一定是合法utf8文本,可以是任意字节,可以用unicode/utf8包的ValidString方法判断。

直接用内建的len方法获取的是字符串的byte数,同样可以使用unicode/utf8包的RuneCountInString来获取字符长度

package main import ( "fmt" "unicode/utf8" ) func main() { data := "♥" fmt.Println(utf8.ValidString(data)) fmt.Println(len(data)) fmt.Println(utf8.RuneCountInString(data)) }

使用值为nil的通道

向值为nil的通道发送和接收信息会永远阻塞,造成死锁。利用这个特性可以在select中动态的打开和关闭case语句块。

package main import "fmt" 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: println("--------") out = nil in = inCh case val = <-in: println("++++++++++") 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) }

分析执行逻辑

  1. 首先令变量inout分别为单向输出和单向输入通道(这里原作者对in和out的意思定义和我似乎相反:我认为输入才是in,输出才是out😓)。
  2. 然后对通道inCh输入第一个数字1,这时候单向输出通道in有值输出,而out为nil——对于select来说,此时只有一个case val = <-in:的选项。于是执行打印++++++++++并将out赋值为outCh,令in值为nil。
  3. 此时对于select来说,内部又变成了case out <- val:选项。内部执行了和2步骤相似的操作。
  4. 以此类推第二个数字。需要注意的是打印协程的输出实机视具体的运行平台而定。

中级篇

json使用Encode和Marshal的区别

两者都是把数据结构转化为json格式,但是两者的结果并不相等。

原因在于Encode是为了流准备的方法,它会在转换结果末尾自动添加一个换行符——这是流式json通信中用于换行分隔另一个json对象的符号。

package main import ( "fmt" "encoding/json" "bytes" ) func main() { data := map[string]int{"key": 1} var b bytes.Buffer json.NewEncoder(&b).Encode(data) raw,_ := json.Marshal(data) if b.String() == string(raw) { fmt.Println("same encoded data") } else { fmt.Printf("'%s' != '%s'\n",raw,b.String()) } }

这是一个规范的结果,不是错误,但是需要注意这个细节差异。

笔者通常使用Marshal方法,确实没注意到这个细节😅。

json自动转义html关键字行为

json包默认任何html关键字都会进行自动转义,这有时候和使用者的预期不符:

有可能第三方提出不能进行转义的奇葩要求,有可能你想表达的意思并非是html关键字代表的意思。

package main func main() { data := "x < y" // 使用者想表达的是x比y小这个意图 raw, _ := json.Marshal(data) fmt.Println(string(raw)) // 结果被转义成"x \u003c y" var b1 bytes.Buffer _ = json.NewEncoder(&b1).Encode(data) fmt.Println(b1.String()) // 和上面一样的结果 var b2 bytes.Buffer enc := json.NewEncoder(&b2) enc.SetEscapeHTML(false) _ = enc.Encode(data) fmt.Println(b2.String()) // 这才是想表达的意思"x < y" }

json数字解码为interface

如果像笔者这样直接使用结构体和Gin接收和发送json数据,很容易忽视这点而踩坑里:

默认情况下,go会将json中的数字解成float64类型的变量,这会导致panic

解决办法有:1.先转成int再使用;2.使用Decoder类型明确指定值类型;3.使用结构体(也就是笔者通常用的方法)

package main import ( "bytes" "encoding/json" "fmt" "log" ) func main() { var data = []byte(`{"status": 200}`) var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { log.Fatalln(err) } var status1 = uint64(result["status"].(float64)) // 第一种方法,先转成uint64再使用 fmt.Println("Status value:", status1) var decoder = json.NewDecoder(bytes.NewReader(data)) decoder.UseNumber() if err := decoder.Decode(&result); err != nil { log.Fatalln(err) } var status2, _ = result["status"].(json.Number).Int64() // 第二种方法,使用Decoder明确指定数字类型 fmt.Println("Status value:", status2) var resultS struct { Status uint64 `json:"status"` } if err := json.NewDecoder(bytes.NewReader(data)).Decode(&resultS); err != nil { log.Fatalln(err) } var status3 = resultS.Status // 第三种方法,使用结构体 fmt.Println("Status value:", status3) }

虽然是个小细节,笔者很少用到第三种以外的方法,仍然值得注意。

值得一提的是,当struct遇到字段类型不固定时(事实上在对接第三方接口的时候很有可能会遇到这种难受的事情),可以使用json.RawMessage来接收并根据情况解码为不同类型的变量。

pakcage main import ( "fmt" "log" ) func main() { records := [][]byte{ []byte(`{"status": 200, "tag": "one"}`), []byte(`{"status": "ok", "tag": "two"}`), } for _, record := range records { var result struct { StatusCode uint64 `json:"-"` StatusName string `json:"-"` Status json.RawMessage `json:"status"` Tag string `json:"tag"` } if err := json.NewDecode(bytes.NewReader(record)).Decoder(&result); err != nil { log.Fatalln(err) } var name string var code uint64 if err := json.Unmarshal(result.Status, &name); err == nil { result.StatusName = name } else if err := json.Unmarshal(result.Status, &code); err == nil { result.StatusCode = code } fmt.Printf("result => %+v\n", result) } }

slice中隐藏的容量

slice中切出新的slice时,底层指向的都是同一个数组。如果原slice非常大,尽管后来切分的新的slice只有一小部分数据,但是cap仍然会和原有的slice一样大。这样会导致难以预料的内存消耗。

正确的做法是使用copy方法复制临时的slice数据到一个指定了内存分配的变量中。

也可以使用完整的切片表达式,input[low:hight:max],这样容量就变成max-low了。

上面两种做法的结果是新的slice底层指向的是新的数组。

package main import "fmt" func main() { raw := make([]byte, 10000) fmt.Println(len(raw), cap(raw), &raw[0]) rawNew := raw[:3] fmt.Println(len(rawNew), cap(rawNew), &rawNew[0]) rawCopy := make([]byte, 3) copy(rawCopy, raw[:3]) fmt.Println(len(rawCopy), cap(rawCopy), &rawCopy[0]) rawFull := raw[:3:3] fmt.Println(len(rawFull), cap(rawFull), &rawFull[0]) }

defer执行时机

defer执行的时间不是在语句块结束后,而是在函数体执行结束后。

如果在main中直接使用defer,结果只有当main结束时defer才会调用。

在如下的循环体中,如果需要每次循环都执行defer里的操作,应该创建一个函数来执行循环中的操作。常见于批量读取文件需要关闭文件之类的场景中。

同时可以注意另一个小细节:每次循环的变量v应该通过赋值或者作为函数参数的方式来使用,否则循环中会指向最后一个值

package main import "fmt" func main() { a := []int{1, 2, 3} for _, v := range a { func(v int) { fmt.Println(v) defer fmt.Println("defer execution") // defer在这个匿名函数执行完毕之后立即调用 }(v) // v作为函数传值 } }

高级篇

值为nil的interface

interface类变量只有在类型和值均为nil的时候才与nil相等。

尤其需要注意当返回值类型为interface时,应明确返回nil,才能用是否为nil来判断。

func main() { var data *byte var in interface{} fmt.Println(data, data == nil) fmt.Println(in, in == nil) in = data fmt.Println(in, in == nil) }