什么,这些常见的go坑你居然没见过?

144 阅读19分钟

序言

分享目标:针对go中经常出现的坑有一个意识。防止在平常coding中出现类似的错误,或者调试有bug时能迅速回忆起讲过的地方。不对原理进行过多的深入

为什么序号不按顺序: 其中小标题的序号代表原著的序号,方便大家快速找到对应原著中的位置,原著在底部参考书籍处。

Go 100mistakes选择其中容易犯和常见的错误一起讨论。

3 错误的初始化

不要依赖Go的init()顺序,Go是根据引入包名字母决定的初始化顺序,如果修改了包名则可能引发意想不到的错误。

一个包中可以定义多个init函数,在这种情况下,包内的init函数的执行顺序是基于源文件的字母顺序。例如,如果一个包包含一个a.go和b.go文件,并且文件中都有init函数,则首先执行的是a.go中的init函数。但是,我们不应该依赖包中init函数的执行顺序,因为这种做法很危险,像重命名源文件会导致顺序重排,从而可能影响执行顺序。

20 未理解slice长度和容量

  • 截断操作or直接赋值后底层会共用一个数组,直到其中一个发生扩容。
  • append可能会产生意料之外的结果。
func main() {
   s1 := make([]int, 3, 6)
   s2 := s1[1:3]
   s1[1] = 1
   print(s2)

   s2 = append(s2, 2)
   print(s2)
   s1 = append(s1, 3) // append的3会覆盖第7行
   print(s1)
   print(s2)
}

func print(s []int) {
   fmt.Printf("len=%d, cap=%d: %v\n", len(s), cap(s), s)
}

## print
len=2, cap=5: [1 0]
len=3, cap=5: [1 0 2]
len=4, cap=6: [0 1 0 3]
len=3, cap=5: [1 0 3]
1.image.png2. image.png
3. image.png4. image.png
  • 实际业务中传递切片的时候,如果有截断等操作要注意 append 产生的意料之外结果。
func main() {
    s := []int{1, 2, 3}
 
    f(s[:2])
    fmt.Println(s) // [1 2 10]
}
 
func f(s []int) {
    _ = append(s, 10)
}

可采取方案

  1. 深拷贝
func f(s1 []int) {
   _ = append(s1, 10)
}

func listing2() {
   s := []int{1, 2, 3}
   sCopy := make([]int, 2, 3)
   copy(sCopy, s)
   f(sCopy)
   result := append(sCopy, s[2])
   fmt.Println(result)
}
  1. 使用s[low:high:max]表达式,限制cap强制append时进行扩容
s := []int{1, 2, 3}
f(s[:2:2])
fmt.Println(s)

22 slice的nil和empty

// nil slice
var s []string
// empty slice
s = make([]string, 0)
  • 不管是nil还是还是empty slices 调用len(slice)都等于0
  • nil切片不会分配内存,空切片会分配内存
  • 如果能够确定最后返回的切片为空,则推荐使用 var s []string, 如果在初始化时已知道切片的长度,则采用make([]string,length)最好

24 正确的对slice使用copy函数

  • copy函数将源切片中的数据拷贝到目标切片时,拷贝的元素个数为下面两个长度中较小的一个。
func bad() {
   src := []int{0, 1, 2}
   var dst []int
   copy(dst, src)
   fmt.Println(dst)

   _ = src
   _ = dst
}
---- 输出
[]
---
func correct() {
   src := []int{0, 1, 2}
   dst := make([]int, len (src) )
   copy(dst, src)
   fmt.Println(dst)

   _ = src
   _ = dst
}
---- 输出
[0 1 2]
---

26 slices的内存泄漏

func getMessageType(msg []byte) []byte {    return msg[:5] }  func getMessageTypeWithCopy(msg []byte) []byte {    msgType := make([]byte, 5)    copy(msgType, msg)    return msgType }  func receiveMessage() []byte {    return make([]byte, 1_000_000_000) }  func printAlloc() {    var m runtime.MemStats    runtime.ReadMemStats(&m)    fmt.Printf("%d KB\n", m.Alloc/1024) }  func main() {    printAlloc()    msg := receiveMessage()    five := getMessageTypeWithCopy(msg)    printAlloc()    runtime.GC()    runtime.KeepAlive(five) // 保持对five的引用    printAlloc() }

使用 five := getMessageTypeWithCopy(msg) --- gc可回收
--- 输出
23567 KB
1000136 KB
23570 KB
----

替换成 five := getMessageType(msg) --- gc无法回收
123 KB
976697 KB
976698 KB

初始化的msg数组长度为1_000_000_000,取前5个元素返回。如果仍然保持msg[:5]的引用,会导致后面的所有元素都无法回收。

可采取解决方案:深拷贝

28 Maps的内存泄漏

详细文章:map泄漏 map底层

func main() {
   // Init
   n := 1_000_000
   m := make(map[int][128]byte)
   printAlloc()

   // Add elements
   for i := 0; i < n; i++ {
      m[i] = randBytes()
   }
   printAlloc()

   // Remove elements
   for i := 0; i < n; i++ {
      delete(m, i)
   }

   // End
   runtime.GC()
   printAlloc()
   runtime.KeepAlive(m)
}

func randBytes() [128]byte {
   return [128]byte{}
}

func printAlloc() {
   var m runtime.MemStats
   runtime.ReadMemStats(&m)
   fmt.Printf("%d MB\n", m.Alloc/1024/1024)
}

$ go run main2.go
0 MB
461 MB
293 MB

当删除了所有 kv 后,内存占用依然有 293 MB,这实际上是创建长度为 100w 的 map 所消耗的内存大小。map的buckets创建后就不会缩容。map中的buckets只会增加不会减少。

  • 当 val 大小 <= 128B 时,val 其实是直接放在 bucket 里的。 会把 bmap 标记为不含指针,这样可以避免 gc 时扫描整个 hmap。
  • 当 val 大小 >= 128B 时,val会转为指针存储。

解决:

  • 定期拷贝map到另一个map;
  • 将 val 类型改成指针,可以减少buckets占用的内存;

30 忽略了元素在range循环中被复制的事实

  • range循环会拷贝

    • 如果是结构体则会拷贝一份
    • 如果是指针则拷贝指针的地址
package main

import (
   "fmt"
   "strings"
)

type account struct {
   balance float32
}

func main() {
   accounts := createAccounts()
   for _, a := range accounts {
      a.balance += 1000
   }
   fmt.Println(accounts) // [{100} {200} {300}]

   accounts = createAccounts()
   for i := range accounts {
      accounts[i].balance += 1000
   }
   fmt.Println(accounts) // [{1100} {1200} {1300}]


   accounts = createAccounts()
   for i := 0; i < len(accounts); i++ {
      accounts[i].balance += 1000
   }
   fmt.Println(accounts) // [{1100} {1200} {1300}]

   accountsPtr := createAccountsPtr()
   for _, a := range accountsPtr {
      a.balance += 1000
   }
   printAccountsPtr(accountsPtr) // [{1100} {1200} {1300}]
}

func createAccounts() []account {
   return []account{
      {balance: 100.},
      {balance: 200.},
      {balance: 300.},
   }
}

func createAccountsPtr() []*account {
   return []*account{
      {balance: 100.},
      {balance: 200.},
      {balance: 300.},
   }
}

func printAccountsPtr(accounts []*account) {
   sb := strings.Builder{}
   sb.WriteString("[")
   s := make([]string, len(accounts))
   for i, account := range accounts {
      s[i] = fmt.Sprintf("{%.0f}", account.balance)
   }
   sb.WriteString(strings.Join(s, " "))
   sb.WriteString("]")
   fmt.Println(sb.String())
}

[{100} {200} {300}]
[{1100} {1200} {1300}]
[{1100} {1200} {1300}]
[{1100} {1200} {1300}]

31 忽略了range循环参数怎样工作的

Range slice

  • range循环在一开始就会拷贝一份range后面的参数
func main() {
   s1 := []int{0, 1, 2}
   for range s1 { // 不会死循环
      s1 = append(s1, 10)
   }

   s2 := []int{0, 1, 2}
   for i := 0; i < len(s2); i++ { // 死循环 
      s2 = append(s2, 10)
   }
}

Range channel

  • 由于range循环时ch已经是一份copy值了,尽管ch=ch2(25行)range循环依然迭代的ch1
package main

import "fmt"

func main() {
   ch1 := make(chan int, 3)
   go func() {
      ch1 <- 0
      ch1 <- 1
      ch1 <- 2
      close(ch1)
   }()

   ch2 := make(chan int, 3)
   go func() {
      ch2 <- 10
      ch2 <- 11
      ch2 <- 12
      close(ch2)
   }()

   ch := ch1
   for v := range ch {
      fmt.Println(v)
      ch = ch2
   }
}

--- 输出
0
1
2

Range array

package main

import "fmt"

func listing1() {
   a := [3]int{0, 1, 2}
   for i, v := range a {
      a[2] = 10
      if i == 2 {
         fmt.Println(v) // 2
      }
   }
}

func listing2() {
   a := [3]int{0, 1, 2}
   for i := range a {
      a[2] = 10
      if i == 2 {
         fmt.Println(a[2]) // 10
      }
   }
}

func listing3() {
   a := [3]int{0, 1, 2}
   for i, v := range &a {
      a[2] = 10
      if i == 2 {
         fmt.Println(v) // 10
      }
   }
}

  • range循环仅在循环开始之前通过复制(无论类型如何)对提供的表达式求值一次。 我们应该记住这种行为,以避免可能导致我们访问错误元素等常见错误。

32 忽略range循环指针的影响范围

package main

import "fmt"

type Customer struct {
   ID      string
   Balance float64
}

type Store struct {
   m map[string]*Customer
}

func main() {
   s := Store{
      m: make(map[string]*Customer),
   }
   s.storeCustomers([]Customer{
      {ID: "1", Balance: 10},
      {ID: "2", Balance: -10},
      {ID: "3", Balance: 0},
   })
   print(s.m)
}
// 是指针,所以map三次存储的都是同一个地址
func (s *Store) storeCustomers(customers []Customer) {
   for _, customer := range customers { // customer会进行拷贝导致存储都是同一个地址
      fmt.Printf("%p\n", &customer)
      s.m[customer.ID] = &customer
   }
}

func print(m map[string]*Customer) {
   for k, v := range m {
      fmt.Printf("key=%s, value=%#v\n", k, v)
   }
}

---- 打印
0x14000098018
0x14000098018
0x14000098018
key=2, value=&main.Customer{ID:"3", Balance:0}
key=3, value=&main.Customer{ID:"3", Balance:0}
key=1, value=&main.Customer{ID:"3", Balance:0}

33 map迭代过程中的错误假设

错误:

  • map循环是有序的
  • map迭代中插入元素
func listing1() {
   m := map[int]bool{
      0: true,
      1: false,
      2: true,
   }

   for k, v := range m {
      if v {
         m[10+k] = true
      }
   }

   fmt.Println(m)
}
--- 多次运行答案不一样
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true]
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true]
map[0:true 1:false 2:true 10:true 12:true 20:true]
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true 40:true]

不要在map迭代过程中进行插入

39 性能不高的字符串连接

  • string是不可变的,每次执行s += "xx"操作时会重新分配一片内存,字符串连接性能比较低
  • strings.Builder{}比较快,对于已经知道要拼接的长度时可使用Grow()预分配(与slice类似)
  • 在简单的字符串拼接上也可以使用s += "xx"的形式。毕竟strings.Builder的代码可读性不如s += "xx"
// 普通
func concat1(values []string) string {
   s := ""
   for _, value := range values {
      s += value
   }
   return s
}
// strings.Builder 但是没有预分配
func concat2(values []string) string {
   sb := strings.Builder{}
   for _, value := range values {
      _, _ = sb.WriteString(value)
   }
   return sb.String()
}
// strings.Builder 有预分配
func concat3(values []string) string {
   total := 0
   for i := 0; i < len(values); i++ {
      total += len(values[i])
   }

   sb := strings.Builder{}
   sb.Grow(total)
   for _, value := range values {
      _, _ = sb.WriteString(value)
   }
   return sb.String()
} 


--- 数组长度1000,每个string长度1000下的对比
BenchmarkConcatV1-4             16      72291485 ns/op
BenchmarkConcatV2-4           1188        878962 ns/op
BenchmarkConcatV3-4           5922        190340 ns/op

42 使用什么类型作为方法的接收者

package main

import "fmt"

type customer struct {
   balance float64
}

func (c customer) add(v float64) {
   c.balance += v
}

type customerPoint struct {
   balance float64
}

func (c *customerPoint) add(operation float64) {
   c.balance += operation
}

func main() {
   c := customer{balance: 100.}
   c.add(50.)
   fmt.Printf("balance: %.2f\n", c.balance) // 100

   cp := customerPoint{balance: 100.0}
   cp.add(50.0)
   fmt.Printf("balance: %.2f\n", cp.balance) // 150
}

必须为指针

  1. 当方法需要改变接受者时。如果接收者是slice,并且需要向slice中添加元素,必须选择指针作为接收者。
  2. 当接收者包含不能拷贝的字段时,例如,对于sync包中的字段,像sync.Mutex,只能选择指针作为接收者。

建议为指针

  1. 大对象时,使用指针相比值有更高的效率。通过benchmark测试可以评估

必须是值

  1. 不希望函数修改接收者情况

建议是值

  1. 当接收者是小的数组或者struct对象。并且不包含可以修改的字段。

43 从不使用命名返回值参数

// 命名返回
func f(a int) (b int) {
        b = a
        return
}
// 非命名返回
func f(a int) int {
        b := a
        return b
}
  • 使用命名返回值(主要提供代码可读性时)

    • 在大多数情况下,在接口中定义的上下文中使用有名参数可以提高代码可读性,而不会产生任何副作用
    •   // 不知道具体的经纬度对应哪个参数
        type locator interface {
                getCoordinates(address string) (float32, float32, error)
        }
      
        type locator interface {
                getCoordinates(address string) (lat, lng float32, err error)
        }
      
    • 在方法实现的上下文中,没有严格的规则,例如如果两个参数类型相同的时候,使用有名参数可以提高代码可读性。也可以利用初始化减少代码。
    •   func ReadFull(r io.Reader, buf []byte) (n int, err error) {
                for len(buf) > 0 && err == nil {
                        var nr int
                        nr, err = r.Read(buf)
                        n += nr
                        buf = buf[nr:]
                }
                return
        }
      

45 返回零接收器(nil != nil)

type TradeError struct {
   ErrNo   int64  `json:"err_no"`
   ErrTips string `json:"err_tips"`
}

func (err *TradeError) Error() string {
   return fmt.Sprintf("errNo:%d,errTips:%s", err.ErrNo, err.ErrTips)
}

func f() error {
   // do something
   var err *TradeError

   return err
}

func main() {
   if err := f(); err != nil {
      // do something
      fmt.Println("err != nil")
   } else {
      fmt.Println("err == nil")
   }
}

// 永远输出
err != nil

  • 对于接口只有值和类型都为nil才会判断为nil
  • 如果对任意接口已经赋值,判断接口是为 nil 值的逻辑一定为false

47 defer作用时如何计算求值

  • 立即对函数的参数求值,而不是在defer后面的语句执行完返回时才计算
func f1() error {
   var status string
   defer notify(status)
   defer incrementCounter(status)
   status = StatusSuccess
   return nil
}

func notify(status string) {
   fmt.Println("notify:", status) // notify: 
}

func incrementCounter(status string) {
   fmt.Println("increment:", status) // increment: 
}
--  输出
increment: 
notify: 

// 指针改变
func f2() error {
   var status string
   defer notifyPtr(&status)
   defer incrementCounterPtr(&status)
   status = StatusSuccess
   return nil
}
-- 
increment: success
notify: success

func notifyPtr(status *string) {
   fmt.Println("notify:", *status)
}

func incrementCounterPtr(status *string) {
   fmt.Println("increment:", *status)
}

// 闭包
func f3() error {
   var status string
   defer func() {
      notify(status)
      incrementCounter(status)
   }()
   status = StatusSuccess
   return nil
}
--
notify: success
increment: success

61 传递不合时宜的上下文

  • 上下文被取消,异步操作可能会意外停止
func TestCancel(t *testing.T) {
   ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
   for i := 0; i < 5; i++ {
      go httpGet(ctx)
   }
   defer cancel()
   time.Sleep(time.Second * 3)
}

func httpGet(ctx context.Context) {
   // Make a request, that will call the baidu homepage
   req, _ := http.NewRequest(http.MethodGet, "http://baidu.com", nil)
   // Associate the cancellable context we just created to the request
   req = req.WithContext(ctx)

   // Create a new HTTP client and execute the request
   client := &http.Client{}
   res, err := client.Do(req)

   // If the request failed, log to STDOUT
   if err != nil {
      fmt.Println("Request failed:", err) // Request failed: Get "http://baidu.com": context deadline exceeded
      return
   }


   fmt.Println("Response received, status code:", res.StatusCode)
}

62 谨慎使用goroutine和循环变量

// 打印结果无法预估
func listing1() {
   s := []int{1, 2, 3}

   for _, i := range s {
      go func() {
         fmt.Print(i)
      }()
   }
}
// 解决一:使用临时变量
func listing2() {
   s := []int{1, 2, 3}

   for _, i := range s {
      val := i
      go func() {
         fmt.Print(val)
      }()
   }
}
// 解决二:传递参数
func listing3() {
   s := []int{1, 2, 3}

   for _, i := range s {
      go func(val int) {
         fmt.Print(val)
      }(i)
   }
}

64 在使用select+channel时期望确定性的结果

  • select选择哪个通道是不确定的,会随机选择,不像switch语句是选择第一个匹配的。
package main

import (
   "fmt"
   "time"
)

func main() {
   messageCh := make(chan int, 10)
   disconnectCh := make(chan struct{})

   go listing1(messageCh, disconnectCh)

   for i := 0; i < 10; i++ {
      messageCh <- i
   }
   disconnectCh <- struct{}{}
   time.Sleep(10 * time.Millisecond)
}
// 无法保证消息处理完,才断开连接。
func listing1(messageCh <-chan int, disconnectCh chan struct{}) {
   for {
      select {
      case v := <-messageCh:
         fmt.Println(v)
      case <-disconnectCh:
         fmt.Println("disconnection, return")
         return
      }
   }
}
// 1. 强制消费完messageCh 再断开连接
func listing2(messageCh <-chan int, disconnectCh chan struct{}) {
   for {
      select {
      case v := <-messageCh:
         fmt.Println(v)
      case <-disconnectCh:
         for {
            select {
            case v := <-messageCh:
               fmt.Println(v)
            default:
               fmt.Println("disconnection, return")
               return
            }
         }
      }
   }
}
// 2. 定义无缓冲的messageCh,发送者会阻塞。
// 3. 只使用一个channel,定义结构体消息,其中一个字段表示数据,另一个字段表示断开连接的标识,
// 只要保证断开连接的消息是最后发送的即可。

65 通知类型通道应使用chan struct{}

通道(channel)是一种通过信号在goroutine之间进行通信的机制。信号可以有数据也可以没有数据。

该通道将在发生特定断开连接时发生通知,一种处理的思路是定义一个chan bool类型的通道,true的含义是断开连接,但是false似乎没有特别的意义,并且应该只会传递ture。那么此时就不需要一个特定的值来传递信息。可以使用chan struct{}类型,它占用的存储空间为零字节。

disconnectCh := make(chan bool)
// 测试 struct{} 占有内存空间
var s struct{}
fmt.Println(unsafe.Sizeof(s)) // 0

比如在go中想使用set,也可以通过定义map[string]struct{}实现。

68 字符串 格式化产生的副作用

  • %v 会调用自实现的String()方法

import (
   "fmt"
   "sync"
)

func main() {
   customer := Customer{}
   _ = customer.UpdateAge1(-1) 
}

type Customer struct {
   mutex sync.RWMutex
   id    string
   age   int
}

func (c *Customer) UpdateAge1(age int) error {
   c.mutex.Lock()
   defer c.mutex.Unlock()

   if age < 0 {
      return fmt.Errorf("age should be positive for customer %v", c) // %v调用String() 导致死锁
   }

   c.age = age
   return nil
}

func (c *Customer) String() string {
   c.mutex.RLock()
   defer c.mutex.RUnlock()
   return fmt.Sprintf("id %s, age %d", c.id, c.age)
}

71 错误使用sync.WaitGroup

  • Add操作必须在启动子goroutine之前,在父goroutine中执行完成,而Done操作必须在子goroutine内部执行完成。
// 错误方式
func listing1() {
   wg := sync.WaitGroup{}
   var v uint64

   for i := 0; i < 3; i++ {
      go func() {
         wg.Add(1)
         atomic.AddUint64(&v, 1)
         wg.Done()
      }()
   }

   wg.Wait()
   fmt.Println(v)
}
---
go run -race xxx.go 会捕获到存在数据竞争
---

正确使用方式:

func listing2() {
   wg := sync.WaitGroup{}
   var v uint64

   wg.Add(3)
   for i := 0; i < 3; i++ {
      go func() {
         atomic.AddUint64(&v, 1)
         wg.Done()
      }()
   }

   wg.Wait()
   fmt.Println(v)
}

func listing3() {
   wg := sync.WaitGroup{}
   var v uint64

   for i := 0; i < 3; i++ {
      wg.Add(1)
      go func() {
         atomic.AddUint64(&v, 1)
         wg.Done()
      }()
   }

   wg.Wait()
   fmt.Println(v)
}

73 errgroup使用

  • 多个goroutine处理时,需要感知其他goroutine存在的错误可以考虑采用errgroup。

例子:

  • 第一个调用在执行了1毫秒时返回了错误
  • 第二和第三个调用在执行了5秒时返回了结果或错误

在我们的例子中,如果有错误产生,返回一个错误即可,不需要返回所有的错误。所以,这里进行的第二和第三个调用是没有意义的,errgroup将取消上下文,从而取消其他goroutine,因此,我们不用等待后面其他goroutine在5秒后返回的错误,这也是使用errgroup的另一个优势.

func handler2(ctx context.Context, strs []string) ([]string, error) {
   results := make([]string, len(strs))
   g, ctx := errgroup.WithContext(ctx) // 可以取消上下文
   //g := errgroup.Group{} // 无法取消
   for i, circle := range strs {
      i := i
      str := circle
      g.Go(func() error {
         result, err := foo(ctx, str, i)
         if err != nil {
            return err
         }
         results[i] = result
         return nil
      })
   }

   if err := g.Wait(); err != nil {
      fmt.Println("err:", err)
      return nil, fmt.Errorf("handler return err, %w", err)
   }
   return results, nil
}

func foo(ctx context.Context, str string, i int) (string, error) {
   if i == 2 {
      return "", fmt.Errorf("i:%d, err", i)
   }
   time.Sleep(time.Second * 2)

   select {
   case <-ctx.Done(): // 可以监听到取消 从而退出
      return "", nil
   default:
      fmt.Println("no error")
      return str, nil
   }
}
func main() {
   strings, err := handler2(context.TODO(), []string{"1", "2", "3"})
   if err != nil {
      fmt.Println("", err.Error())
      return
   }
   fmt.Println(strings)
}

74 不要随意复制sync包中变量

  • 变量资源本身带状态且操作要配套的不能拷贝

sync包中不能拷贝的变量

  • sync.Cond
  • sync.Map
  • sync.Mutex
  • sync.RWMutex
  • sync.Once
  • sync.Pool
  • sync.WaitGroup

锁的复制有状态

func main() {
   type MyMutex struct {
      count int
      sync.Mutex
   }
   var mu MyMutex
   mu.Lock()
   var mu1 = mu //加锁后复制了一个新的Mutex出来,此时 mu1 跟 mu的锁状态一致,都是加锁的状态
   mu.count++
   mu.Unlock()
   mu1.Lock() // 已经是加锁状态重复加锁则报错
   mu1.count++
   mu1.Unlock()
   fmt.Println(mu.count, mu1.count)
}
func main() {
   counter := NewCounter()

   go func() {
      counter.Increment1("foo")
   }()
   go func() {
      counter.Increment1("bar")
   }()

   time.Sleep(10 * time.Millisecond)
}

type Counter struct {
   mu       sync.Mutex
   counters map[string]int
}

func NewCounter() Counter {
   return Counter{counters: map[string]int{}}
}

// 复制sync.Mutex 加的不是同一个锁
func (c Counter) Increment1(name string) {
   c.mu.Lock()
   defer c.mu.Unlock()
   c.counters[name]++ // 并发读写错误
}
//  go run -race xx.go 存在数据竞争

75 错误的duration时间值

func listing1() {
   ticker := time.NewTicker(1000)
   for {
      select {
      case <-ticker.C:
         fmt.Println("tick")
      }
   }
}

func listing2() {
   ticker := time.NewTicker(time.Microsecond * 100)
   for {
      select {
      case <-ticker.C:
         fmt.Println("tick")
      }
   }
}
  • time.Duration实际上是int64类型的别名,它的单位是纳秒。这里传的是1000纳秒,也就是1微秒。

76 time.After导致内存泄露

比如想实现一段时间内没接收到消息则....的场景

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

上述代码,即使ch通道有消息,在一小时内时一直走event := <-ch分支,但是每次都会运行time.After(time.Hour)而该代码每次申请通道资源大概会消耗200字节的内存。如果每小时收到500万条消息,那会消耗200Byte*5000000=1G的内存空间。

func After(d Duration) <-chan Time {
 return NewTimer(d).C
}

func NewTimer(d Duration) *Timer {
 c := make(chan Time, 1)
 t := &Timer{
  C: c,
  r: runtimeTimer{
   when: when(d),
   f:    sendTime,
   arg:  c,
  },
 }
 startTimer(&t.r)
 return t
}

建议版本

func consumer3(ch <-chan Event) {
   timerDuration := 1 * time.Hour
   timer := time.NewTimer(timerDuration)
   defer timer.Stop()
   for {
      timer.Reset(timerDuration)
      select {
      case event := <-ch:
         handle(event)
      case <-timer.C:
         log.Println("warning: no messages received")
      }
   }
}
  • 在循环中使用time.After并不是唯一可能导致内存泄露的原因,本质原因与重复调用的代码有关。

77 JSON处理常见问题

类型内嵌导致的 序列化 问题

func main() {
   type Event1 struct {
      ID int
      time.Time
   }
   
   event := Event1{
      ID:   1234,
      Time: time.Now(),
   }

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

   fmt.Println(string(b))
   return
}
-- 
"2023-02-02T00:04:28.103983+08:00". ID不见了
---
  • time.Time其中实现了json.Marshaler接口的MarshalJSON方法,故改变了序列化结果

因单调时钟导致的 序列化 问题 (略)

type Time struct {
 // wall and ext encode the wall time seconds, wall time nanoseconds,
 // and optional monotonic clock reading in nanoseconds.
 //
 // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
 // a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
 // The nanoseconds field is in the range [0, 999999999].
 // If the hasMonotonic bit is 0, then the 33-bit field must be zero
 // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
 // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
 // unsigned wall seconds since Jan 1 year 1885, and ext holds a
 // signed 64-bit monotonic clock reading, nanoseconds since process start.
 wall uint64
 ext  int64

 // loc specifies the Location that should be used to
 // determine the minute, hour, month, day, and year
 // that correspond to this Time.
 // The nil location means UTC.
 // All UTC times are represented with loc==nil, never loc==&utcLoc.
 loc *Location
}

wall和ext共同记录了时间,但是分为两种情况,一种是没有记录单调时钟(比如是通过字符串解析得到的时间),另一种是记录了单调时钟(比如通过Now)。

func main() {
   type Event struct {
      Time time.Time
   }
   // 通过time.Now()
   t := time.Now()
   event1 := Event{
      Time: t,
   }

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

   var event2 Event
   err = json.Unmarshal(b, &event2) // 通过序列号生成
   if err != nil {
      return
   }

   fmt.Println(event1 == event2)

   fmt.Println(event1.Time)
   fmt.Println(event2.Time)
   return
}
----
false
2023-02-02 00:10:08.881853 +0800 CST m=+0.000104626
------------------------------------ --------------
             Wall time               Monotonic time
             
2023-02-02 00:10:08.881853 +0800 CST
---

序列化 数值到map[T]interface{}存在的问题

  • 任何数值,当将它通过JSON反序列化到一个map中时,无论数值是否包含小数,都将被转化为float64类型
  • 生产代码中真实踩坑记录
func listing1() error {
   b := getMessage()
   var m map[string]any
   err := json.Unmarshal(b, &m)
   if err != nil {
      return err
   }
   fmt.Printf("%T\n", m["id"]) // float64
   if m["id"] == 32 {
      fmt.Println("id is int && id == 32") // 不输出
   } else if m["id"] == float64(32) {
      fmt.Println("id is float && id == float64(32)") // 输出此项  id is float && id == float64(32)

   }
   return nil
}

func getMessage() []byte {
   str := "{\n        "id": 32,\n        "name": "foo"\n}"
   return []byte(str)
}

func main() {
   listing1()
}

79 忘记关闭临时资源

  • 未及时调用需要关闭的临时资源,会造成资源泄露。
  • 比如http等请求时需要及时调用close()
func (h handler) getStatusCode(body io.Reader) (int, error) {
        resp, err := h.client.Post(h.url, "application/json", body)
        if err != nil {
                return 0, err
        }
        
        defer func() {
                err := resp.Body.Close()
                if err != nil {
                        log.Printf("failed to close response: %v\n", err)
                }
        }()

        return resp.StatusCode, nil
}
  • 不要将defer写在err检查之前,可能造成panic
func (h handler) getStatusCode(body io.Reader) (int, error) {
        resp, err := h.client.Post(h.url, "application/json", body)
        // err后resp为nil会造成panic
        defer func() {
                err := resp.Body.Close()
                if err != nil {
                        log.Printf("failed to close response: %v\n", err)
                }
        }()
        if err != nil {
                return 0, err
        }
        return resp.StatusCode, nil
}

参考文档

100 mistakes Github源码

Understanding Real-World Concurrency Bugs in Go

ctx引入的坑

go中占位符使用

深入理解fmt包

墙上时钟和单调时钟

文件描述符close问题