本文的整理于 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs, 下面是自己阅读完源文章后翻译了一些以及加上自己的一点见解。
这些情况使用于 Go 1.5 以及更低版本
目录 #
- 关闭 HTTP 返回的主体 (Response Body) 问题
- 关闭 HTTP 连接问题
- 把数字类型的 Json 存放到 Interface 问题
- 比较 Struts,Arrays,Slices 和 Maps
- 从 panic 中恢复问题
- 使用 range 对 Slice,Array 和 Map 的引用值进行更新问题
- Slice 中数据隐藏的问题
- Slice 的伸缩问题
- 注意类型声明和方法
- 理解’for switch’和’for select’
- 变量的遍历和闭包在 for 中使用要注意的问题
- 函数参数在使用 defer 时要注意的问题
- 执行函数在使用 defer 时要注意的问题
- 类型断言失败时数据处理问题
-
#
关闭 HTTP 返回的主体 (Response Body) 问题 #
对于 http 返回的 body 即使你不读取也要调用 Close 关闭,这样可以保证在开启了长连接时 http 连接可以被重用,
推荐关闭是是使用defer
, 而且要在对返回的 error 进行判断是否为 nil 后才调用 defer(即 defer response.Close() 应该置于 if err!=nil{}后面), 否则在函数返回时调用 resonse.Close 会产生 panic(对一个 nil 执行 Close 操作是不允许的), 参考 stackOverFlow 问答
如果连接重用对于程序来说很重要,你可能需要在函数的后面加上_, err = io.Copy(ioutil.Discard, resp.Body)
这个很有必要,因为如果代码中没有对整个 body 进行读取数据,比如使用 json 对数据进行转换
json.NewDecoder(resp.Body).Decode(&data)
时就有可能造成连接的不可重用
关闭 HTTP 连接问题 #
有时候不想重用 HTTP 连接,比如隔一段时间向不同服务器发送少量的 http 请求,可以把 request 的 Close 设置为 true
或者在 HTTP 头部加上 req.Header.Add("Connection", "close")
亦或者把长连接进行一个全局的关闭,需要创建一个http.Transport
,如下
tr := &http.Transport{DisableKeepAlives: true}
client := &http.Client{Transport: tr}
resp, err := client.Get("http://golang.org")
if resp != nil {
defer resp.Body.Close()
}
但如果与相同的服务器进行频繁的 HTTP 请求交互,还是把 KeepAlive 启用,且不要关闭 Connection 为好
把数字类型的 Json 存放到 Interface 问题 #
如果不指定 struct 变量对应的 json 类型,对于 json 的数字数据在 decode 时会转换成 float64 的类型,比如下面
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
fmt.Println("error:", err)
return
}
//var status = result["status"].(int) //error
var status = result["status"].(float64) //ok
fmt.Println("status value:",status)
另一种来获取想要的数据类型的方法是 使用 Decoder
, 把 json 数字转换成 json.Number 再直接转换成想要的类型,如
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
var decoder = json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := decoder.Decode(&result); err != nil {
fmt.Println("error:", err)
return
}
var status,_ = result["status"].(json.Number).Int64() //json.Numer->Int64
fmt.Println("status value:",status)
比较 Struts,Arrays,Slices 和 Maps #
一般来说只有基础的类型变量才可以直接使用 [==] 来比较的,而数组只有在元素个数相同时可以直接比较,Go 语言提供一系列的帮助函数来比较那些不能直接使用 [==] 来比较的变量,最常用的的reflect
包中的DeepEqual()
m1 := map[string]string{"one": "a","two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true
DeepEqual() 示例
但DeepEqual()
并不会把 nil Slice 和 empty Slice 视作一样,而 bytes.Equal() 却把 “nil”和 “empty” Slice 同等看待
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true
bytes.Equal() 示例
DeepEqual()
在比较 Slice 或者 map 时并不总是靠谱的
v1 := []string{"one","two"}
v2 := []interface{}{"one","two"}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1, v2))
//prints: v1 == v2: false (not ok)
data := map[string]interface{}{
"code": 200,
"value": []string{"one","two"},
}
encoded, _ := json.Marshal(data)
var decoded map[string]interface{}
json.Unmarshal(encoded, &decoded)
fmt.Println("data == decoded:",reflect.DeepEqual(data, decoded))
//prints: data == decoded: false (not ok)
DeepEqual() 比较 slice,map 示例
在(使用 ==,bytes.Equal(), bytes.Compare()) 比较时防止字母大小问题,记得使用bytes
或者strins
包中的ToUpper()
或者ToLower()
来转换,而对于包含有其它语言的字符测应该使用string.EqualFold()
和bytes.EqualFold()
如果 Slice 里面包含了一些密文如以 cryptographic hashes,tokens, 这些往往需要大量计算,造成 timing attacks, 可以使用crypto/subtle
包中的函数如subtle.ConstantTimeCompare()
来避免这类问题
从 panic 中恢复问题 #
recover() 要配合 defer 来使用,且调用的 defer() 时必须与 panic() 处于同一级函数
或者在上级函数中,因为 panic 在没有被捕获时是一层一层往上级传递的
注意 recover 在 defer 调用的函数的位置不可嵌于其它函数中,否则不会生效
还有 recover 的 defer 代码必须置于 panic() 之前
func pp() {
recover() //work
//not work
// func() {
// defer recover()
// }()
}
main{
defer pp()
panic("ddd")
//not execute
//defer pp()
}
使用 range 对 Slice,Array 和 Map 的引用值进行更新问题 #
对于值类型的 slice 在 range 进所得到 value 是源值一个 copy, 比如想要更新一个 int slice 这样是不行的
data := []int{1,2,3}
for _,v := range data {
v *= 10 //original item is not changed
}
fmt.Println("data:",data) //prints data: [1 2 3]
当然可以用下标指定更新的元素
data := []int{1,2,3}
for i,_ := range data {
data[i] *= 10
}
如果是一组指针 slice, 指针的值是地址,地址也是值,但更新数据时是源地址的数据故在 range 的更新会有效,
data := []*struct{num int} {{1},{2},{3}}
for _,v := range data {
v.num *= 10
}
fmt.Println(data[0],data[1],data[2]) //prints &{10} &{20} &{30}
Slice 中数据隐藏的问题 #
要知道 slice 变量的底层 (underlying array) 也是一个数组
像这种情况,在任何一个 slice 中更改数据都将会影响到底层的数组,
raw := make([]int, 10)
fmt.Println(len(raw), cap(raw), &raw[0]) //prints: 10 10 <byte_addr_x>
a := raw[:3]
a[1] = 988
fmt.Printf("a: %+v\n", a)
fmt.Printf("raw: %+v\n", raw)
若想要更新时不影响原来的 slice, 应该新建一个 slice, 然后从源 slice 中 copy 想要的数据
raw := make([]int, 10)
fmt.Println(len(raw), cap(raw), &raw[0]) //prints: 10 10 <byte_addr_x>
a := make([]int,3)
copy(a,raw[:3]
fmt.Printf("a: %+v\n", a)
fmt.Printf("raw: %+v\n", raw)
或者从源 slice 切分时加上最后一个容量限制大小 (full slice expression), 如
raw := make([]int, 10)
fmt.Println(len(raw), cap(raw), &raw[0]) //prints: 10 10 <byte_addr_x>
// a := raw[:3]
a := raw[:3:3]
a[1] = 988
a = append(a, 999)
//vs
//a[2] = 988
//a = a[:2:2]
//a[1] = 1111
fmt.Printf("a: %+v,cap(a):%d,len(a):%d arrd:%v \n", a, cap(a), len(a), &a[0])
fmt.Printf("raw: %+v,cap(raw):%d,len(raw):%d arrd:%v \n", raw, cap(raw), len(raw), &raw[0])
注意使用 a:=raw[:len:cap]\(这里的 cap 是原 slice 的长度,len 自取)这种全切分方式获取到的 slice 可能指向源 slice 的底层数组,若对 a 进行append 新的数据即增加数据时,a 会指向新的并且是自动扩展后复制了原来数据的底层数组, 否则 a 还是和源 slice 一样, 双方的操作都会互相影响
这文章 很值得阅读和理解。
Slice 的伸缩问题 #
这个问题要掌握的知识和 Slice 中数据隐藏的问题一样,都是 slice 中对底层数组的理解
注意类型声明和方法 #
如果你从一种结构类型中定义新的类型,新的类型并不会继承源类型的方法,如
type myMutex sync.Mutex
func main() {
var mtx myMutex
mtx.Lock() //error
mtx.Unlock() //error
}
可以使用匿名成员方式把这个源结构包含在结构中来解决
type myLocker struct {
sync.Mutex
}
func main() {
var lock myLocker
lock.Lock() //ok
lock.Unlock() //ok
}
此时 myLocker 已经和 sync.Mutex 一样可以使用相同的方法,它们都实现同样的接口(方法), 所以在使用时也可以这样初始化
type myLocker sync.Locker
func main() {
var lock myLocker = new(sync.Mutex)
lock.Lock() //ok
lock.Unlock() //ok
}
理解’for switch’和’for select’ #
在 switch 或者 select 中使用不带后缀 label 的 break 会跳出整个 loop, 如果不想使用 return,break label
的方式是不错的选择,当然goto
也可以在后面加 label
break label 示例
变量的遍历和闭包在 for 中使用要注意的问题 #
data := []string{"one","two","three"}
for _,v := range data {
vcopy := v //
go func() {
fmt.Println(vcopy)
}()
}
time.Sleep(3 * time.Second)
//goroutines print: three, three, three
上面中的 v 在整个 for range loop 中是共用的(不太好解释), 故最后显示的是最后一个 three,
若要显示 slice 中的每个元素,在 loop 应该使用新的变量,这个每一个 goroutine 中的函数(或者匿名函数)所使用的变量才不是同一个数据
而像这种情况
data := []string{"one","two","three"}
for _,v := range data {
go func(in string) {
fmt.Println(in)
}(v)
}
time.Sleep(3 * time.Second)
//goroutines print: one, two, three
因为 v 是作为函数的参数传入,是原来值的 copy, 如不管 v 在 loop 过程改变了,函数中的参数值还是最初传进来的那个
不过下面就不太好解释了,有点复杂
for trap 示例
引用类型的实例与非引用的实例在 for range loop 里面调用方法的结果不一样!!! 怎么解释?
故最好还是使用 fori loop 安全一点, 突然想起了康力 说过”我一般不用 for range”
函数参数在使用 defer 时要注意的问题 #
var i int = 1
defer fmt.Println("result =>",func() int { return i * 2 }())
i++
//prints: result => 2 (not ok if you expected 4)
(defer)[https://play.golang.org/p/3RmaKGDHqF]
这里要注意 defer 中使用的 i 是相对的属于全局变量,在 i++ 之前 i 为 1 时,就与保存在 defer 中
再看这个
这与变量的遍历和闭包在 for 中使用要注意的问题 有一定的相同点,但要明白,defer 在是函数退出时才执行, 但其所使用的变量值却不一定就是各个相关变量在函数执行后的值
执行函数在使用 defer 时要注意的问题 #
正如函数参数在使用 defer 时要注意的问题所提到,defer 在函数退出时执行而不是在最后一行代码执行完执行,如里在 for loop 中使用 defer 容易出错, 如
for _,target := range targets {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err) //prints error: too many open files
break
}
defer f.Close() //will not be closed at the end of this code block
//do something with the file...
}
可以使用一个函数来包裹 defer:
for _,target := range targets {
func() {
f, err := os.Open(target)
if err != nil {
fmt.Println("bad target:",target,"error:",err)
return
}
defer f.Close() //ok
//do something with the file...
}()
类型断言失败时数据处理问题 #
在对 interface 进行 type assert 时,可能有人喜欢用旧的变量名来保存要 type assert 的变量返回值,如
var data interface{} = "great"
if data, ok := data.(int); ok {
fmt.Println("[is an int] value =>",data)
} else {
fmt.Println("[not an int] value =>",data)
//prints: [not an int] value => 0 (not "great")
}
正确的方式应该使用新的变量来保存返回值:
if res, ok := data.(int); ok {
fmt.Println("[is an int] value =>",res)
} else {
fmt.Println("[not an int] value =>",data)
//prints: [not an int] value => great (as expected)
}
Goroutine 堵塞和资源泄漏问题 #
这一部分关系到 goroutine 和 channel 通信处理问题,看下面的示例
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
这是从多个目标中获取第一个结果,存在的问题为,多个 goroutine 往同一个 channel 写数据, 但最后会只有一个数据可以返回, 这样的话其它的 goroutine 会被堵塞 (channel 的 buffer 默认为 1), 最后造成资源的泄漏,
解决的一个方法,生成足够大的 channel buffer:
c := make(chan Result,len(replicas))
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
另一个方法是使用select
来防止写 channel 堵塞:
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
default:
}
}
还有另外一个方法是 select 一个 channel 来判断是否退出 goroutine:
done := make(chan struct{})
defer close(done)
searchReplica := func(i int) {
select {
case c <- replicas[i](query):
case <- done:
}
}
这里使用 struct{}类型的 channel 是因为这种类型不需要占用空间