go新手常见陷阱
节选自《50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs》,仅摘录一些笔者比较在意的片段。
关联仓库yuchanns/gobyexample(包含测试用例)
初级篇
未指定类型变量不能用nil初始化
支持nil
初始化的变量类型有interface
、function
、pointer
、map
、slice
和channel
。所以使用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)
}
分析执行逻辑
- 首先令变量
in
和out
分别为单向输出和单向输入通道(这里原作者对in和out的意思定义和我似乎相反:我认为输入才是in,输出才是out😓)。 - 然后对通道
inCh
输入第一个数字1,这时候单向输出通道in有值输出,而out为nil——对于select来说,此时只有一个case val = <-in:
的选项。于是执行打印++++++++++并将out赋值为outCh,令in值为nil。 - 此时对于select来说,内部又变成了
case out <- val:
选项。内部执行了和2步骤相似的操作。 - 以此类推第二个数字。需要注意的是打印协程的输出实机视具体的运行平台而定。
中级篇
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)
}