我为什么放弃Go语言?

我为什么放弃Go语言?

腾小云导读

你在什么时分会产生“想要抛弃用 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 函数,那么履行次序会是:

我为什么放弃Go语言?

还有一种状况,假设是运用 “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语言?

核算规则我就不在这儿展现了,感兴趣的能够自己去查查,我这儿说说这种计数法在 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,所以它们实践上都有必定的差错,可是能够看到先乘再加精度丢掉会更小

假设想要准确核算浮点的话,能够测验 “github.com/shopspring/…” 库,换成这个库咱们再来核算一下:

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,那么底层的数据结构是这样的:

我为什么放弃Go语言?

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 仍是指向了底层相同的数组,构如下:

我为什么放弃Go语言?

由于指向了同一个数组,那么当咱们改动第一个槽位的时分,比方 s1[1]=2,实践上两个 slice 的数据都会产生改动:

我为什么放弃Go语言?

可是当咱们运用 append 的时分状况会有所不同:

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

我为什么放弃Go语言?

s1 的 len 并没有被改动,所以看到的仍是3元素。

还有一件比较有趣的细节是,假设再接着 append s1 那么第四个元素会被掩盖掉:

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

我为什么放弃Go语言?

咱们再持续 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]

我为什么放弃Go语言?

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

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

我为什么放弃Go语言?

假设 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 持有的同一个:

我为什么放弃Go语言?

所以咱们真的要只回来 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 这块空间里边:

我为什么放弃Go语言?

所以咱们能够这样在 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 := "hllo"
  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 值,所以输出的是 hllo 而不是 hllo。

那么根据上面的剖析,咱们就能够知道在迭代获取字符的时分不能只获取单个 byte,应该运用 range 回来的 value值:

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

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

s := "hllo"
  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>

我为什么放弃Go语言?

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

我为什么放弃Go语言?

发现不一会儿 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 完成的。

我为什么放弃Go语言?

当从内存中取单元到 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:

我为什么放弃Go语言?

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 无效,需求从头加载:

我为什么放弃Go语言?

加了 padding 之后数据都不在同一个 cache line 上了,即便产生了修正 invalid 不是同一行数据也不需求从头加载。

我为什么放弃Go语言?

25、内存对齐

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

比方下面的比方中由于内存对齐的存在,所以下面的比方中 b 这个字段只能在后边另外找地址为8的倍数地址开端寄存:

我为什么放弃Go语言?

除此之外还有一个零巨细字段对齐的问题,假设结构体或数组类型不包含巨细大于零的字段或元素,那么它的巨细就为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 在工作中简单忽视掉的问题。内容量较大,常见过错和技巧也许多,能够重复阅览,感兴趣的开发者能够收藏下来渐渐研讨。

参阅:

go.dev/ref/mem

colobu.com/2019/01/24/…

teivah.medium.com/go-and-cpu-…

geektutu.com/post/hpg-es…

github.com/uber-go/aut…

gfw.go101.org/article/uns…

-End-

原创作者|罗志贇

技能责编|吴连火

我为什么放弃Go语言?

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

我为什么放弃Go语言?
我为什么放弃Go语言?

我为什么放弃Go语言?

我为什么放弃Go语言?

我为什么放弃Go语言?