10. 标准库

75 提供错误的time duration #

没有单位写法会带来误导.

ticker := time.NewTicker(1000)

附带单位

ticker := time.NewTicker(1000 * time.Nanosecond)

76 time.After内存泄漏 #

func f1(ch <-chan int) {

	for {
		select {
		case event := <-ch:
			fmt.Println(event)
		case <-time.After(time.Hour):
			log.Println("warning: no messages received")
		}
	}
}

每次走select分支都是新创建time.After,time.After里面包裹着chan且要等待1小时候释放.

修复问题


func f3(ch <-chan int) {

	for {
		ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
		select {
		case event := <-ch:
			cancel() // chan得到释放
			fmt.Println(event)
		case <-ctx.Done():
			log.Println("warning: no messages received")
		}
	}
}

或者最佳实践(因为变量复用)


func f4(ch <-chan int) {
	timeD := time.Hour
	timer := time.NewTimer(timeD)
	for {
		timer.Reset(timeD)
		select {
		case event := <-ch:
			fmt.Println(event)
		case <-timer.C:
			log.Println("warning: no messages received")
		}
	}
}

77 json处理 #

类型内嵌导致的意外

func json1() {
	event := Event{ID: 1234, Time: time.Now()}

	b, err := json.Marshal(event)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(string(b))
}

type Event struct {
	ID int
	time.Time
}
"2023-10-27T18:54:09.695182+08:00"

上文ID: 1234消失了?


因为time.Time也类似的实现了MarshalJSON() ([]byte, error) 这个接口. 影响了json.Marshal的行为

修复方法

都导出

type Event struct {
	ID   int
	Time time.Time
}

或者修改实现

func (e Event) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		ID   int `json:"id"`
		Time time.Time
	}{
		ID:   e.ID,
		Time: e.Time,
	})
}
时钟变化

time.Time包含wallmonotonic2种类型.

其中wall用于一天中确定时间. 比如network time protocol(NTP)中的时间.

测量持续时间时用的是monotonic

json例子:


type Event3 struct {
	Time time.Time
}

func run1() {
	t := time.Now()
	event1 := Event3{Time: t}

	b, err := json.Marshal(event1)
	if err != nil {
		fmt.Println(err)
		return
	}

	var event2 Event3
	err = json.Unmarshal(b, &event2)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(event1 == event2)
	fmt.Println(event1)
	fmt.Println(event2)
}
false
{2023-10-27 19:26:59.523668 +0800 CST m=+0.000127335}
{2023-10-27 19:26:59.523668 +0800 CST}

其中2023-10-27 19:26:59.523668 +0800 CST m=+0.000127335

  • wall指的是2023-10-27 19:26:59.523668 +0800 CST
  • monotonic指的是m=+0.000127335

时间通过json的encode decode后不相等,可用fmt.Println(event1.Time.Equal(event2.Time))判断

可以设置t.Truncate(0)剥离monotonic

    t := time.Now()
	event1 := Event3{
		Time: t.Truncate(0),
	}

文中还点到了time.Now().In(location)切换时区.

any时数值变成了float64
根据JSON规范,数值类型默认解码为float64

func f5() {
	var i int = 12 // int
	d, err := json.Marshal(i)
	if err != nil {
		fmt.Println(err)
	}
	var m any // any
	err = json.Unmarshal(d, &m)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("%T\n", m)
}
float64

78 SQL #

sql.Open("mysql", dsn)只验证参数,不创建连接

准备就绪后可用db.Ping()真正建立连接


连接池功能

  • SetMaxOpenConns
  • SetMaxIdleConns
  • SetConnMaxIdleTime
  • SetConnMaxLifetime,一般小于数据库的wait_timeout

SQL预编译

  • 提高性能,避免重复编译
  • 参数化SQL值安全性高
  • 方便语句重用

stmt, err := db.Prepare("SELECT * FROM ORDER WHERE ID = ?")

记住还有context传播函数,PrepareContextQueryContext


sql.NullString主要用来处理数据库表字段可能为空(NULL)的字符串(string)类型的数据.

类似处理null字段还有sql.NullBool 等等


rows遍历存在错误处理

rows, err := db.QueryContext(ctx, "select id, name from users")
if err != nil {
   // handle error
   return
}
defer rows.Close() 

for rows.Next() {
   var id int
   var name string

   err := rows.Scan(&id, &name)
   if err != nil {
      // handle scan error
      return 
   }

   // process row data
}

err = rows.Err()
if err != nil {
   // The query failed
   return
}

// No error, all good

执行查询错误 rows, err := db.QueryContext

关闭rows错误 err := rows.Close()

扫描rows错误 err := rows.Scan(&department, &age)

是为了判断结果集迭代是否正常完成,还是因错误而提前终止的 rows.Err()

79 忘记关闭资源 #

HTTP客户端

避免资源泄漏

resp, err := h.client.Post
if err != nil {
	return
}

defer func(){
// HTTP客户端请求后的body记得关闭.无论是否读取body
err=resp.Body.Close()
}

服务端则会自动执行,只是客户端需要关闭


关于无论如何客户端body都要执行关闭原因

  • 如果不读取body内容,直接关闭body,默认HTTP客户端可能会主动关闭连接
  • 如果读取了body的部分或全部内容后再关闭,默认HTTP客户端不会主动关闭连接

所以执行resp.Body.Close()是必要的保证


使用keep-alive时,即使不用body还是需要执行读取.

_, _ = io.Copy(io.Discard, resp.Body)

具体原因 https://go.dev/src/net/http/response.go#L63

The default HTTP client’s Transport may not

// reuse HTTP/1.x “keep-alive” TCP connections if the Body is

// not read to completion and closed.

为了复用 HTTP/1.x “keep-alive"的TCP链接,如果body没有读取完整关闭的情况下是无法做到的.

sql.Rows
rows, err := db.Query
if err!=nil{
	return
}
rows.Close() //不使用则会连接泄漏
os.File
f, err := os.OpenFile
if err!=nil{
return
}
f.Close() //记得

文件写入时f.Sync()可安全忽略f.Close()错误.

80 reply HTTP request后忘记return #

func Handler(w http.ResponseWriter, r *http.Request) {
  // 处理请求逻辑
  
  // 返回响应
  w.WriteHeader(200)
  w.Write([]byte("Hello World"))
  
  return  //记得return 防止后面还有逻辑
  
}

若迟迟不return 发生覆盖现象,出现重复的header头之类.

81 使用默认的HTTP端 #

客户端和服务端都有config配置,多个timeout参数,需要一一浏览.


客户端一般流程:

  • dial
  • TLS 握手
  • 请求
  • 读取header
  • 读取body

客户端timeout配置可以控制以上流程节点超时设置.

默认启用连接池,若主动禁止则http.Transport.DisableKeepAlives

连接空闲超时http.Transport.IdleConnTimeout

http.Transport.MaxIdleConns最大空闲数

http.Transport.MaxIdleConnsPerHost主机最大空闲连接数


服务端一般流程:

  • 等待客户端请求
  • TLS握手
  • 读取请求header
  • 读取body
  • 响应

服务端timeout配置可以控制以上流程节点超时设置.

comments powered by Disqus