程序默认不等所有 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)
}
如下,main() 主程序不等两个 goroutine 执行完就直接退出了:
常用解决办法:使用 "WaitGroup" 变量,它会让主程序等待所有 goroutine 执行完毕再退出。
如果你的 goroutine 要做消息的循环处理等耗时操作,可以向它们发送一条 kill 消息来关闭它们。或直接关闭一个它们都等待接收数据的 channel:
// 等待所有 goroutine 执行完毕
// 进入死锁
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doIt(i, done, wg)
}
close(done)
wg.Wait()
fmt.Println("all done!")
}
func doIt(workerID int, done <-chan struct{}, wg sync.WaitGroup) {
fmt.Printf("[%v] is running\n", workerID)
defer wg.Done()
<-done
fmt.Printf("[%v] is done\n", workerID)
}
执行结果:
看起来好像 goroutine 都执行完了,然而报错:
fatal error: all goroutines are asleep - deadlock!
为什么会发生死锁?goroutine 在退出前调用了 wg.Done() ,程序应该正常退出的。
原因是 goroutine 得到的 "WaitGroup" 变量是 var wg WaitGroup 的一份拷贝值,即 doIt() 传参只传值。所以哪怕在每个 goroutine 中都调用了 wg.Done(), 主程序中的 wg 变量并不会受到影响。
// 等待所有 goroutine 执行完毕
// 使用传址方式为 WaitGroup 变量传参
// 使用 channel 关闭 goroutine
func main() {
var wg sync.WaitGroup
done := make(chan struct{})
ch := make(chan interface{})
workerCount := 2
for i := 0; i < workerCount; i++ {
wg.Add(1)
go doIt(i, ch, done, &wg) // wg 传指针,doIt() 内部会改变 wg 的值
}
for i := 0; i < workerCount; i++ { // 向 ch 中发送数据,关闭 goroutine
ch <- i
}
close(done)
wg.Wait()
close(ch)
fmt.Println("all done!")
}
func doIt(workerID int, ch <-chan interface{}, done <-chan struct{}, wg *sync.WaitGroup) {
fmt.Printf("[%v] is running\n", workerID)
defer wg.Done()
for {
select {
case m := <-ch:
fmt.Printf("[%v] m => %v\n", workerID, m)
case <-done:
fmt.Printf("[%v] is done\n", workerID)
return
}
}
}
运行效果:
只有在数据被 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" // 不会被接收处理
}
运行效果:
从已关闭的 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) // 不能关闭,还有其他的 sender
time.Sleep(2 * time.Second) // 模拟做其他的操作
}
运行结果:
针对上边有 bug 的这个例子,可使用一个废弃 channel done 来告诉剩余的 goroutine 无需再向 ch 发送数据。此时 <- done 的结果是 {}:
func main() {
ch := make(chan int)
done := make(chan struct{})
for i := 0; i < 3; i++ {
go func(idx int) {
select {
case ch <- (idx + 1) * 2:
fmt.Println(idx, "Send result")
case <-done:
fmt.Println(idx, "Exiting")
}
}(i)
}
fmt.Println("Result: ", <-ch)
close(done)
time.Sleep(3 * time.Second)
}
运行效果:
在一个值为 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(2 * time.Second)
}
runtime 死锁错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]
利用这个死锁的特性,可以用在 select 中动态的打开和关闭 case 语句块:
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)
}
运行效果:
- 若函数 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)
}
运行结果:
- 关闭 HTTP 的响应体
使用 HTTP 标准库发起请求、获取响应时,即使你不从响应中读取任何数据或响应为空,都需要手动关闭响应体。新手很容易忘记手动关闭,或者写在了错误的位置:
// 请求失败造成 panic
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close() // resp 可能为 nil,不能读取 Body
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
func checkError(err error) {
if err != nil{
log.Fatalln(err)
}
}
上边的代码能正确发起请求,但是一旦请求失败,变量 resp 值为 nil,造成 panic:
panic: runtime error: invalid memory address or nil pointer dereference
应该先检查 HTTP 响应错误为 nil,再调用 resp.Body.Close() 来关闭响应体:
// 大多数情况正确的示例
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
checkError(err)
defer resp.Body.Close() // 绝大多数情况下的正确关闭方式
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
输出:
Get https://api.ipify.org?format=...: x509: certificate signed by unknown authority
绝大多数请求失败的情况下,resp 的值为 nil 且 err 为 non-nil。但如果你得到的是重定向错误,那它俩的值都是 non-nil,最后依旧可能发生内存泄露。2 个解决办法:
可以直接在处理 HTTP 响应错误的代码块中,直接关闭非 nil 的响应体。
手动调用 defer 来关闭响应体:
// 正确示例
func main() {
resp, err := http.Get("http://www.baidu.com")
// 关闭 resp.Body 的正确姿势
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
resp.Body.Close() 早先版本的实现是读取响应体的数据之后丢弃,保证了 keep-alive 的 HTTP 连接能重用处理不止一个请求。但 Go 的最新版本将读取并丢弃数据的任务交给了用户,如果你不处理,HTTP 连接可能会直接关闭而非重用,参考在 Go 1.5 版本文档。
如果程序大量重用 HTTP 长连接,你可能要在处理响应的逻辑代码中加入:
_, err = io.Copy(ioutil.Discard, resp.Body) // 手动丢弃读取完毕的数据
如果你需要完整读取响应,上边的代码是需要写的。比如在解码 API 的 JSON 响应数据:
json.NewDecoder(resp.Body).Decode(&data)
- 关闭 HTTP 连接
一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 "net/http" 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。解决办法有 2 个,请求结束后:
直接设置请求变量的 Close 字段值为 true,每次请求结束后就会主动关闭连接。
设置 Header 请求头部选项 Connection: close,然后服务器返回的响应头部也会有这个选项,此时 HTTP 标准库会主动断开连接。
// 主动关闭连接
func main() {
req, err := http.NewRequest("GET", "http://golang.org", nil)
checkError(err)
req.Close = true
//req.Header.Add("Connection", "close") // 等效的关闭方式
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(string(body))
}
你可以创建一个自定义配置的 HTTP transport 客户端,用来取消 HTTP 全局的复用连接:
func main() {
tr := http.Transport{DisableKeepAlives: true}
client := http.Client{Transport: &tr}
resp, err := client.Get("https://golang.google.cn/")
if resp != nil {
defer resp.Body.Close()
}
checkError(err)
fmt.Println(resp.StatusCode) // 200
body, err := ioutil.ReadAll(resp.Body)
checkError(err)
fmt.Println(len(string(body)))
}
根据需求选择使用场景:
- 若你的程序要向同一服务器发大量请求,使用默认的保持长连接。
- 若你的程序要连接大量的服务器,且每台服务器只请求一两次,那收到请求后直接关闭连接。或增加最大文件打开数 fs.file-max
的值。 - 将 JSON 中的数字解码为 interface 类型
在 encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理,比如下边的代码会造成 panic:
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
fmt.Printf("%T\n", result["status"]) // float64
var status = result["status"].(int) // 类型断言错误
fmt.Println("Status value: ", status)
}
panic: interface conversion: interface {} is float64, not int
如果你尝试 decode 的 JSON 字段是整型,你可以:
-
将 int 值转为 float 统一使用
- 将 decode 后需要的 float 值转为 int 使用
// 将 decode 的值转为 int 使用
func main() {
var data = []byte(`{"status": 200}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatalln(err)
}
var status = uint64(result["status"].(float64))
fmt.Println("Status value: ", status)
}
使用 Decoder 类型来 decode JSON 数据,明确表示字段的值类型
// 指定字段类型
func main() {
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 {
log.Fatalln(err)
}
var status, _ = result["status"].(json.Number).Int64()
fmt.Println("Status value: ", status)
}
// 你可以使用 string 来存储数值数据,在 decode 时再决定按 int 还是 float 使用
// 将数据转为 decode 为 string
func main() {
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 {
log.Fatalln(err)
}
var status uint64
err := json.Unmarshal([]byte(result["status"].(json.Number).String()), &status);
checkError(err)
fmt.Println("Status value: ", status)
}
- 使用 struct 类型将你需要的数据映射为数值型
// struct 中指定字段类型
func main() {
var data = []byte(`{"status": 200}`)
var result struct {
Status uint64 `json:"status"`
}
err := json.NewDecoder(bytes.NewReader(data)).Decode(&result)
checkError(err)
fmt.Printf("Result: %+v", result)
}
- 可以使用 struct 将数值类型映射为 json.RawMessage 原生数据类型
适用于如果 JSON 数据不着急 decode 或 JSON 某个字段的值类型不固定等情况:
// 状态名称可能是 int 也可能是 string,指定为 json.RawMessage 类型
func main() {
records := [][]byte{
[]byte(`{"status":200, "tag":"one"}`),
[]byte(`{"status":"ok", "tag":"two"}`),
}
for idx, record := range records {
var result struct {
StatusCode uint64
StatusName string
Status json.RawMessage `json:"status"`
Tag string `json:"tag"`
}
err := json.NewDecoder(bytes.NewReader(record)).Decode(&result)
checkError(err)
var name string
err = json.Unmarshal(result.Status, &name)
if err == nil {
result.StatusName = name
}
var code uint64
err = json.Unmarshal(result.Status, &code)
if err == nil {
result.StatusCode = code
}
fmt.Printf("[%v] result => %+v\n", idx, result)
}
}
- struct、array、slice 和 map 的值比较
可以使用相等运算符 == 来比较结构体变量,前提是两个结构体的成员都是可比较的类型:
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) // true
}
如果两个结构体中有任意成员是不可比较的,将会造成编译错误。注意数组成员只有在数组元素可比较时候才可比较。
type data struct {
num int
checks [10]func() bool // 无法比较
doIt func() bool // 无法比较
m map[string]string // 无法比较
bytes []byte // 无法比较
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2: ", v1 == v2)
}
invalid operation: v1 == v2 (struct containing [10]func() bool cannot be compared)
Go 提供了一些库函数来比较那些无法使用 == 比较的变量,比如使用 "reflect" 包的 DeepEqual() :
// 比较相等运算符无法比较的元素
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // true
m1 := map[string]string{"one": "a", "two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("v1 == v2: ", reflect.DeepEqual(m1, m2)) // true
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
// 注意两个 slice 相等,值和顺序必须一致
fmt.Println("v1 == v2: ", reflect.DeepEqual(s1, s2)) // true
}
这种比较方式可能比较慢,根据你的程序需求来使用。DeepEqual() 还有其他用法:
func main() {
var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2: ", reflect.DeepEqual(b1, b2)) // false
}
注意:
DeepEqual() 并不总适合于比较 slice
func main() {
var str = "one"
var in interface{} = "one"
fmt.Println("str == in: ", reflect.DeepEqual(str, in)) // true
v1 := []string{"one", "two"}
v2 := []string{"two", "one"}
fmt.Println("v1 == v2: ", reflect.DeepEqual(v1, v2)) // false
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)) // false
}
如果要大小写不敏感来比较 byte 或 string 中的英文文本,可以使用 "bytes" 或 "strings" 包的 ToUpper() 和 ToLower() 函数。比较其他语言的 byte 或 string,应使用 bytes.EqualFold() 和 strings.EqualFold()
如果 byte slice 中含有验证用户身份的数据(密文哈希、token 等),不应再使用 reflect.DeepEqual()、bytes.Equal()、 bytes.Compare()。这三个函数容易对程序造成 timing attacks,此时应使用 "crypto/subtle" 包中的 subtle.ConstantTimeCompare() 等函数
reflect.DeepEqual() 认为空 slice 与 nil slice 并不相等,但注意 byte.Equal() 会认为二者相等:
func main() {
var b1 []byte = nil
b2 := []byte{}
// b1 与 b2 长度相等、有相同的字节序
// nil 与 slice 在字节上是相同的
fmt.Println("b1 == b2: ", bytes.Equal(b1, b2)) // true
}
- 从 panic 中恢复
在一个 defer 延迟执行的函数中调用 recover() ,它便能捕捉 / 中断 panic
// 错误的 recover 调用示例
func main() {
recover() // 什么都不会捕捉
panic("not good") // 发生 panic,主程序退出
recover() // 不会被执行
println("ok")
}
// 正确的 recover 调用示例
func main() {
defer func() {
fmt.Println("recovered: ", recover())
}()
panic("not good")
}
从上边可以看出,recover() 仅在 defer 执行的函数中调用才会生效。
// 错误的调用示例
func main() {
defer func() {
doRecover()
}()
panic("not good")
}
func doRecover() {
fmt.Println("recobered: ", recover())
}
recobered: <nil> panic: not good
- 在 range 迭代 slice、array、map 时通过更新引用来更新元素
在 range 迭代中,得到的值其实是元素的一份值拷贝,更新拷贝并不会更改原来的元素,即是拷贝的地址并不是原有元素的地址:
func main() {
data := []int{1, 2, 3}
for _, v := range data {
v *= 10 // data 中原有元素是不会被修改的
}
fmt.Println("data: ", data) // data: [1 2 3]
}
如果要修改原有元素的值,应该使用索引直接访问:
func main() {
data := []int{1, 2, 3}
for i, v := range data {
data[i] = v * 10
}
fmt.Println("data: ", data) // data: [10 20 30]
}
如果你的集合保存的是指向值的指针,需稍作修改。依旧需要使用索引访问元素,不过可以使用 range 出来的元素直接更新原有值:
func main() {
data := []*struct{ num int }{{1}, {2}, {3},}
for _, v := range data {
v.num *= 10 // 直接使用指针更新
}
fmt.Println(data[0], data[1], data[2]) // &{10} &{20} &{30}
}
完整阅读,请点击以下:
Golang 新手可能会踩的 50 个坑(一)
Golang 新手可能会踩的 50 个坑(二)
Golang 新手可能会踩的 50 个坑(三)