《100 Go Mistakes and How to Avoid Them》之标准库篇
礼物说10.1 提供了错误的time.Duration值
很多的标准库提供通用方法接收类型为time.Duration的参数,然而time.Duration只是int64
的一种别名类型,新开发者很容易会陷入误区,比如java开发者会传递一个毫秒数。
:= time.NewTicker(1000)
ticker for {
select {
case <-ticker.C:
// Do something
}
}
上面的例子会启动一个1000纳秒的定时器,正常使用应该是ticker = time.NewTicker(1000 * time.Millisecond)
10.2 time.After内存泄漏
time.After
功能是在x时间后会接受到一条channel信息,经常用在并发场景中。有时候在循环中调用了time.After
会引起内存泄漏。我们下面举个场景:
func consumer(ch <-chan Event) {
for {
select {
case event := <-ch:
(event)
handlecase <-time.After(time.Hour):
.Println("warning: no messages received")
log}
}
}
上面是一种消费场景,期望如果一小时都没有接收到事件则warn。这样的代码看似没问题。实际上会引起内存泄漏。 time.After
会返回一个channel,我们可能期望在循环结束后channel会关闭,但实际不是。time.After创建的资源直到时间结束才会回收,而每一次的调用会占用200b的内存。所以如果我们一小时接收500w的消息,那就会产生1GB的内存泄漏。
解决方案之一是使用ctx,然后接收ctx.Done().这种方式的缺点是会在每次迭代中重复创建ctx。
func consumer(ch <-chan Event) {
for {
, cancel := context.WithTimeout(context.Background(), time.Hour)
ctxselect {
case event := <-ch:
()
cancel(event)
handlecase <-ctx.Done():
.Println("warning: no messages received")
log}
}
}
另一种解决方案是使用time.NewTimer
,创建一个全局的定时器,在每次迭代前调用Reset
方法重置。同时他也提供了Stop
方法可以在其他使用场景使用.
func consumer(ch <-chan Event) {
:= 1 * time.Hour
timerDuration := time.NewTimer(timerDuration)
timer
for {
.Reset(timerDuration)
timerselect {
case event := <-ch:
(event)
handlecase <-timer.C:
.Println("warning: no messages received")
log}
}
}
10.3 常见的json操作错误
10.3.1 类型内嵌1上的异常行为
我们先举个例子。
type Event struct {
int
ID .Time // 内嵌类型
time}
:= Event{
event : 1234,
ID: time.Now(),
Time}
, err := json.Marshal(event)
bif err != nil {
return err
}
.Println(string(b)) fmt
结果是2021-05-18T21:15:08.381652+02:00
。与你心里的预期相符么,为什么ID字段消失了?要了解原因我们需要知道2个原理:
- 实现了
MarshalJSON()
方法在json.Marchal
时会优先使用。
type Marshaler interface {
() ([]byte, error)
MarshalJSON}
内嵌类型可以继承方法
我们可以直接使用
Event.Second()
等等time.Time的函数方法.
结果其实已经显而易见了,Event
继承了time.Time的MarshalJSON方法,所以json.Marshal
只处理了time.Time.
我们的解决方案可以为:
- 不使用 内嵌类型
type Event struct {
int
ID .Time
Time time}
- 实现自己的
MarshalJSON
方法
func (e Event) MarshalJSON() ([]byte, error) {
return json.Marshal(
struct {
int
ID .Time
Time time}{
: e.ID,
ID: e.Time,
Time},
)
}
10.3.2 JSON 以及 单调时钟2
当我们序列化/反序列化一个带有time.Time的结构体时,很容易出现一些比较上的错误。这是因为Go的Time涵盖了wall clock
(常见的时间)& monotonic clock
(单调时间,所谓单调,就是只会不停的往前增长,不受校时操作的影响,这个时间是自进程启动以来的秒数).本章节主要讲下两种时钟类型以及对于json的影响。
wall clock 表示当前的时间点,如果时钟是同步于NTP的,他会往前或者往后调整。我们没法使用wall clock去度量时间间隔。所以操作系统提供了第二种时钟类型:monotonic clock.保证了时间会一直往前走不会受到校时的影响。
我们看个例子:
type Event struct {
.Time
Time time}
func main() {
:= time.Now()
t := Event{Time: t}
event1 , err := json.Marshal(event1)
bif err != nil {
.Println(err)
fmt}
var event2 Event
= json.Unmarshal(b, &event2)
err if err != nil {
.Println(err)
fmt}
.Println(event1 == event2)
fmt.Println(event1.Time)
fmt.Println(event2.Time)
fmt}
会发现结果是:
false
2023-07-14 17:44:23.781872 +0800 CST m=+0.000096126
2023-07-14 17:44:23.781872 +0800 CST
Go中,time.Time
包含了两种时钟.
当我们反序列化json时,time.Time只包括wall time.所以会出现不一致的问题。解决方案我们可以使用:
.Println(event1.Time.Equal(event2.Time))
fmttrue
Equal
函数不会考虑monotonic time。但是这种方式只能比较time.Time字段,并不是Event结构体. 所以我们可以使用Truncate
函数。
:= Event{
event1 : t.Truncate(0),
Time}
Truncate
函数将时间调整某个精度的倍数。
10.3.3 Map中的any
当kv类型不确定时,我们会使用map[string]any
去接收json反序列化的结果。但里面也会有一些坑可能会导致panic。
:= getMessage()
b var m map[string]any
:= json.Unmarshal(b, &m)
err if err != nil {
return err
}
当我们参数为:
{
"id": 32,
"name": "foo"
}
可以得到结果 map[id:32 name:foo]
然后这里会有一个坑:任何的数字类型的值,无论是否有小数点,都会被转化成float64
类型。
.Printf("%T\n", m["id"])
fmtfloat64
10.4 常见的SQL错误
database/sql
提供了一系列的接口围绕SQL数据库,在使用的时候也会出现一些常见的错误。
10.4.1 忘记sql.Open并不需要和数据库建立连接
sql.Open
并不会建立与数据库之间的连接。 Open may just validate its arguments without creating a connection to the database.
在某些case下,我们想要在所有依赖都准备完成之后启动服务。也就是我们需要在sql.Open
之后确定数据库时可到达的,我们可以使用Ping
函数。
, err := sql.Open("mysql", dsn)
dbif err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
10.4.2 忘记连接池
我们需要了解Go中是如何处理数据库连接池的。sql.Open
返回一个*sql.DB
的结构体.这个结构体代表的不是单一的数据库连接,而是连接池。同样,使用合适的参数去配置连接池同样重要。
- SetMaxOpenConns
- SetMaxIdleConns
- SetConnMaxIdleTime
- SetConnMaxLifetime
10.4.3 不使用预编译语句
预编译语句是很多数据库都支持的特性,可以处理重复的SQL语句。内部SQL语句已经预编译了,根据数据参数来区分。这样有两个好处:
- 效率 语句不需要再编译
- 安全 可以防止SQL注入
, err := db.Prepare("SELECT * FROM ORDER WHERE ID = ?")
stmtif err != nil {
return err
}
, err := stmt.Query(id)
rows// ...
10.4.4 错误处理null值
, err := db.Query("SELECT DEP, AGE FROM EMP WHERE ID = ?", id)
rowsif err != nil {
return err
}
var (
string
department int
age )
for rows.Next() {
:= rows.Scan(&department, &age)
err if err != nil {
return err
}
// ...
}
上述例子当遇到Null值时,会报错:
2021/10/29 17:58:05 sql: Scan error on column index 0, name "DEPARTMENT": converting NULL to string is unsupported
第一种解决方式是使用一个字符串指针:
var department *string
,当遇到Null值,department
会是nil。
另一种方案是使用sql.NullXXX
类型,比如sql.NullString
:
var department sql.NullString
。sql.NullString
是字符串的一个封装,包括了2个导出字段:String
包含了字符串,Valid
代表是否为Null。
10.4.5 没有处理row的迭代错误
, err := db.QueryContext(ctx,sql)
rows...
for rows.Next() {
:= rows.Scan(&department, &age)
err if err != nil {
return "", 0, err
}
}
return
当我们迭代rows.Next()
时,我们同样不能遗漏error的情况。
for rows.Next() {
// ...
}
if err := rows.Err(); err != nil {
return "", 0, err
}
10.5 没有关闭临时资源
开发者在使用临时资源的时候一定要记得关闭,避免内存泄漏。实现了io.Closer
的结构体也同样如此。
10.5.1 HTTP body
如下述例子:
type handler struct {
.Client
client httpstring
url }
func (h handler) getBody() (string, error) {
, err := h.client.Get(h.url)
respif err != nil {
return "", err
}
, err := io.ReadAll(resp.Body)
bodyif err != nil {
return "", err
}
return string(body), nil
}
我们使用http.Get
,然后使用io.ReadAll
解析返回内容.看上去没什么问题,其实会有资源泄漏。 resp
的类型是*http.Response
。包含了io.ReadCloser
字段Body.如果http.Get
没有报错,那我们就需要关闭他。我们需要defer resp.Body.Close()
10.5.2 sql.Rows
sql.Rows
是一组SQL查询返回值的结构体,它实现了io.Closer
。我们需要defer rows.Close()
10.5.3 os.File
os.File
是打开文件描述符。一样需要关闭。
10.6 在HTTP请求后忘记return
语句
当我们处理HTTPhandler时,很容易忘记return
.比如下面的例子:
func handler(w http.ResponseWriter, req *http.Request) {
:= foo(req)
err if err != nil {
.Error(w, "foo", http.StatusInternalServerError)
http}
, _ = w.Write([]byte("all good"))
_.WriteHeader(http.StatusCreated)
w}
忘记了return
,遇到了error也会继续执行逻辑,最终返回all good
。所以我们需要知道http.Error
并不能停止逻辑执行,我们必须要手动终止返回。
10.7 使用默认的HTTP客户端和服务端
http
标准库提供了HTTP客户端以及服务端的实现。然而开发者很容易犯的一个通用错误:在生产环境使用默认的实现。
10.7.1 HTTP client
我们先定义下default client
是什么意思。当我们使用零值的http.Client
:
:= &http.Client{}
client , err := client.Get("https://golang.org/") resp
或者使用http.Get
函数:
, err := http.Get("https://golang.org/") resp
两种方式最终都会使用到DefaultClient
。
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}
那么使用默认客户端会有什么问题呢?首先,它不包括任何的超时。可能会导致请求永不结束,然后消耗完系统资源。
我们可以举个例子去复写这些超时控制字段
:= &http.Client{
client : 5 * time.Second,
Timeout: &http.Transport{
Transport: (&net.Dialer{
DialContext: time.Second,
Timeout}).DialContext,
: time.Second,
TLSHandshakeTimeout: time.Second,
ResponseHeaderTimeout},
}
其次,默认客户端的连接池也是被配置好的,在生产环境不一定适用。
10.7.2 HTTP server
默认的服务端也是一样,当我们使用http.Serve
,http.ListenAndServe
,http .ListenAndServeTLS
时,使用的都是默认http.Server
.
http.TimeoutHandler
会封装一个handler去限定时间,当超时发生,会返回503 Service Unavailable
,并且会cancel上下文。
PS: 我们故意忽略了http.Server.WriteTimeout
。在http.TimeoutHandler
出现后,并不是必须要用的。而且它使用起来比较复杂。超时发生后,它会关闭连接并且不返回HTTP code.他也不会去取消上下文,执行协程会一直执行。
当我们暴露站点给客户端时,最好的方式是设置http.Server.ReadHeaderTimeout
,并且使用http.TimeoutHandler
包装函数。举例:
:= &http.Server{
s : ":8080",
Addr: 500 * time.Millisecond,
ReadHeaderTimeout: 500 * time.Millisecond,
ReadTimeout: http.TimeoutHandler(handler, time.Second, "foo"),
Handler}