Golang作为一个略古怪而新的语言,有自己一套特色和哲学。从其他语言转来的开发者在刚接触到的时候往往大吃苦头,我也不例外。这篇文章很细致地介绍了Golang的一些常见坑点,读完全篇中枪好多次。故将其转载。由于文章很长,分为上下两部分,第一部分记录初级篇,第二部分记录进阶和高级篇:此为第二部分,若要看第一部分,请转至这里
感谢原文作者Kyle Quest以及翻译者影风LEY。出处见下:
原文链接:http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/
译文链接:http://www.shwley.com/index.php/archives/80/以及http://www.shwley.com/index.php/archives/82/。
转载已获得授权。
目录
- 初级篇
- 开大括号不能放在单独的一行
- 未使用的变量
- 未使用的Imports
- 简式的变量声明仅可以在函数内部使用
- 使用简式声明重复声明变量
- 偶然的变量隐藏Accidental Variable Shadowing
- 不使用显式类型,无法使用“nil”来初始化变量
- 使用“nil” Slices and Maps
- Map的容量
- 字符串不会为“nil”
- Array函数的参数
- 在Slice和Array使用“range”语句时的出现的不希望得到的值
- Slices和Arrays是一维的
- 访问不存在的Map Keys
- Strings无法修改
- String和Byte Slice之间的转换
- String和索引操作
- 字符串不总是UTF8文本
- 字符串的长度
- 在多行的Slice、Array和Map语句中遗漏逗号
- log.Fatal和log.Panic不仅仅是Log
- 内建的数据结构操作不是同步的
- String在“range”语句中的迭代值
- 对Map使用“for range”语句迭代
- "switch"声明中的失效行为
- 自增和自减
- 按位NOT操作
- 操作优先级的差异
- 未导出的结构体不会被编码
- 有活动的Goroutines下的应用退出
- 向无缓存的Channel发送消息,只要目标接收者准备好就会立即返回
- 向已关闭的Channel发送会引起Panic
- 使用"nil" Channels
- 传值方法的接收者无法修改原有的值
- 进阶篇
- 关闭HTTP的响应
- 关闭HTTP的连接
- 比较Structs, Arrays, Slices, and Maps
- 从Panic中恢复
- 在Slice, Array, and Map "range"语句中更新引用元素的值
- 在Slice中"隐藏"数据
- Slice的数据“毁坏”
- "走味的"Slices
- 类型声明和方法
- 从"for switch"和"for select"代码块中跳出
- "for"声明中的迭代变量和闭包
- Defer函数调用参数的求值
- 被Defer的函数调用执行
- 失败的类型断言
- 阻塞的Goroutine和资源泄露
- 高级篇
- 使用指针接收方法的值的实例
- 更新Map的值
- "nil" Interfaces和"nil" Interfaces的值
- 栈和堆变量
- GOMAXPROCS, 并发, 和并行
- 读写操作的重排顺序
- 优先调度
进阶篇
关闭HTTP的响应
当你使用标准http库发起请求时,你得到一个http的响应变量。如果你不读取响应主体,你依旧需要关闭它。注意对于空的响应你也一定要这么做。对于新的Go开发者而言,这个很容易就会忘掉。
一些新的Go开发者确实尝试关闭响应主体,但他们在错误的地方做。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close()//not ok
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
这段代码对于成功的请求没问题,但如果http的请求失败, resp 变量可能会是 nil ,这将导致一个runtime panic。
最常见的关闭响应主体的方法是在http响应的错误检查后调用 defer 。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()//ok, most of the time :-)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
大多数情况下,当你的http响应失败时, resp 变量将为 nil ,而 err 变量将是 non-nil 。然而,当你得到一个重定向的错误时,两个变量都将是 non-nil 。这意味着你最后依然会内存泄露。
通过在http响应错误处理中添加一个关闭 non-nil 响应主体的的调用来修复这个问题。另一个方法是使用一个 defer 调用来关闭所有失败和成功的请求的响应主体。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
resp.Body.Close() 的原始实现也会读取并丢弃剩余的响应主体数据。这确保了http的链接在keepalive http连接行为开启的情况下,可以被另一个请求复用。最新的http客户端的行为是不同的。现在读取并丢弃剩余的响应数据是你的职责。如果你不这么做,http的连接可能会关闭,而无法被重用。这个小技巧应该会写在Go 1.5的文档中。
如果http连接的重用对你的应用很重要,你可能需要在响应处理逻辑的后面添加像下面的代码:
_, err = io.Copy(ioutil.Discard, resp.Body)
如果你不立即读取整个响应将是必要的,这可能在你处理json API响应时会发生:
json.NewDecoder(resp.Body).Decode(&data)
关闭HTTP的连接
一些HTTP服务器保持会保持一段时间的网络连接(根据HTTP 1.1的说明和服务器端的“keep-alive”配置)。默认情况下,标准http库只在目标HTTP服务器要求关闭时才会关闭网络连接。这意味着你的应用在某些条件下消耗完sockets/file的描述符。
你可以通过设置请求变量中的 Close 域的值为 true ,来让http库在请求完成时关闭连接。
另一个选项是添加一个 Connection 的请求头,并设置为 close 。目标HTTP服务器应该也会响应一个 Connection: close 的头。当http库看到这个响应头时,它也将会关闭连接。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
req, err := http.NewRequest("GET","http://golang.org",nil)
if err != nil {
fmt.Println(err)
return
}
req.Close = true
//or do this:
//req.Header.Add("Connection", "close")
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(string(body)))
}
你也可以取消http的全局连接复用。你将需要为此创建一个自定义的http传输配置。
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
tr := &http.Transport{DisableKeepAlives: true}
client := &http.Client{Transport: tr}
resp, err := client.Get("http://golang.org")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(string(body)))
}
如果你向同一个HTTP服务器发送大量的请求,那么把保持网络连接的打开是没问题的。然而,如果你的应用在短时间内向大量不同的HTTP服务器发送一两个请求,那么在引用收到响应后立刻关闭网络连接是一个好主意。增加打开文件的限制数可能也是个好主意。当然,正确的选择源自于应用。
比较Structs, Arrays, Slices, and Maps
如果结构体中的各个元素都可以用你可以使用等号来比较的话,那就可以使用相号, == ,来比较结构体变量。
package main
import "fmt"
type data struct {
num int
fp float32
complex complex64
str string
char rune
yes bool
events <-chan string
handler interface{}
ref *byte
raw [10]byte
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2) //prints: v1 == v2: true
}
如果结构体中的元素无法比较,那使用等号将导致编译错误。注意数组仅在它们的数据元素可比较的情况下才可以比较。
package main
import "fmt"
type data struct {
num int //ok
checks [10]func() bool //not comparable
doit func() bool //not comparable
m map[string] string //not comparable
bytes []byte //not comparable
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",v1 == v2)
}
Go确实提供了一些助手函数,用于比较那些无法使用等号比较的变量。
最常用的方法是使用 reflect 包中的 DeepEqual() 函数。
package main
import (
"fmt"
"reflect"
)
type data struct {
num int //ok
checks [10]func() bool //not comparable
doit func() bool //not comparable
m map[string] string //not comparable
bytes []byte //not comparable
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true
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
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true
}
除了很慢(这个可能会也可能不会影响你的应用), DeepEqual() 也有其他自身的技巧。
package main
import (
"fmt"
"reflect"
)
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false
}
DeepEqual() 不会认为空的slice与“nil”的slice相等。这个行为与你使用 bytes.Equal() 函数的行为不同。 bytes.Equal() 认为“nil”和空的slice是相等的。
package main
import (
"fmt"
"bytes"
)
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",bytes.Equal(b1, b2)) //prints: b1 == b2: true
}
DeepEqual() 在比较slice时并不总是完美的。
package main
import (
"fmt"
"reflect"
"encoding/json"
)
func main() {
var str string = "one"
var in interface{} = "one"
fmt.Println("str == in:",str == in,reflect.DeepEqual(str, in))
//prints: str == in: true true
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)
}
如果你的byte slice(或者字符串)中包含文字数据,而当你要不区分大小写形式的值时(在使用 == , bytes.Equal() ,或者 bytes.Compare() ),你可能会尝试使用“bytes”和“string”包中的 ToUpper() 或者 ToLower() 函数。对于英语文本,这么做是没问题的,但对于许多其他的语言来说就不行了。这时应该使用 strings.EqualFold() 和 bytes.EqualFold() 。
如果你的byte slice中包含需要验证用户数据的隐私信息(比如,加密哈希、tokens等),不要使用 reflect.DeepEqual() 、 bytes.Equal() ,或者 bytes.Compare() ,因为这些函数将会让你的应用易于被定时攻击。为了避免泄露时间信息,使用 'crypto/subtle' 包中的函数(即, subtle.ConstantTimeCompare() )。
从Panic中恢复
recover() 函数可以用于获取/拦截panic。仅当在一个defer函数中被完成时,调用 recover() 将会完成这个小技巧。
Incorrect:
ackage main
import "fmt"
func main() {
recover() //doesn't do anything
panic("not good")
recover() //won't be executed :)
fmt.Println("ok")
}
Works:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("recovered:",recover())
}()
panic("not good")
}
recover() 的调用仅当它在defer函数中被直接调用时才有效。
Fails:
package main
import "fmt"
func doRecover() {
fmt.Println("recovered =>",recover()) //prints: recovered => <nil>
}
func main() {
defer func() {
doRecover() //panic is not recovered
}()
panic("not good")
}
在Slice, Array, and Map "range"语句中更新引用元素的值
在“range”语句中生成的数据的值是真实集合元素的拷贝。它们不是原有元素的引用。这意味着更新这些值将不会修改原来的数据。同时也意味着使用这些值的地址将不会得到原有数据的指针。
package main
import "fmt"
func main() {
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]
}
如果你需要更新原有集合中的数据,使用索引操作符来获得数据。
package main
import "fmt"
func main() {
data := []int{1,2,3}
for i,_ := range data {
data[i] *= 10
}
fmt.Println("data:",data) //prints data: [10 20 30]
}
如果你的集合保存的是指针,那规则会稍有不同。如果要更新原有记录指向的数据,你依然需要使用索引操作,但你可以使用 for range 语句中的第二个值来更新存储在目标位置的数据。
package main
import "fmt"
func main() {
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时,新的slice将引用原有slice的数组。如果你忘了这个行为的话,在你的应用分配大量临时的slice用于创建新的slice来引用原有数据的一小部分时,会导致难以预期的内存使用。
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
return raw[:3]
}
func main() {
data := get()
fmt.Println(len(data),cap(data),&data[0]) //prints: 3 10000 <byte_addr_x>
}
为了避免这个陷阱,你需要从临时的slice中拷贝数据(而不是重新划分slice)。
package main
import "fmt"
func get() []byte {
raw := make([]byte,10000)
fmt.Println(len(raw),cap(raw),&raw[0]) //prints: 10000 10000 <byte_addr_x>
res := make([]byte,3)
copy(res,raw[:3])
return res
}
func main() {
data := get()
fmt.Println(len(data),cap(data),&data[0]) //prints: 3 3 <byte_addr_y>
}
Slice的数据“毁坏”
比如说你需要重新一个路径(在slice中保存)。你通过修改第一个文件夹的名字,然后把名字合并来创建新的路劲,来重新划分指向各个文件夹的路径。
package main
import (
"fmt"
"bytes"
)
func main() {
path := []byte("AAAA/BBBBBBBBB")
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB
dir1 = append(dir1,"suffix"...)
path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})
fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB (not ok)
fmt.Println("new path =>",string(path)
|
请发表评论