使用Golang时遇到的一些坑


1、 【致命】不是所有Panic都能捕获

我们知道Golang给开发人员提供recover()机制,对堆栈异常(panic)进行捕获并自定义其处理逻辑。下面举个例子:

构造一个除0的异常场景:

代码:

package main

import “fmt”

func func1 () {

            defer func () {

                        if e : = recover () ; e ! = nil {

                                    fmt.Println(“Recover!”, e)

                        }

            } ()


            i : = 0

            fmt.Println(“Panic:”, 1/i)

}         


func main () {

            func1 ()

}                     


输出结果:

Recover! Runtime error:integer divide by zero

Process finished with exit code 0


我们看到程序正常退出,没有异常,说明recover()按照预期捕获到panic异常;但不是所有panic都能通过recover()捕捉到的,比如:并发操作map实例。

构造并发操作map的场景:

代码如下:

package main

import “fmt”

func func2() {

            defer func () {

                        if e : = recover() ; e ! = nil {

                                    fmt.Println(“Recover!”, e)

                        }

            } ()


            m : =make(map[i|nt]int)

            go func () {

                        for {

                                    m[0] = 0

                        }

            } ()

            for {

                        fmt.Println(“Panic:”,m[0])

            }

}

func main() {

            func2()

}         





输出结果:

Panic: 0


Fatal error: concurrent map read and map write


……


Process finished with exit code 2


以上结果可知,我们不能单纯依靠recover()解决函数内部所有panic异常,应该做到以下几点:

a) 通过编写代码校验,防止能预期到的panic,比如:空指针引用的指针判断。

b) 对于无法预期的panic,使用recover()捕获并加以处理。

c) 使用map时,必须要考虑是否存在并发读写场景,存在时,应使用ConcurrentMap组件或自己加sync.RWMutex进行加锁保护。

相关参考:

关于并发读写map导致的panic无法使用recover()捕获,是Go1.6增加的一个特性,https://golang.org/doc/go1.6#runtime;

当然这个并非是唯一一个无法通过recover()捕获的场景,还有可能Go本身的bug,https://github.com/golang/go/issues/21717;这个Bug在Go1.9.2才修复

2、 【严重】小心Map的内存泄露

大部分Golang程序做业务缓存实现时,都使用了map,看以下代码片段,简单模拟了这种使用场景:

代码如下:



Package main

import (

            “strings”

            “time”

            “runtime/debug”

)

func main() {

            m : = make(map[int]string)

            s : = strings.Repeat(“x”, 1024)

            for i : = 0; i < 10000000;i|++ {

                        m[i] = s

            }

            for        i : = 0;i < 10000000;i++{

                        delete(m, i)

            }

for {

                        debug.FreeOSMemory ()

                        time.Sleep(time.Second)

}

}



增加环境变量GODEBUG=gctrace=1,运行可见,即使代码逻辑清空了map,但进程内存使用并没有像预期那样“实报实销”:


应如何解决:定期替换成新的map,释放旧的map对象。


3、 【提示】不是每次Map遍历都能得到相同排序的集合

经常遇到一些业务场景,需要将map的所有元素打印输出,在Golang里面实现是非常简单的,一个for range就可以实现,但结果却有点出人意料:

呈上代码:

package main

import “fmt”

func main() {

            m := make(map[int]int)

            for i := 0; i < 100; i++ {

                        m[i] = i

            }

            for k := range m {

                        fmt.Print(k, “,”)

            }

            fmt.Println()

            for k := range m {

                        fmt.Print(k, “,”)

            }

            fmt.Println()

}

                       


输出结果:

37,45,48,63,77,97,0,……

57,65,2,49,43,44,53,……


Process finished with exit code 0


两次遍历的结果均不相同,这是为什么呢?这是Golang故意增加的一个随机数导致的,https://blog.golang.org/go-maps-in-action;所以如果对结果一致性有要求的业务逻辑,就不能简单的遍历map了,可以这样实现:

代码君:

import “sort”


var m map[int]string

var keys []int

for k := range m {

            keys = append(keys, k)

}

sort.Ints(keys)

for _, k := range keys {

            fmt.Println(“Key:”, k, “Value:”, m[k])

}


4、 【严重】客户端执行Response.Body.Close()后HTTP连接真的关闭了吗?

按照官方文档对标准库中的http client包的说明,在使用时需要主动调用Response.Body.Close()将连接关闭,但并不说明只要写了这句话就能关闭连接的。看看以下场景:



package main

import (

            “fmt”

            “net/http”

            “time”

)

func main() {

            cli := &http.Client{}

            for {

                        resp, err :=

cli.Get(“http://10.21.243.149:30100/health”)

             if err != nil {

                        fmt.Println(err)

                        return

            }

            fmt.Println(“status:”, resp.StatusCode)

            resp.Body.Close()

            time.Sleep(500*time.Millisecond)

}

}                     


执行后,统计了下连接数,发现大量TIME_WAIT连接没有按预期回收,看来Response.Body.Close()没生效。

D:\>netstat /n|find /c “30100”

234


我们把代码调整如下:



            fmt.Println(“status:”, resp.StatusCode)

            io.Copy(ioutil.Discard, resp.Body)  // add this

            resp.Body.Close()

            time.Sleep(500*time.Millisecond)



再看看连接数统计:

D:\>netstat /n|find /c “30100”

1


为什么?以上是个较为特殊的场景,业务只关心请求的响应码,并不关心响应体,所以没有加入读取resp.Body的代码。可以看到此时关闭Body读取数据通道,会导致Golang底层没有真正关闭连接。要解决这个这种场景出现的连接泄露问题,需要在Close前额外加入io.Copy(ioutil.Discard, resp.Body),来完成TCP响应体读取流程。

            手打勿喷。Go语言未来的前景很不错,而在国内的大厂中,华为云对此的支持还是可以的,在其微服务应用平台、微服务引擎中开放了Go语言的服务框架。

            目前,华为云有一个关于微服务的活动,有想应用服务微服务化需求的朋友可以考虑试用一下!

https://activity.huaweicloud.com/cse/index.html?dfk

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/31543630/viewspace-2156712/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/31543630/viewspace-2156712/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值