关于go:我为什么放弃Go语言

47次阅读

共计 28222 个字符,预计需要花费 71 分钟才能阅读完成。

腾小云导读

你在什么时候会产生“想要放弃用 Go 语言”的念头?兴许是在用 Go 开发过程中,接连不断踩坑的时候。本文作者提炼和总结《100 Go Mistakes and How to Avoid Them》里的精髓内容,并联合本身的工作教训,盘点了 Go 的常见典型谬误,撰写了这篇超全避坑指南。让咱们追随文章,一起重拾用 Go 的信念 \~

目录

1 留神 shadow 变量

2 慎用 init 函数

3 embed types 优缺点

4 Functional Options Pattern 传递参数

5 小心八进制整数

6 float 的精度问题

7 slice 相干留神点 slice 相干留神点

8 留神 range

9 留神 break 作用域

10 defer

11 string 相干

12 interface 类型返回的非 nil 问题

13 Error

14 happens before 保障

15 Context Values

16 应多关注 goroutine 何时进行

17 Channel

18 string format 带来的 dead lock

19 谬误应用 sync.WaitGroup

20 不要拷贝 sync 类型

21 time.After 内存泄露

22 HTTP body 遗记 Close 导致的泄露

23 Cache line

24 对于 False Sharing 造成的性能问题

25 内存对齐

26 逃逸剖析

27 byte slice 和 string 的转换优化

28 容器中的 GOMAXPROCS

29 总结

01、留神 shadow 变量

var client *http.Client
  if tracing {client, err := createClientWithTracing()
    if err != nil {return err}
    log.Println(client)
  } else {client, err := createDefaultClient()
    if err != nil {return err}
    log.Println(client)  }

在下面这段代码中,申明了一个 client 变量,而后应用 tracing 控制变量的初始化,可能是因为没有申明 err 的缘故,应用的是 := 进行初始化,那么会导致 外层的 client 变量永远是 nil。这个例子实际上是很容易产生在咱们理论的开发中,尤其须要留神。

如果是因为 err 没有初始化的缘故,咱们在初始化的时候能够这么做:

var client *http.Client
  var err error
  if tracing {client, err = createClientWithTracing() 
  } else {...}
    if err != nil { // 避免反复代码
        return err    }

或者内层的变量申明换一个变量名字,这样就不容易出错了。

咱们也能够应用工具剖析代码是否有 shadow,先装置一下工具:

go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow

而后应用 shadow 命令:

go vet -vettool=C:\Users\luozhiyun\go\bin\shadow.exe .\main.go
# command-line-arguments
.\main.go:15:3: declaration of "client" shadows declaration at line 13
.\main.go:21:3: declaration of "client" shadows declaration at line 13

02、慎用 init 函数

应用 init 函数之前须要留神上面几件事:

2.1 init 函数会在全局变量之后被执行

init 函数并不是最先被执行的,如果申明了 const 或全局变量,那么 init 函数会在它们之后执行:

package main

import "fmt"

var a = func() int {fmt.Println("a")
  return 0
}()

func init() {fmt.Println("init")
}

func main() {fmt.Println("main")
}

// output
a
initmain

2.2 init 初始化按解析的依赖关系程序执行

比方 main 包外面有 init 函数,依赖了 redis 包,main 函数执行了 redis 包的 Store 函数,恰好 redis 包外面也有 init 函数,那么执行程序会是:

还有一种状况,如果是应用 “import \_ foo” 这种形式引入的,也是会先调用 foo 包中的 init 函数。

2.3 扰乱单元测试

比方咱们在 init 函数中初始了一个全局的变量,然而单测中并不需要,那么实际上会减少单测得复杂度,比方:

var db *sql.DB
func init(){dataSourceName := os.Getenv("MYSQL_DATA_SOURCE_NAME")
    d, err := sql.Open("mysql", dataSourceName)
    if err != nil {log.Panic(err)
    }
    db = d}

在下面这个例子中 init 函数初始化了一个 db 全局变量,那么在单测的时候也会初始化一个这样的变量,然而很多单测其实是很简略的,并不需要依赖这个货色。

03、embed types 优缺点

embed types 指的是咱们在 struct 外面定义的匿名的字段,如:

type Foo struct {Bar}
type Bar struct {Baz int}

那么在下面这个例子中,咱们能够通过 Foo.Baz 间接拜访到成员变量,当然也能够通过 Foo.Bar.Baz 拜访。

这样在很多时候能够减少咱们应用的便捷性,如果没有应用 embed types 那么可能须要很多代码,如下:

type Logger struct {writeCloser io.WriteCloser}

func (l Logger) Write(p []byte) (int, error) {return l.writeCloser.Write(p)
}

func (l Logger) Close() error {return l.writeCloser.Close()
}

func main() {l := Logger{writeCloser: os.Stdout}
        _, _ = l.Write([]byte("foo"))
        _ = l.Close()}

如果应用了 embed types 咱们的代码能够变得很简洁

type Logger struct {io.WriteCloser}

func main() {l := Logger{WriteCloser: os.Stdout}
        _, _ = l.Write([]byte("foo"))
        _ = l.Close()}

然而同样它也有毛病,有些字段咱们并不想 export,然而 embed types 可能给咱们带进来,例如:

type InMem struct {
  sync.Mutex
  m map[string]int
}

func New() *InMem {return &InMem{m: make(map[string]int)}}

Mutex 个别并不想 export,只想在 InMem 本人的函数中应用,如:

func (i *InMem) Get(key string) (int, bool) {i.Lock()
  v, contains := i.m[key]
  i.Unlock()
  return v, contains}

然而这么写却能够让拿到 InMem 类型的变量都能够应用它外面的 Lock 办法:

m := inmem.New()
m.Lock() // ??

04、Functional Options Pattern 传递参数

这种办法在很多 Go 开源库都有看到过应用,比方 zap、GRPC 等。

它常常用在须要传递和初始化校验参数列表的时候应用,比方咱们当初须要初始化一个 HTTP server,外面可能蕴含了 port、timeout 等等信息,然而参数列表很多,不能间接写在函数上,并且咱们要满足灵便配置的要求,毕竟不是每个 server 都须要很多参数。那么咱们能够:

设置一个不导出的 struct 叫 options,用来寄存配置参数;创立一个类型 type Option func(options *options) error,用这个类型来作为返回值;

比方咱们当初要给 HTTP server 外面设置一个 port 参数,那么咱们能够这么申明一个 WithPort 函数,返回 Option 类型的闭包,当这个闭包执行的时候会将 options 的 port 填充进去:

type options struct {port *int}

type Option func(options *options) error

func WithPort(port int) Option {
         // 所有的类型校验,赋值,初始化啥的都能够放到这个闭包外面做
        return func(options *options) error {
                if port < 0 {return errors.New("port should be positive")
                }
                options.port = &port
                return nil
        }}

如果咱们当初有一个这样的 Option 函数集,除了下面的 port 以外,还能够填充 timeout 等。而后咱们能够利用 NewServer 创立咱们的 server:

func NewServer(addr string, opts ...Option) (*http.Server, error) {
        var options options
        // 遍历所有的 Option
        for _, opt := range opts {
                // 执行闭包
                err := opt(&options)
                if err != nil {return nil, err}
        }

        // 接下来能够填充咱们的业务逻辑,比方这里设置默认的 port 等等
        var port int
        if options.port == nil {port = defaultHTTPPort} else {
                if *options.port == 0 {port = randomPort()
                } else {port = *options.port}
        }

        // ...}

初始化 server:

server, err := httplib.NewServer("localhost", 
               httplib.WithPort(8080),                httplib.WithTimeout(time.Second))

这样写的话就比拟灵便,如果只想生成一个简略的 server,咱们的代码能够变得很简略:

server, err := httplib.NewServer("localhost")

05、小心八进制整数

比方上面例子:

sum := 100 + 010  fmt.Println(sum)

你认为要输入 110,其实输入的是 108,因为 在 Go 中以 0 结尾的整数示意八进制。

它常常用在解决 Linux 权限相干的代码上,如上面关上一个文件:

file, err := os.OpenFile("foo", os.O_RDONLY, 0644)

所以为了可读性,咱们在用八进制的时候最好应用 “0o” 的形式示意,比方下面这段代码能够示意为:

file, err := os.OpenFile("foo", os.O_RDONLY, 0o644)

06、float 的精度问题

在 Go 中浮点数示意形式和其余语言一样,都是通过迷信计数法示意,float 在存储中分为三局部:

符号位(Sign): 0 代表正,1 代表为负 指数位(Exponent): 用于存储迷信计数法中的指数数据,并且采纳移位存储 尾数局部(Mantissa):尾数局部

计算规定我就不在这里展现了,感兴趣的能够本人去查查,我这里说说这种计数法在 Go 外面会有哪些问题。

func f1(n int) float64 {
  result := 10_000.
  for i := 0; i < n; i++ {result += 1.0001}
  return result
}

func f2(n int) float64 {
  result := 0.
  for i := 0; i < n; i++ {result += 1.0001}
  return result + 10_000.}

在下面这段代码中,咱们简略地做了一下加法:

n Exact result f1 f2
10 10010.001 10010.001 10010.001
1k 11000.1 11000.1 11000.1
1m 1.01E+06 1.01E+06 1.01E+06

能够看到 n 越大,误差就越大,并且 f2 的误差是小于 f1 的。

对于乘法咱们能够做上面的试验:

a := 100000.001
b := 1.0001
c := 1.0002

fmt.Println(a * (b + c))fmt.Println(a*b + a*c)

输入:

200030.00200030004
200030.0020003

正确输入应该是 200030.0020003,所以它们实际上都有肯定的误差,然而能够看到 先乘再加精度失落会更小

如果想要精确计算浮点的话,能够尝试 “http://github.com/shopspring/decimal” 库,换成这个库咱们再来计算一下:

a := decimal.NewFromFloat(100000.001)
b := decimal.NewFromFloat(1.0001)
c := decimal.NewFromFloat(1.0002)

fmt.Println(a.Mul(b.Add(c))) //200030.0020003

07、slice 相干留神点

7.1 辨别 slice 的 length 和 capacity

首先让咱们初始化一个带有 length 和 capacity 的 slice:

s := make([]int, 3, 6)

在 make 函数外面,capacity 是可选的参数。下面这段代码咱们创立了一个 length 是 3,capacity 是 6 的 slice,那么底层的数据结构是这样的:

slice 的底层实际上指向了一个数组。当然,因为咱们的 length 是 3,所以这样设置 s[4] = 0 会 panic 的。须要应用 append 能力增加新元素。

panic: runtime error: index out of range [4] with length 3

当 appned 超过 cap 大小的时候,slice 会主动帮咱们扩容,在元素数量小于 1024 的时候每次会扩充一倍,当超过了 1024 个元素每次扩充 25%

有时候咱们会应用:操作符从另一个 slice 下面创立一个新切片:

s1 := make([]int, 3, 6)
s2 := s1[1:3]

实际上这两个 slice 还是指向了底层同样的数组,构如下:

因为指向了同一个数组,那么当咱们扭转第一个槽位的时候,比方 s1[1]=2,实际上两个 slice 的数据都会产生扭转:

然而当咱们应用 append 的时候状况会有所不同:

s2 = append(s2, 3)

fmt.Println(s1) // [0 2 0]
fmt.Println(s2) // [2 0 3]

s1 的 len 并没有被扭转,所以看到的还是 3 元素。

还有一件比拟乏味的细节是,如果再接着 append s1 那么第四个元素会被笼罩掉:

s1 = append(s1, 4)
  fmt.Println(s1) // [0 2 0 4]
  fmt.Println(s2) // [2 0 4]

咱们再持续 append s2 直到 s2 产生扩容,这个时候会发现 s2 实际上和 s1 指向的不是同一个数组了:

s2 = append(s2, 5, 6, 7)
fmt.Println(s1) //[0 2 0 4]
fmt.Println(s2) //[2 0 4 5 6 7]

除了下面这种状况,还有一种状况 append 会产生意想不到的成果:

s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3 := append(s2, 10)

如果 print 它们应该是这样:

s1=[1 2 10], s2=[2], s3=[2 10]

7.2 slice 初始化

对于 slice 的初始化实际上有很多种形式:

func main() {var s []string
        log(1, s)

        s = []string(nil)
        log(2, s)

        s = []string{}
        log(3, s)

        s = make([]string, 0)
        log(4, s)
}

func log(i int, s []string) {fmt.Printf("%d: empty=%t\tnil=%t\n", i, len(s) == 0, s == nil)}

输入:

1: empty=true   nil=true
2: empty=true   nil=true
3: empty=true   nil=false
4: empty=true   nil=false

前两种形式会创立一个 nil 的 slice,后两种会进行初始化,并且这些 slice 的大小都为 0。

对于 var s []string 这种形式来说,益处就是 不必做任何的内存调配。比方上面场景可能能够节俭一次内存调配:

func f() []string {var s []string
        if foo() {s = append(s, "foo")
        }
        if bar() {s = append(s, "bar")
        }
        return s}

对于 s := []string{} 这种形式来说,它 比拟适宜初始化一个已知元素的 slice

s := []string{"foo", "bar", "baz"}

如果没有这个需要其实用 var s []string 比拟好,反正在应用的适宜都是通过 append 增加元素,var s []string 还能节俭一次内存调配。

如果咱们初始化了一个空的 slice,那么 最好是应用 len(xxx) == 0 来判断 slice 是不是空的,如果应用 nil 来判断可能会永远非空的状况,因为对于 s := []string{} 和 s = make([]string, 0) 这两种初始化都是非 nil 的。

对于 []string(nil) 这种初始化的形式,应用场景很少,一种比拟不便地应用场景是用它来进行 slice 的 copy:

src := []int{0, 1, 2}
dst := append([]int(nil), src...)

对于 make 来说,它能够初始化 slice 的 length 和 capacity,如果咱们能确定 slice 外面会寄存多少元素,从性能的角度思考最好应用 make 初始化好,因为对于一个空的 slice append 元素进去每次达到阈值都须要进行扩容,上面是填充 100 万元素的 benchmark:

BenchmarkConvert_EmptySlice-4 22 49739882 ns/op
BenchmarkConvert_GivenCapacity-4 86 13438544 ns/op
BenchmarkConvert_GivenLength-4 91 12800411 ns/op

能够看到,如果咱们提前填充好 slice 的容量大小,性能是空 slice 的四倍,因为少了扩容时元素复制以及从新申请新数组的开销。

7.3 copy slice

src := []int{0, 1, 2}
var dst []int
copy(dst, src)
fmt.Println(dst) // []

应用 copy 函数 copy slice 的时候须要留神,下面这种状况实际上会 copy 失败,因为对 slice 来说是由 length 来管制可用数据,copy 并没有复制这个字段,要想 copy 咱们能够这么做:

src := []int{0, 1, 2}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst) //[0 1 2]

除此之外也能够用下面提到的:

src := []int{0, 1, 2}
dst := append([]int(nil), src...)

7.4 slice capacity 内存开释问题

先来看个例子:

type Foo struct {v []byte
}

func keepFirstTwoElementsOnly(foos []Foo) []Foo {return foos[:2]
}

func main() {foos := make([]Foo, 1_000)
  printAlloc()

  for i := 0; i < len(foos); i++ {foos[i] = Foo{v: make([]byte, 1024*1024),
    }
  }
  printAlloc()

  two := keepFirstTwoElementsOnly(foos)
  runtime.GC()
  printAlloc()
  runtime.KeepAlive(two)}

下面这个例子中应用 printAlloc 函数来打印内存占用:

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

下面 foos 初始化了 1000 个容量的 slice,外面 Foo struct 每个都持有 1M 内存的 slice,而后通过 keepFirstTwoElementsOnly 返回持有前两个元素的 Foo 切片,咱们的想法是手动执行 GC 之后其余的 998 个 Foo 会被 GC 销毁,然而输入后果如下:

387 KB
1024315 KB1024319 KB

实际上并没有,起因就是实际上 keepFirstTwoElementsOnly 返回的 slice 底层持有的数组是和 foos 持有的同一个:

所以咱们真的要只返回 slice 的前 2 个元素的话应该这样做:

func keepFirstTwoElementsOnly(foos []Foo) []Foo {res := make([]Foo, 2)
        copy(res, foos)
        return res}

不过下面这种办法会初始化一个新的 slice,而后将两个元素 copy 过来。不想进行多余的调配能够这么做:

func keepFirstTwoElementsOnly(foos []Foo) []Foo {for i := 2; i < len(foos); i++ {foos[i].v = nil
        }
        return foos[:2]}

08、留神 range

8.1 copy 的问题

应用 range 的时候如果咱们间接批改它返回的数据会不失效,因为返回的数据并不是原始数据:

type account struct {balance float32}
 
  accounts := []account{{balance: 100.},
    {balance: 200.},
    {balance: 300.},
  }
  for _, a := range accounts {a.balance += 1000}

如果像下面这么做,那么输入的 accounts 是:

[{100} {200} {300}]

所以咱们想要扭转 range 中的数据能够这么做:

for i := range accounts {accounts[i].balance += 1000}

range slice 的话也会 copy 一份:

s := []int{0, 1, 2}
for range s {s = append(s, 10) }

这份代码在 range 的时候会 copy 一份,因而只会调用三次 append 后进行。

8.2 指针问题

比如咱们想要 range slice 并将返回值存到 map 外面供前面业务应用,相似这样:

type Customer struct {
    ID string
    Balance float64
}

test := []Customer{{ID: "1", Balance: 10},
      {ID: "2", Balance: -10},
      {ID: "3", Balance: 0},
} 

var m map[string]*Customer
for _, customer := range test {m[customer.ID] = &customer}

然而这样遍历 map 外面存的并不是咱们想要的,你会发现存的 value 都是最初一个:

{"1":{"ID":"3","Balance":0},"2":{"ID":"3","Balance":0},"3":{"ID":"3","Balance":0}}

这是因为当咱们应用 range 遍历 slice 的时候,返回的 customer 变量实际上是一个固定的地址:

for _, customer := range test {fmt.Printf("%p\n", &customer) // 咱们想要获取这个指针的时候}

输入:

0x1400000e240
0x1400000e2400x1400000e240

这是因为迭代器会把数据都放入到 0x1400000e240 这块空间外面:

所以咱们能够这样在 range 外面获取指针:

for _, customer := range test {
    current := customer // 应用局部变量
    fmt.Printf("%p\n", &current) // 这里获取的指针是 range copy 进去元素的指针  }

或者:

for i := range test {current := &test[i] // 应用局部变量
    fmt.Printf("%p\n", current)  }

09、留神 break 作用域

比方说:

for i := 0; i < 5; i++ {fmt.Printf("%d", i)

      switch i {
      default:
      case 2:
              break
      }  }

下面这个代码原本想 break 进行遍历,实际上只是 break 了 switch 作用域,print 仍然会打印:0,1,2,3,4。

正确做法应该是通过 label 的形式 break:

loop:
  for i := 0; i < 5; i++ {fmt.Printf("%d", i) 
    switch i {
    default:
    case 2:
      break loop
    }  }

有时候咱们会没留神到本人的谬误用法,比方上面:

for {
    select {
    case <-ch:
      // Do something
    case <-ctx.Done():
      break
    }  }

下面这种写法会导致只跳出了 select,并没有终止 for 循环,正确写法应该这样:

loop:
  for {
    select {
    case <-ch:
      // Do something
    case <-ctx.Done():
      break loop
    }  }

10、defer

10.1 留神 defer 的调用机会

有时候咱们会像上面一样应用 defer 去敞开一些资源:

func readFiles(ch <-chan string) error {
            for path := range ch {file, err := os.Open(path)
                    if err != nil {return err}
    
                    defer file.Close()
    
                    // Do something with file
            }
            return nil}

因为 defer 会在办法完结的时候调用,然而 如果下面的 readFiles 函数永远没有 return,那么 defer 将永远不会被调用,从而造成内存泄露。并且 defer 写在 for 循环外面,编译器也无奈做优化,会影响代码执行性能。

为了防止这种状况,咱们能够 wrap 一层:

func readFiles(ch <-chan string) error {
      for path := range ch {if err := readFile(path); err != nil {return err} 
      }
      return nil
} 

func readFile(path string) error {file, err := os.Open(path)
      if err != nil {return err}

      defer file.Close()

      // Do something with file
      return nil}

10.2 留神 defer 的参数

defer 申明时会先计算确定参数的值。

func a() {
    i := 0
    defer notice(i) // 0
    i++
    return
}

func notice(i int) {fmt.Println(i)}

在这个例子中,变量 i 在 defer 被调用的时候就曾经确定了,而不是在 defer 执行的时候,所以下面的语句输入的是 0。

所以咱们想要获取这个变量的实在值,应该用援用:

func a() {
  i := 0
  defer notice(&i) // 1
  i++
  return}

10.2 defer 下的闭包

func a() int {
  i := 0
  defer func() {fmt.Println(i + 1) //12
  }()
  i++
  return i+10  
}

func TestA(t *testing.T) {fmt.Println(a()) //11}

如果换成闭包的话,实际上闭包中对变量 i 是通过 指针传递 ,所以能够读到实在的值。然而下面的例子中 a 函数返回的是 11 是因为执行程序是:

先计算(i+10)-> (call defer) -> (return)

11、string 相干

11.1 迭代带来的问题

在 Go 语言中,字符串是一种根本类型,默认是通过 utf8 编码的字符序列,当字符为 ASCII 码时则占用 1 个字节,其余字符依据须要占用 2-4 个字节,比方中文编码通常须要 3 个字节。

那么咱们在做 string 迭代的时候可能会产生意想不到的问题:

s := "hêllo"
  for i := range s {fmt.Printf("position %d: %c\n", i, s[i])
  }
  fmt.Printf("len=%d\n", len(s))

输入:

position 0: h
position 1: Ã
position 3: l
position 4: l
position 5: o
len=6

下面的输入中发现第二个字符是 Ã,不是 ê,并且地位 2 的输入”隐没“了,这其实就是因为 ê 在 utf8 外面实际上占用 2 个 byte:

s h ê l l o
[]byte(s) 68 c3 aa 6c 6c 6f

所以咱们在迭代的时候 s[1] 等于 c3 这个 byte 等价 Ã 这个 utf8 值,所以输入的是 hÃllo 而不是 hêllo。

那么依据下面的剖析,咱们就能够晓得在迭代获取字符的时候不能只获取单个 byte,应该应用 range 返回的 value 值:

s := "hêllo"
  for i, v := range s {fmt.Printf("position %d: %c\n", i, v)  }

或者咱们能够把 string 转成 rune 数组,在 go 中 rune 代表 Unicode 码位,用它能够输入单个字符:

s := "hêllo"
  runes := []rune(s)
  for i, _ := range runes {fmt.Printf("position %d: %c\n", i, runes[i])  }

输入:

position 0: h
position 1: ê
position 2: l
position 3: l
position 4: o

11.2 截断带来的问题

在下面咱们讲 slice 的时候也提到了,在对 slice 应用:操作符进行截断的时候,底层的数组实际上指向同一个,在 string 外面也须要留神这个问题,比方上面:

func (s store) handleLog(log string) error {if len(log) < 36 {return errors.New("log is not correctly formatted")
            }
            uuid := log[:36]
            s.store(uuid)
            // Do something    }

这段代码用了:操作符进行截断,然而如果 log 这个对象很大,比方下面的 store 办法把 uuid 始终存在内存里,可能会造成底层的数组始终不开释,从而造成内存泄露。

为了解决这个问题,咱们能够先复制一份再解决:

func (s store) handleLog(log string) error {if len(log) < 36 {return errors.New("log is not correctly formatted")
            }
            uuid := strings.Clone(log[:36]) // copy 一份
            s.store(uuid)
            // Do something    }

12、interface 类型返回的非 nil 问题

如果咱们想要继承 error 接口实现一个本人的 MultiError:

type MultiError struct {errs []string
}

func (m *MultiError) Add(err error) {m.errs = append(m.errs, err.Error())
}

func (m *MultiError) Error() string {return strings.Join(m.errs, ";")}

而后在应用的时候返回 error,并且想通过 error 是否为 nil 判断是否有谬误:

func Validate(age int, name string) error {
  var m *MultiError
  if age < 0 {m = &MultiError{}
    m.Add(errors.New("age is negative"))
  }
  if name == "" {
    if m == nil {m = &MultiError{}
    }
    m.Add(errors.New("name is nil"))
  }

  return m
}

func Test(t *testing.T) {if err := Validate(10, "a"); err != nil {t.Errorf("invalid")
  }}

实际上 Validate 返回的 err 会总是为 非 nil 的,也就是下面代码只会输入 invalid:

invalid <nil>

13、Error

13.1 error wrap

对于 err 的 return 咱们个别能够这么解决:

err:= xxx()
 if err != nil {return err}

然而这样解决只是简略地将原始的谬误抛出去了,无奈晓得以后解决的这段程序的上下文信息,这个时候咱们可能会自定义个 error 构造体,继承 error 接口:

err:= xxx()
 if err != nil {return XXError{Err: err} }

而后咱们把上下文信息都加到 XXError 中,然而这样尽管能够增加一些上下文信息,然而每次都须要创立一个特定类型的 error 类会变得很麻烦,那么在 1.13 之后,咱们能够应用 %w 进行 wrap。

if err != nil {return fmt.Errorf("xxx failed: %w", err) }

当然除了下面这种做法以外,咱们还能够间接 %v 间接格式化咱们的错误信息:

if err != nil {return fmt.Errorf("xxx failed: %v", err) }

这样做的毛病就是咱们会失落这个 err 的类型信息,如果不须要这个类型信息,只是想往上抛打印一些日志当然也无所谓。

13.2 error Is & As

因为咱们的 error 能够会被 wrap 好几层,那么应用 == 是可能无奈判断咱们的 error 到底是不是咱们想要的特定的 error,那么能够用 errors.Is:

var BaseErr = errors.New("base error")

func main() {err1 := fmt.Errorf("wrap base: %w", BaseErr)
   err2 := fmt.Errorf("wrap err1: %w", err1)
   println(err2 == BaseErr)
   
   if !errors.Is(err2, BaseErr) {panic("err2 is not BaseErr")
   }
   println("err2 is BaseErr")}

输入:

false
err2 is BaseErr

在下面,咱们通过 errors.Is 就能够判断出 err2 外面蕴含了 BaseErr 谬误。errors.Is 外面会递归调用 Unwrap 办法拆包装,而后挨个应用 == 判断是否和指定类型的 error 相等。

errors.As 次要用来做类型判断,起因也是和下面一样,error 被 wrap 之后咱们通过 err.(type) 无奈直接判断,errors.As 会用 Unwrap 办法拆包装,而后挨个判断类型。应用如下:

type TypicalErr struct {e string}

func (t TypicalErr) Error() string {return t.e}

func main() {err := TypicalErr{"typical error"}
   err1 := fmt.Errorf("wrap err: %w", err)
   err2 := fmt.Errorf("wrap err1: %w", err1)
   var e TypicalErr
   if !errors.As(err2, &e) {panic("TypicalErr is not on the chain of err2")
   }
   println("TypicalErr is on the chain of err2")
   println(err == e)}

输入:

TypicalErr is on the chain of err2
true

13.3 解决 defer 中的 error

比方上面代码,咱们如果在调用 Close 的时候报错是没有解决的:

func getBalance(db *sql.DB, clientID string) (float32, error) {rows, err := db.Query(query, clientID)
            if err != nil {return 0, err}
            defer rows.Close()
    
            // Use rows    }

那么兴许咱们能够在 defer 中打印一些 log,然而无奈 return,defer 不承受一个 err 类型的返回值:

defer func() {err := rows.Close()
            if err != nil {log.Printf("failed to close rows: %v", err)
            }
            return err // 无奈通过编译    }()

那么咱们可能想通过默认 err 返回值的形式将 defer 的 error 也返回了:

func getBalance(db *sql.DB, clientID string) (balance float32, err error) {rows, err = db.Query(query, clientID)
            if err != nil {return 0, err}
            defer func() {err = rows.Close()
            }()
    
            // Use rows    }

下面代码看起来没问题,那么如果 Query 的时候和 Close 的时候同时产生异样呢?其中有一个 error 会被笼罩,那么咱们能够依据本人的需要抉择一个打印日志,另一个 error 返回:

defer func() {closeErr := rows.Close()
            if err != nil {
                    if closeErr != nil {log.Printf("failed to close rows: %v", err)
                    }
                    return
            }
            err = closeErr    }()

14、happens before 保障

创立 goroutine 产生先于 goroutine 执行,所以上面这段代码先读一个变量,而后在 goroutine 中写变量不会产生 data race 问题:

i := 0
    go func() {i++}()

goroutine 退出没有任何 happen before 保障,例如上面代码会有 data race:

i := 0
    go func() {i++}()    fmt.Println(i)

channel 操作中 send 操作是 happens before receive 操作:

var c = make(chan int, 10)
var a string

func f() {
  a = "hello, world"
  c <- 0
}

func main() {go f()
  <-c
  print(a)}

下面执行程序应该是:

variable change -》channel send -》channel receive -》variable read

下面可能保障肯定输入 “hello, world”。

close channel 是 happens before receive 操作,所以上面这个例子中也不会有 data race 问题:

i := 0
    ch := make(chan struct{})
    go func() {
            <-ch
            fmt.Println(i)
    }()
    i++
    close(ch)

在无缓冲的 channel 中 receive 操作是 happens before send 操作的,例如:

var c = make(chan int)
var a string

func f() {
  a = "hello, world"
  <-c
}

func main() {go f()
  c <- 0
  print(a)}

这里同样能保障输入 hello, world。

15、Context Values

在 context 外面咱们能够通过 key value 的模式传递一些信息:

context.WithValue 是从 parentCtx 创立,所以创立进去的 ctx 既蕴含了父类的上下文信息,也蕴含了以后新加的上下文。

fmt.Println(ctx.Value("key"))

应用的时候能够间接通过 Value 函数输入。那么其实就能够想到,如果 key 雷同的话前面的值会笼罩后面的值的,所以在写 key 的时候能够自 定义一个非导出的类型作为 key 来保障惟一

package provider
    
    type key string
    
    const myCustomKey key = "key"
    
    func f(ctx context.Context) {ctx = context.WithValue(ctx, myCustomKey, "foo")
            // ...    }

16、应多关注 goroutine 何时进行

很多同学感觉 goroutine 比拟轻量,认为能够随便地启动 goroutine 去执行任何而不会有很大的性能损耗。这个观点根本没错,然而如果在 goroutine 启动之后因为代码问题导致它始终占用,没有进行,数量多了之后 可能会造成内存透露

比方上面的例子:

ch := foo()
    go func() {
            for v := range ch {// ...}    }()

如果在该 goroutine 中的 channel 始终没有敞开,那么这个 goroutine 就不会完结,会始终挂着占用一部分内存。

还有一种状况是咱们的主过程曾经进行运行了,然而 goroutine 外面的工作还没完结就被主过程杀掉了,那么这样也可能造成咱们的工作执行出问题,比方资源没有开释,抑或是数据还没解决完等等,如下:

func main() {newWatcher()
    
            // Run the application
    }
    
    type watcher struct {/* Some resources */}
    
    func newWatcher() {w := watcher{}
            go w.watch()}

下面这段代码就可能呈现主过程曾经执行 over 了,然而 watch 函数还没跑完的状况,那么其实能够通过设置 stop 函数,让主过程执行完之后执行 stop 函数即可:

func main() {w := newWatcher()
            defer w.close()
    
            // Run the application
    }
    
    func newWatcher() watcher {w := watcher{}
            go w.watch()
            return w
    }
    
    func (w watcher) close() {// Close the resources}

17、Channel

17.1 select & channel

select 和 channel 搭配起来往往有意想不到的成果,比方上面:

for {
            select {
            case v := <-messageCh:
                    fmt.Println(v)
            case <-disconnectCh:
                    fmt.Println("disconnection, return")
                    return
            }    }

下面代码中承受了 messageCh 和 disconnectCh 两个 channel 的数据,如果咱们想先承受 messageCh 的数组再承受 disconnectCh 的数据,那么下面代码会产生 bug,如:

for i := 0; i < 10; i++ {messageCh <- i}
    disconnectCh <- struct{}{}

咱们想要下面的 select 先输入完 messageCh 外面的数据,而后再 return,实际上可能会输入:

0
1
2
3
4
disconnection, return

这是因为 select 不像 switch 会顺次匹配 case 分支,select 会 随机 执行上面的 case 分支,所以想要做到先生产 messageCh channel 数据,如果 只有单个 goroutine 生产数据能够这样做:

应用无缓冲的 messageCh channel,这样在发送数据的时候会始终期待,直到数据被生产了才会往下走,相当于是个同步模型了(无缓冲的 channel 是 receive happens before send);在 select 外面应用单个 channel,比方面的 demo 中咱们能够定义一种非凡的 tag 来完结 channel,当读到这个非凡的 tag 的时候 return,这样就没必要用两个 channel 了。

如果有多个 goroutine 生产数据,那么能够这样:

for {
   select {
    case v := <-messageCh:
        fmt.Println(v)
    case <-disconnectCh:
        for {
           select {
                case v := <-messageCh:
                        fmt.Println(v)
                default:
                        fmt.Println("disconnection, return")
                    return
                            }
                    }
            }    }

在读取 disconnectCh 的时候外面再套一个循环读取 messageCh,读完了之后会调用 default 分支进行 return。

17.2 不要应用 nil channel

应用 nil channel 进行收发数据的时候会永远阻塞,例如发送数据:

var ch chan int
ch <- 0 //block

接收数据:

var ch chan int
<-ch //block

17.3 Channel 的 close 问题

channel 在 close 之后依然能够接收数据的,例如:

ch1 := make(chan int, 1)
  close(ch1)
  for {
    v := <-ch1
    fmt.Println(v)  }

这段代码会始终 print 0。这会导致什么问题呢?比方咱们想要将两个 channel 的数据会集到另一个 channel 中:

func merge(ch1, ch2 <-chan int) <-chan int {ch := make(chan int, 1) 
        go func() {
          for {
            select {
            case v:= <-ch1:
              ch <- v
            case v:= <-ch2:
              ch <- v
            }
          }
          close(ch) // 永远运行不到
        }() 
        return ch}

因为 channel 被 close 了还能够接管到数据,所以下面代码中,即便 ch1 和 ch2 都被 close 了,也是运行不到 close(ch) 这段代码,并且还始终将 0 推入到 ch channel 中。所以为了感知到 channel 被敞开了,咱们应该应用 channel 返回的两个参数:

   v, open := <-ch1
   fmt.Print(v, open) //open 返回 false 示意没有被敞开

那么回到咱们下面的例子中,就能够这样做:

func merge(ch1, ch2 <-chan int) <-chan int {ch := make(chan int, 1)
  ch1Closed := false
  ch2Closed := false

  go func() {
    for {
      select {
      case v, open := <-ch1:
        if !open { // 如果曾经敞开
          ch1Closed = true // 标记为 true
          break
        }
        ch <- v
      case v, open := <-ch2:
        if !open { // 如果曾经敞开
          ch2Closed = true// 标记为 true
          break
        }
        ch <- v
      }

      if ch1Closed && ch2Closed {// 都敞开了
        close(ch)// 敞开 ch
        return
      }
    }
  }() 
  return ch }

通过两个标记以及返回的 open 变量就能够判断 channel 是否被敞开了,如果都敞开了,那么执行 close(ch)。

18、string format 带来的 dead lock

如果类型定义了 String() 办法,它会被用在 fmt.Printf() 中生成默认的输入:等同于应用格式化描述符 %v 产生的输入。还有 fmt.Print() 和 fmt.Println() 也会主动应用 String() 办法。

那么咱们看看上面的例子:

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

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

  if age < 0 {return fmt.Errorf("age should be positive for customer %v", c)
  }

  c.age = age
  return nil
}

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

这个例子中,如果调用 UpdateAge 办法 age 小于 0 会调用 fmt.Errorf,格式化输入,这个时候 String() 办法外面也进行了加锁,那么这样会造成死锁。

mutex.Lock -> check age -> Format error -> call String() -> mutex.RLock

解决办法也很简略,一个是放大锁的范畴,在 check age 之后再加锁,另一种办法是 Format error 的时候不要 Format 整个构造体,能够改成 Format id 就行了。

19、谬误应用 sync.WaitGroup

sync.WaitGroup 通常用在并发中期待 goroutines 工作实现,用 Add 办法增加计数器,当工作实现后须要调用 Done 办法让计数器减一。期待的线程会调用 Wait 办法期待,直到 sync.WaitGroup 内计数器为零。

须要留神的是 Add 办法是怎么应用的,如下:

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)

这样应用可能会导致 v 不肯定等于 3,因为在 for 循环外面创立的 3 个 goroutines 不肯定比里面的主线程先执行,从而导致在调用 Add 办法之前可能 Wait 办法就执行了,并且恰好 sync.WaitGroup 外面计数器是零,而后就通过了。

正确的做法应该是在创立 goroutines 之前就将要创立多少个 goroutines 通过 Add 办法增加进去。

20、不要拷贝 sync 类型

sync 包外面提供一些并发操作的类型,如 mutex、condition、wait gorup 等等,这些类型都不应该被拷贝之后应用。

有时候咱们在应用的时候拷贝是很隐秘的,比方上面:

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

func (c Counter) Increment(name string) {c.mu.Lock()
  defer c.mu.Unlock()
  c.counters[name]++
}

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

func main() {counter := NewCounter()
  go counter.Increment("aa")
  go counter.Increment("bb")}

receiver 是一个值类型,所以调用 Increment 办法的时候实际上拷贝了一份 Counter 外面的变量。这里咱们能够将 receiver 改成一个指针,或者将 sync.Mutex 变量改成指针类型。

所以如果:

receiver 是值类型;函数参数是 sync 包类型;函数参数的构造体外面蕴含了 sync 包类型;

遇到这种状况须要留神检查一下,咱们能够借用 go vet 来检测,比方下面如果并发调用了就能够检测进去:

» go vet . bear@BEARLUO-MB7
# github.com/cch123/gogctuner/main
./main.go:53:9: Increment passes lock by value: github.com/cch123/gogctuner/main.Counter contains sync.Mutex

21、time.After 内存泄露

咱们用一个简略的例子模仿一下:

package main

import (
    "fmt"
    "time"
)
//define a channel
var chs chan int

func Get() {
    for {
        select {
            case v := <- chs:
                fmt.Printf("print:%v\n", v)
            case <- time.After(3 * time.Minute):
                fmt.Printf("time.After:%v", time.Now().Unix())
        }
    }
}

func Put() {
    var i = 0
    for {
        i++
        chs <- i
    }
}

func main() {chs = make(chan int, 100)
    go Put()
    Get()}

逻辑很简略就是先往 channel 外面存数据,而后不停地应用 for select case 语法从 channel 外面取数据,为了避免长时间取不到数据,所以在下面加了 time.After 定时器,这里只是简略打印一下。

而后我没用 pprof 看一下内存占用:

$ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap

发现不一会儿 Timer 的内存占用很高了。这是因为在计时器触发之前,垃圾收集器不会回收 Timer,然而在循环外面每次都调用 time.After 都会实例化一个一个新的定时器,并且这个定时器会在激活之后才会被革除。

为了防止这种状况咱们能够应用 上面代码:

func Get() {delay := time.NewTimer(3 * time.Minute)

    defer delay.Stop()

    for {delay.Reset(3 * time.Minute)

        select {
            case v := <- chs:
                fmt.Printf("print:%v\n", v)
            case <- delay.C:
                fmt.Printf("time.After:%v", time.Now().Unix())
        }
    }}

22、HTTP body 遗记 Close 导致的泄露

type handler struct {
        client http.Client
        url string
}

func (h handler) getBody() (string, error) {resp, err := h.client.Get(h.url)
        if err != nil {return "", err}

        body, err := io.ReadAll(resp.Body)
        if err != nil {return "", err}

        return string(body), nil}

下面这段代码看起来没什么问题,然而 resp 是 *http.Response 类型,外面蕴含了 Body io.ReadCloser 对象,它是一个 io 类,必须要正确敞开,否则是会产生资源泄露的。个别咱们能够这么做:

defer func() {err := resp.Body.Close()
        if err != nil {log.Printf("failed to close response: %v\n", err)
        }}()

23、Cache line

目前在计算机中,次要有两大存储器 SRAM 和 DRAM。主存储器是由 DRAM 实现的,也就是咱们常说的内存,在 CPU 里通常会有 L1、L2、L3 这样三层高速缓存是用 SRAM 实现的。

当从内存中取单元到 cache 中时,会一次取一个 cacheline 大小的内存区域到 cache 中,而后存进相应的 cacheline 中,所以当你读取一个变量的时候,可能会把它相邻的变量也读取到 CPU 的缓存中(如果正好在一个 cacheline 中),因为有很大的几率你会持续拜访相邻的变量,这样 CPU 利用缓存就能够减速对内存的拜访。

cacheline 大小通常有 32 bit,64 bit,128 bit。拿我电脑的 64 bit 举例:

cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size 
64

咱们设置两个函数,一个 index 加 2,一个 index 加 8:

func sum2(s []int64) int64 {
  var total int64
  for i := 0; i < len(s); i += 2 {total += s[i]
  }
  return total
}

func sum8(s []int64) int64 {
  var total int64
  for i := 0; i < len(s); i += 8 {total += s[i]
  }
  return total}

这看起来 sum8 解决的元素比 sum2 少四倍,那么性能应该也快四倍左右,书上说只快了 10%,然而我没测进去这个数据,无所谓了大家晓得因为 cacheline 的存在,并且数据在 L1 缓存外面性能很高就行了。

而后再看看 slice 类型的构造体和构造体里蕴含 slice:

type Foo struct {
        a int64
        b int64
}

func sumFoo(foos []Foo) int64 {
        var total int64
        for i := 0; i < len(foos); i++ {total += foos[i].a
        }
        return total}

Foo 外面蕴含了两个字段 a 和 b,sumFoo 会遍历 Foo slice 将所有 a 字段加起来返回。

type Bar struct {a []int64
        b []int64}

func sumBar(bar Bar) int64 {
        var total int64
        for i := 0; i < len(bar.a); i++ {total += bar.a[i]
        }
        return total}

Bar 外面是蕴含了 a,b 两个 slice,sumBar 会将 Bar 外面的 a 的元素和相加返回。咱们同样用两个 benchmark 测试一下:

func Benchmark_sumBar(b *testing.B) {
  s := Bar{a: make([]int64, 16),
    b: make([]int64, 16),
  }

  b.RunParallel(func(pb *testing.PB) {for pb.Next() {sumBar(s)
    }
  })
}

func Benchmark_sumFoo(b *testing.B) {s := make([]Foo, 16)

  b.RunParallel(func(pb *testing.PB) {for pb.Next() {sumFoo(s)
    }
  })}

测试后果:

# go test -gcflags "-N -l" -bench .
Benchmark_sumBar-16 249029368 4.855 ns/opBenchmark_sumFoo-16 238571205 5.056 ns/op

sumBar 会比 sumFoo 快一点的。这是因为对于 sumFoo 来说要读完整个数据才行,而对于 sumBar 来说只须要读前 16 bytes 读入到 cache line:

24、对于 False Sharing 造成的性能问题

False Sharing 是因为多线程对于同一片内存进行并行读写操作的时候会造成内存缓存生效,而重复将数据载入缓存所造成的性能问题。

因为当初 CPU 的缓存都是分级的,对于 L1 缓存来说是每个 Core 所独享的,那么就有可能面临缓存数据生效的问题。

如果同一片数据被多个 Core 同时加载,那么它就是共享状态在共享状态下想要批改数据要先向所有的其余 CPU 外围播送一个申请,要求先把其余 CPU 外围外面的 cache,都变成有效的状态,而后再更新以后 cache 外面的数据。

CPU 外围外面的 cache 变成有效之后就不能应用了,须要从新加载,因为不同级别的缓存的速度是差别很大的,所以这其实性能影响还蛮大的,咱们写个测试看看。

type MyAtomic interface {IncreaseAllEles()
}

type Pad struct {
  a uint64
  _p1 [15]uint64
  b uint64
  _p2 [15]uint64
  c uint64
  _p3 [15]uint64
}

func (myatomic *Pad) IncreaseAllEles() {atomic.AddUint64(&myatomic.a, 1)
  atomic.AddUint64(&myatomic.b, 1)
  atomic.AddUint64(&myatomic.c, 1)
}

type NoPad struct {
  a uint64
  b uint64
  c uint64
}

func (myatomic *NoPad) IncreaseAllEles() {atomic.AddUint64(&myatomic.a, 1)
  atomic.AddUint64(&myatomic.b, 1)
  atomic.AddUint64(&myatomic.c, 1)}

这里我定义了两个构造体 Pad 和 NoPad。而后咱们定义一个 benchmark 进行多线程测试:

func testAtomicIncrease(myatomic MyAtomic) {
  paraNum := 1000
  addTimes := 1000
  var wg sync.WaitGroup
  wg.Add(paraNum)
  for i := 0; i < paraNum; i++ {go func() {
      for j := 0; j < addTimes; j++ {myatomic.IncreaseAllEles()
      }
      wg.Done()}()}
  wg.Wait()}
func BenchmarkNoPad(b *testing.B) {myatomic := &NoPad{}
  b.ResetTimer()
  testAtomicIncrease(myatomic)
}

func BenchmarkPad(b *testing.B) {myatomic := &Pad{}
  b.ResetTimer()
  testAtomicIncrease(myatomic)}

后果能够看到快了 40% 左右:

BenchmarkNoPad
BenchmarkNoPad-10      1000000000           0.1360 ns/op
BenchmarkPad
BenchmarkPad-10        1000000000           0.08887 ns/op

如果没有 pad 话,变量数据都会在一条 cache line 外面,这样如果其中一个线程批改了数据会导致另一个线程的 cache line 有效,须要从新加载:

加了 padding 之后数据都不在同一个 cache line 上了,即便产生了批改 invalid 不是同一行数据也不须要从新加载。

25、内存对齐

简而言之,当初的 CPU 拜访内存的时候是一次性拜访多个 bytes,比方 64 位架构一次拜访 8bytes,该处理器只能从地址为 8 的倍数的内存开始读取数据,所以要求数据在寄存的时候首地址的值是 8 的倍数寄存,这就是所谓的内存对齐。

比方上面的例子中因为内存对齐的存在,所以上面的例子中 b 这个字段只能在前面另外找地址为 8 的倍数地址开始寄存:

除此之外还有一个零大小字段对齐的问题,如果构造体或数组类型不蕴含大小大于零的字段或元素,那么它的大小就为 0。比方 x [0]int8, 空构造体 struct{}。当它作为字段时不须要对齐,然而作为构造体最初一个字段时须要对齐。咱们拿空构造体来举个例子:

type M struct {
    m int64
    x struct{}}

type N struct {x struct{}
    n int64
}

func main() {m := M{}
    n := N{}
    fmt.Printf("as final field size:%d\nnot as final field size:%d\n", unsafe.Sizeof(m), unsafe.Sizeof(n))}

输入:

as final field size:16
not as final field size:8

当然,咱们不可能手动去调整内存对齐,咱们能够通过应用工具 fieldalignment:

$ go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

$ fieldalignment -fix .\main\my.go
main\my.go:13:9: struct of size 24 could be 16

26、逃逸剖析

Go 是通过在编译器里做逃逸剖析(escape analysis)来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上。对于 Go 来说,咱们能够通过上面指令来看变量是否逃逸:

go run -gcflags '-m -l' main.go
-m 会打印出逃逸剖析的优化策略,实际上最多总共能够用 4 个 -m,然而信息量较大,个别用 1 个就能够了。-l 会禁用函数内联,在这里禁用掉内联能更好的察看逃逸状况,缩小烦扰。

26.1 指针逃逸

在函数中创立了一个对象,返回了这个对象的指针。这种状况下,函数尽管退出了,然而因为指针的存在,对象的内存不能随着函数完结而回收,因而只能调配在堆上。

type Demo struct {name string}

func createDemo(name string) *Demo {d := new(Demo) // 局部变量 d 逃逸到堆
  d.name = name
  return d
}

func main() {demo := createDemo("demo")
  fmt.Println(demo)}

咱们检测一下:

go run -gcflags '-m -l'  .\main\main.go
# command-line-arguments
main\main.go:12:17: leaking param: name
main\main.go:13:10: new(Demo) escapes to heap
main\main.go:20:13: ... argument does not escape&{demo}

26.2 interface{}/any 动静类型逃逸

因为编译期间很难确定其参数的具体类型,也会产生逃逸,例如这样:

func createDemo(name string) any {d := new(Demo) // 局部变量 d 逃逸到堆
  d.name = name
  return d}

26.3 切片长度或容量没指定逃逸

如果应用部分切片时,已知切片的长度或容量,请应用常量或数值字面量来定义,否则也会逃逸:

func main() {
    number := 10
    s1 := make([]int, 0, number)
    for i := 0; i < number; i++ {s1 = append(s1, i)
    }
    s2 := make([]int, 0, 10)
    for i := 0; i < 10; i++ {s2 = append(s2, i)
    }}

输入一下:

go run -gcflags '-m -l'  main.go    
 
./main.go:65:12: make([]int, 0, number) escapes to heap
./main.go:69:12: make([]int, 0, 10) does not escape

26.4 闭包

例如上面:Increase() 返回值是一个闭包函数,该闭包函数拜访了内部变量 n,那变量 n 将会始终存在,直到 in 被销毁。很显然,变量 n 占用的内存不能随着函数 Increase() 的退出而回收,因而将会逃逸到堆上。

func Increase() func() int {
  n := 0
  return func() int {
    n++
    return n
  }
}

func main() {in := Increase()
  fmt.Println(in()) // 1
  fmt.Println(in()) // 2}

输入:

go run -gcflags '-m -l'  main.go  
 
./main.go:64:5: moved to heap: n
./main.go:65:12: func literal escapes to heap

27、byte slice 和 string 的转换优化

间接通过强转 string(bytes) 或者 []byte(str) 会带来数据的复制,性能不佳,所以在谋求极致性能场景应用 unsafe 包的形式间接进行转换来晋升性能:

// toBytes performs unholy acts to avoid allocations
func toBytes(s string) []byte {return *(*[]byte)(unsafe.Pointer(&s))
}
// toString performs unholy acts to avoid allocations
func toString(b []byte) string {return *(*string)(unsafe.Pointer(&b))}

在 Go 1.12 中,减少了几个办法 String、StringData、Slice 和 SliceData , 用来做这种性能转换。

28、容器中的 GOMAXPROCS

自 Go 1.5 开始,Go 的 GOMAXPROCS 默认值曾经设置为 CPU 的核数,然而在 Docker 或 k8s 容器中 runtime.GOMAXPROCS() 获取的是 宿主机的 CPU 核数。这样会导致 P 值设置过大,导致生成线程过多,会减少上

下文切换的累赘,导致重大的上下文切换,节约 CPU。

所以能够应用 uber 的 automaxprocs 库,大抵原理是读取 CGroup 值辨认容器的 CPU quota,计算失去理论外围数,并主动设置 GOMAXPROCS 线程数量。

import _ "go.uber.org/automaxprocs"

func main() {// Your application logic here}

29、总结

以上就是本篇文章对《100 Go Mistakes How to Avoid Them》书中内容的技术总结,也是一些在日常应用 Go 在工作中容易漠视掉的问题。内容量较大,常见谬误和技巧也很多,能够重复浏览,感兴趣的开发者能够珍藏下来缓缓钻研。

参考:

https://go.dev/ref/mem

https://colobu.com/2019/01/24/cacheline-affects-performance-in-go/

https://teivah.medium.com/go-and-cpu-caches-af5d32cc5592

https://geektutu.com/post/hpg-escape-analysis.html

https://github.com/uber-go/automaxprocs

https://gfw.go101.org/article/unsafe.html

-End-

原创作者|罗志贇

技术责编|吴连火

应用 Go 语言时还有什么易错点?欢送在腾讯云开发者公众号评论区分享。咱们将选取 1 则最有意义的分享,送出腾讯云开发者 - 文化衫 1 件(见下图)。6 月 12 日中午 12 点开奖。

正文完
 0