博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Go 语言常见的坑
阅读量:2287 次
发布时间:2019-05-09

本文共 4353 字,大约阅读时间需要 14 分钟。

点击上方蓝色“Golang来啦”关注我哟

加个“星标”,天天 15 分钟,掌握 Go 语言

via:

https://medium.com/better-programming/common-go-pitfalls-a92197cd96d2
作者:Tyler Finethy
四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!

原文如下:


我喜欢 Go 语言有几个原因:

  1. 语言本身极其简洁(只有 25 个关键字);

  2. 能轻而易举地实现交叉编译;

  3. 天然支持创建可靠的 HTTP 服务器;

从根本上来讲,Go 是一种 boring 的语言,可能这就是为什么可以用它来开发一些诸如 Docker 和 Kubernetes 等很棒的项目,像 Cloudflare 等具有高性能和弹性要求的公司也正在使用它。

尽管上手很容易,但是有很多细节还是值得关注。如果你在不清楚的情况下编写代码,很可能会导致各种稀奇古怪的问题,并且很难发现和纠正错误。

下面会给大家列举一些常见错误,是在 review 生产代码时发现的。希望你再遇到相同问题时能轻松地解决。

HTTP 超时时间

HTTP 超时时间,其实在已经跟大家讨论过这个问题。但仍然值得再提一提,因为好的解决方案总是需要更多的时间思考的。

使用默认的 HTTP 客户端可以发出 HTTP 请求,为了说明问题,下面是一个使用 GET 请求访问 google.com 的例子:

package mainimport (    "io/ioutil"    "log"    "net/http")var (    c = &http.Client{})func main() {    req, err := http.NewRequest("GET", "google.com", nil)    if err != nil {        log.Fatal(err)    }    res, err := c.Do(req)    if err != nil {        log.Fatal(err)    }    defer res.Body.Close()    b, _ := ioutil.ReadAll(res.Body)    ...}

正如指出的,默认的 HTTP 客户端没有设置超时时间,这意味着请求有可能会被长时间挂起(ps:具体原因可以查看原文)

所以,解决这个问题最好的办法是什么呢?

&http.Client{Timeout: time.Minute},给 HTTP 客户端定义一个合理的超时时间。你也可以考虑给 HTTP 请求加上 context,这样做有几个好处:

  1. 有能力取消正在进行的 HTTP 请求;

  2. 为一些特殊请求指定超时时间;

第 2 个好处显得尤为重要,比如你知道有几个请求需要耗时很长时间,超过 1 个小时。但是你又不想每个请求都设置这么长的超时时间,你就可以只针对特殊请求设置比较长的超时时间。

上面的例子中,如果加上 context 代码会像下面这样:

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)defer cancel()req = req.WithContext(ctx)res, err := c.Do(req)...

请求时间如果超过了超时时间,c.Do() 调用就会返回 DeadlineExceeded 错误,可以很容易地处理错误或者重试。

数据库连接

我参与的每一个 Go 项目几乎都会出现数据库连接问题。我认为对刚入门 Go 语言的新手来说,有个难以绕过去的点,sql.DB 对象是并发安全的连接池,而不是单个数据库连接。这意味着连接使用完之后如果没有返还给进程池,会轻易导致连接数耗尽,甚至最后导致应用程序宕掉。

例如,数据库连接池包含打开和空闲连接,分别是通过下面这些选项设置的:

  • SetConnMaxLifetime,连接可以重用的最长时间;

  • SetMaxIdleConns,最大的空闲连接数量;

  • SetMaxOpenConns,最大的打开连接数量;

需要注意的是,即使你的最大打开连接数设置成 200,如果连接使用完不返还连接池,应用程序也有可能会耗尽数据库能接受的最大连接数,最后导致宕机、重启服务。你需要检查数据库设置,以确保正确设置了这些参数。

如果数据库没有设置这些参数,应用程序将轻而易举地耗尽数据库能接受的连接数。

让我们回到进程池的问题上,查询数据库之后,很多开发人员会忘记关闭 *sql.Rows 对象,这就会导致超出最大连接数限制,并导致死锁或者高延迟。下面给大家展示下类似的代码片段:

package mainimport (    "context"    "database/sql"    "fmt"    "log")var (    ctx context.Context    db  *sql.DB)func main() {    age := 27    ctx, cancel := context.WithTimeout(context.Background(), time.Minute)    defer cancel()    rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)    if err != nil {        log.Fatal(err)    }    for rows.Next() {        var name string        if err := rows.Scan(&name); err != nil {            log.Fatal(err)        }        fmt.Println(name)    }    ...}

相信你也注意到,正如能在 HTTP 请求上添加 context 一样,我们也可以在数据库查询时添加超时时间的 context。这没什么问题。

正如上面讨论的,我们需要关闭 rows 对象将连接返还给进程池,防止连接数超出。

rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)if err != nil {    log.Fatal(err)}defer rows.Close()

如果在函数或者包之间传递数据库连接,尤其难以发现这一点。

goroutine 或者内存泄漏

最后一个要讨论的常见问题是 goroutine 泄漏,一般这个问题难以发现,但通常是由开发人员的错误引起的。

使用 channel 时通常会发生这种问题,比如:

package mainfunc main() {    c := make(chan error)    go func() {        for err := range c {            if err != nil {                panic(err)            }        }    }()    c <- someFunc()    ...}

如果我们不关闭通道 c 或者 someFunc() 不返回错误,我们初始化的 goroutine 将会挂起直到程序终止。

我们不可能找出每一个导致 goroutine 泄漏的 地方,我通常采用两种方法来检测和消除它们。

第一种方法是在单元测试方法里使用探测器,比如使用 Uber 开源的 goleak 库,就像下面这个例子一样:

func TestA(t *testing.T) {    defer goleak.VerifyNone(t)    // test logic here.}

这段代码就会验证,在代码优美关闭 30s 之后是否还有多余的 goroutine 在运行。

另一种方法是在应用程序的运行实例上使用 Go profiler,并查看存活的 goroutine 数量。其中一种方法就是使用 net/http/pprof 库,并查看生成的火焰图。

就像下面这样使用它:

import _ "net/http/pprof"func someFunc() {    go func() {        log.Println(http.ListenAndServe("localhost:6060", nil))    }}

上面这段代码,pprof 占用 6060 端口,对于特别严重的泄漏,如果你刷新将会看到协程数量在增多;对于更多的一些微小泄漏问题,则需要查看 profile 发现具体的问题,profile 页面就像下面这样:

goroutine profile: total 392 @ 0x43cf10 0x44ca6b 0x980600 0x46b301#    0x9805ff    database/sql.(*DB).connectionCleaner+0x36f  /usr/local/go/src/database/sql/sql.go:9502 @ 0x43cf10 0x44ca6b 0x980b18 0x46b301#    0x980b17    database/sql.(*DB).connectionOpener+0xe7    /usr/local/go/src/database/sql/sql.go:10522 @ 0x43cf10 0x44ca6b 0x980c4b 0x46b301#    0x980c4a    database/sql.(*DB).connectionResetter+0xfa  /usr/local/go/src/database/sql/sql.go:1065...

如果你的应用程序是空闲的,但是你又看见大数据量的 goroutine,这说明程序已经有问题了。确认泄漏位置之后,我仍然建议在单元测试中使用探测器,以确保解决问题。

总结

希望上面讨论的这些常见错误,如果以后你也遇到,可以帮助你更快地识别并完美地解决问题。

推荐阅读:


这是持续翻译的第 14/100 篇优质文章。

如果你有想交流的话题,欢迎留言。


如果我的文章对你有所帮助,点赞、转发都是一种支持!

给个[在看],是对四哥最大的支持

转载地址:http://pyfnb.baihongyu.com/

你可能感兴趣的文章
MySQL主从同步
查看>>
MySQL半同步复制
查看>>
MySQL主库宕机从库提权
查看>>
MySQL主主模式
查看>>
MySQL错误代码
查看>>
MySQL binlog的三种模式
查看>>
MySQL利用binlog增量恢复数据库
查看>>
Tomcat多实例多应用
查看>>
Tomcat启动慢解决方法
查看>>
Tomca主配置文件详解
查看>>
Tomcat创建虚拟主机
查看>>
Tomcat集群
查看>>
Tomcat DeltaManager集群共享session
查看>>
Tomcat连接Apache之mod_proxy模块
查看>>
sersync+rsync数据同步
查看>>
使用com.aspose.words将word模板转为PDF文件时乱码解决方法
查看>>
Linux发送邮件
查看>>
YUM安装PHP5.6
查看>>
YUM源安装MySQL5.7
查看>>
Tomcat日志切割cronolog
查看>>