BruceChen7 / gitblog

My blog
6 stars 1 forks source link

effective golang #45

Open BruceChen7 opened 1 year ago

BruceChen7 commented 1 year ago

参考资料

init使用

在golang中的每个模块可以,定义init函数,用来初始化该包内的全局变量,我们可以看看它的特点

package main
import "fmt"
// 另外一个文件
func init() {
    fmt.Println("init 1")
}
func init() {
    fmt.Println("init 2")
}
func main() {
    fmt.Println("main")  // 输出 init 1, init2  main
}

优先使用strconv而不是fmt

将原语转换为字符串或从字符串转换时,strconv速度比fmt快。

避免字符串到字节的转换

将原语转换为字符串或从字符串转换时,strconv速度比fmt快。

// bad
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}

// good
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}

不要一劳永逸使用goroutine

一般来说,每个 goroutine:

// bad
go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()

// good
var (
  stop = make(chan struct{}) // 告诉 goroutine 停止
  done = make(chan struct{}) // 告诉我们 goroutine 退出了
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-tick.C:
      flush()
    case <-stop:
      return
    }
  }
}()
// 其它...
close(stop)  // 指示 goroutine 停止
<-done       // and wait for it to exit

等待goroutine退出

一个由系统生成的 goroutine, 必须有一种方案能等待 goroutine 的退出。 有两种常用的方法可以做到这一点

var wg sync.WaitGroup
for i := 0; i < N; i++ {
  wg.Add(1)
  go func() {
    defer wg.Done()
    // ...
  }()
}

// To wait for all to finish:
wg.Wait()

另外一种是下面的:

done := make(chan struct{})
go func() {
  defer close(done)
  // ...
}()

// To wait for the goroutine to finish:
<-done

命名风格

包名

当命名包时,请按下面规则选择一个名称:

局部变量的声明

如果将变量明确设置为某个值,则应使用短变量声明形式 (:=)。

// bad
var s = "foo"
// god
s := "foo"

nil是一个有效的slice

nil 是一个有效的长度为 0 的 slice,这意味着,

Channel 的 size 要么是 1,要么是无缓冲的

channel 通常 size 应为 1 或是无缓冲的。默认情况下,channel 是无缓冲的,其 size 为零。任何其他尺寸都必须经过严格的审查。我们需要考虑如何确定大小,考虑是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。

// 大小:1
c := make(chan int, 1) // 或者
// 无缓冲 channel,大小为 0
c := make(chan int)

在边界处拷贝Slices 和 Maps

slices和maps包含了指向底层数据的指针,因此在需要复制它们时要特别注意。请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改

// bad
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// 你是要修改 d1.trips 吗?
trips[0] = ...

// good
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// 这里我们修改 trips[0],但不会影响到 d1.trips
trips[0] = ...

使用defer来释放资源

使用 defer 释放资源,诸如文件和锁。

// bad
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount
// 当有多个 return 分支时,很容易遗忘 unlock

// good
p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

避免字符串到字节的转换

不要反复从固定字符串创建字节slice。相反,请执行一次转换并捕获结果。

// bad
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
// good
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}

尽量初始化时指定Map容量

make(map[T1]T2, hint)
// bad
m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}

// good
files, _ := ioutil.ReadDir("./files")
m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
    m[f.Name()] = f
}

避免使用全局变量

使用选择依赖注入方式避免改变全局变量。 既适用于函数指针又适用于其他值类型

// bad
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}
// sign_test.go
func TestSign(t *testing.T) {
  oldTimeNow := _timeNow
  _timeNow = func() time.Time {
    return someFixedTime
  }
  defer func() { _timeNow = oldTimeNow }()
  assert.Equal(t, want, sign(give))
}

// bad
// sign.go
type signer struct {
  now func() time.Time
}
func newSigner() *signer {
  return &signer{
    now: time.Now,
  }
}
func (s *signer) Sign(msg string) string {
  now := s.now()
  return signWithTime(msg, now)

}
// sign_test.go
func TestSigner(t *testing.T) {
  s := newSigner()
  s.now = func() time.Time {
    return someFixedTime
  }
  assert.Equal(t, want, s.Sign(give))
}

不要 panic

在生产环境中运行的代码必须避免出现 panic。panic是cascading failures级联失败的主要根源。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它

// bad
func foo(bar string) {
  if len(bar) == 0 {
    panic("bar must not be empty")
  }
  // ...
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  foo(os.Args[1])
}

// good
func foo(bar string) error {
  if len(bar) == 0 {
    return errors.New("bar must not be empty")
  }
  // ...
  return nil
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  if err := foo(os.Args[1]); err != nil {
    panic(err)
  }
}

避免在公共结构中嵌入类型

这些嵌入的类型泄漏实现细节、禁止类型演化和模糊的文档。假设共享的AbstractList实现了多种列表类型,请避免在具体的列表实现中嵌入 AbstractList。 相反,只需手动将方法写入具体的列表,该列表将委托给抽象列表。

// bad
type AbstractList struct {}
// 添加将实体添加到列表中。
func (l *AbstractList) Add(e Entity) {
  // ...
}
// 移除从列表中移除实体。
func (l *AbstractList) Remove(e Entity) {
  // ...
}

// ConcreteList 是一个实体列表。
type ConcreteList struct {
  *AbstractList
}

// good
// ConcreteList 是一个实体列表。
type ConcreteList struct {
  list *AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
  return l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
  return l.list.Remove(e)
}

Go 允许类型嵌入作为继承和组合之间的折衷。外部类型获取嵌入类型的方法的隐式副本。 默认情况下,这些方法委托给嵌入实例的同一方法。 结构还获得与类型同名的字段。 所以,如果嵌入的类型是 public,那么字段是 public。为了保持向后兼容性,外部类型的每个未来版本都必须保留嵌入类型

很少需要嵌入类型。 这是一种方便,可以帮助您避免编写冗长的委托方法 即使嵌入兼容的抽象列表 interface,而不是结构体,这将为开发人员提供更大的灵活性来改变未来,但仍然泄露了具体列表使用抽象实现的细节

// bad
// AbstractList 是各种实体列表的通用实现。
type AbstractList interface {
  Add(Entity)
  Remove(Entity)
}
// ConcreteList 是一个实体列表。
type ConcreteList struct {
  AbstractList
}
// good
// ConcreteList 是一个实体列表。
type ConcreteList struct {
  list *AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
  return l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
  return l.list.Remove(e)
}

无论是使用嵌入式结构还是使用嵌入式接口,嵌入式类型都会限制类型的演化.

尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在文档中发现完整列表接口的间接性操作

判断接口类型的变量是否相等

func main() {
    printNonEmptyInterface1()
}

type T struct {
    name string
}
func (t T) Error() string {
    return "bad error"
}
func printNonEmptyInterface1() {
    var err1 error    // 非空接口类型
    var err1ptr error // 非空接口类型
    var err2 error    // 非空接口类型
    var err2ptr error // 非空接口类型

    err1 = T{"eden"}
    err1ptr = &T{"eden"}

    err2 = T{"eden"}
    err2ptr = &T{"eden"}

    println("err1:", err1)
    println("err2:", err2)
    println("err1 = err2:", err1 == err2)             // true
    println("err1ptr:", err1ptr)
    println("err2ptr:", err2ptr)
    println("err1ptr = err2ptr:", err1ptr == err2ptr) // false
}

具体interface的接口表示

// $GOROOT/src/runtime/runtime2.go
type iface struct { // 非空接口类型的运行时表示
    tab  *itab
    data unsafe.Pointer
}

type eface struct { // 空接口类型的运行时表示
    _type *_type
    data  unsafe.Pointer
}

// $GOROOT/src/runtime/print.go
func printeface(e eface) {
    print("(", e._type, ",", e.data, ")")
}

func printiface(i iface) {
    print("(", i.tab, ",", i.data, ")")
}

第一个字段功能相似,都是表示类型信息的,而第二个指针字段的功能也相同,都是指向当前赋值给该接口类型变量的动态类型变量的值,我们从printeface和printiface的实现可以看出println会将接口类型变量的类型信息与data信息输出,代码如下:

err1: (0x10c6cc0,0xc000092f20)
err2: (0x10c6cc0,0xc000092f40)
err1 = err2: true
err1ptr: (0x10c6c40,0xc000092f50)
err2ptr: (0x10c6c40,0xc000092f30)
err1ptr = err2ptr: false

为了找到真正原因,用lensm工具以图形化方式展示出汇编与源Go代码的对应关系:

img

img

Go都会调用runtime.ifaceeq来进行比较

// $GOROOT/src/runtime/alg.go
func efaceeq(t *_type, x, y unsafe.Pointer) bool {
      if t == nil {
          return true
      }
      eq := t.equal
      if eq == nil {
          panic(errorString("comparing uncomparable type " + t.string()))
      }
      if isDirectIface(t) {
          // Direct interface types are ptr, chan, map, func, and single-element structs/arrays thereof.
          // Maps and funcs are not comparable, so they can't reach here.
          // Ptrs, chans, and single-element items can be compared directly using ==.
          return x == y
      }
      return eq(x, y)
} 

func ifaceeq(tab *itab, x, y unsafe.Pointer) bool {
    if tab == nil {
        return true
    }
    t := tab._type
    eq := t.equal
    if eq == nil {
        panic(errorString("comparing uncomparable type " + t.string()))
    }
    if isDirectIface(t) {
        // See comment in efaceeq.
        return x == y
    }
    return eq(x, y)
}

这回对于接口类型变量的相等性判断一目了然了(由efaceeq中isDirectIface函数上面的注释可见)!

常见的数据竞争

循环索引变量捕获导致的data race

for _, job := range jobs {
    go func() {
        ProcessJob(job)
    }()
}

由于错误变量捕获导致的data race

x, err := Foo()
if err != nil {
}
go func() {
    var y int 
    y, err = Bar()
    if err != nil {
    }
}()
var z int
z, err = Bar()
if err != nil {
}

由于命名的返回变量被捕获,导致数据data race。

func NamedReturnCallee(result int) {
    result = 0
    if .. {
        return // 等价于return 10
    }
    go func() {
        ... = result  // data race
    }
    return 20 // 等价于 result = 20
}

slice是令人困惑的类型,会产生难以察觉的的data race

func ProcessAll(uuids []string) {
    var results []string
    var mutex sync.Mutex
    safeAppend = func (res string) {
        mutex.Lock()
        results = append(result, res)
        mutex.Unlock()
    }
    for _, uuid := range uuids {
        go func(id string, results string) {
            res := Foo(id)
            safeAppend(res)
        }
    }(uuid, results) // 这里有data race
}

对map并发读写导致了频繁的data race

func processOrders(uuids []string) error {
    var errMap = make(map[string]error)
    go func(uuid string) {
        orderHandle, err := GetOrder(uuid)
        if err != nil {
            errMap[uuid] = err // data race
            return
        }
    }(uuid)
    return combineError(errMap) // data race
}

混合使用channel 和共享内存使代码变得复杂,容易发生data race

func (f *Future) Start() {
    go func() {
        resp, err := f.f()
        f.response = resp
        f.err = err // data race
        f.ch <- 1
    }()
}
func (f *Future) Wait(ctx context.Context) error {
    select {
    case <- f.ch
            return nil
    case <- ctx.Done()
        f.err = ErrCancelled // data race

    }
}

sync.WaitGroup 不正确的Add 或者Done 产生data race

func WaitExample(itemIds []int) int {
    wg sync.WaitGroup
    results := make([]int, len(itemIds))
    for i := 0; i < len(itemIds); i++ {
        go (idx int) {
            wg.Add(1) // data race
            results[idx] = ..
            wg.Done()
        }(i)
    }
    wg.Wait() // data race
}

func Do() {
    var wg sync.WaitGroup
    go func() {
        defer clean(&locationErr) // locationErr data race
        defer wg.Done()
        // ...
    }
    wg.Wait()
    if locationErr != nil { // data race
    }
}

unsafe.Pointer

使用go vet来保证代码的严谨性

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // An array of contiguous uint32 values stored in memory.
    arr := []uint32{1, 2, 3}

    // The number of bytes each uint32 occupies: 4.
    const size = unsafe.Sizeof(uint32(0))

    // Take the initial memory address of the array and begin iteration.
    p := uintptr(unsafe.Pointer(&arr[0]))
    for i := 0; i < len(arr); i++ {
        // Print the integer that resides at the current address and then
        // increment the pointer to the next value in the array.
        fmt.Printf("%d ", (*(*uint32)(unsafe.Pointer(p))))
        p += size
    }
}

表面是会输出1 2 3,但是使用go vet

go vet .
# github.com/mdlayher/example
./main.go:20:33: possible misuse of unsafe.Pointer

规则说明:

Converting a Pointer to a uintptr produces the memory address of the value pointed at, as an integer. The usual use for such a uintptr is to print it.

Conversion of a uintptr back to Pointer is not valid in general.

A uintptr is an integer, not a reference. Converting a Pointer to a uintptr creates an integer value with no pointer semantics. Even if a uintptr holds the address of some object, the garbage collector will not update that uintptr’s value if the object moves, nor will that uintptr keep the object from being reclaimed

分裂出来的代码是

p := uintptr(unsafe.Pointer(&arr[0]))

// What happens if there's a garbage collection here?
fmt.Printf("%d ", (*(*uint32)(unsafe.Pointer(p))))

因为我们把地址值存到了p中,没有立即使用,那么可能其中发生了垃圾回收。我们使用将unsafe.Point类型指针转换成uintpr后,是不能狗直接转回到unsafe.Pointer的,但是只有一种情况例外

If p points into an allocated object, it can be advanced through the object by conversion to uintptr, addition of an offset, and conversion back to Pointer.

也就是执行指针的算术运算逻辑


package main

import (
    "fmt"
    "unsafe"
)

func main() {
    // An array of contiguous uint32 values stored in memory.
    arr := []uint32{1, 2, 3}

    // The number of bytes each uint32 occupies: 4.
    const size = unsafe.Sizeof(uint32(0))

    for i := 0; i < len(arr); i++ {
        // Print an integer to the screen by:
        //   - taking the address of the first element of the array
        //   - applying an offset of (i * 4) bytes to advance into the array
        //   - converting the uintptr back to *uint32 and dereferencing it to
        //     print the value
        fmt.Printf("%d ", *(*uint32)(unsafe.Pointer(
            uintptr(unsafe.Pointer(&arr[0])) + (uintptr(i) * size),
        )))
    }
}

这样使用后,就正常了

go vet .

golang中的socket编程

socket无数据

如果对方未发送数据到socket,接收方(Server)会阻塞在Read操作上,这和前面提到的“模型”原理是一致的。执行该Read操作的goroutine也会被挂起。runtime会监视该socket,直到其有数据才会重新调度该socket对应的Goroutine完成read

有部分数据

如果socket中有部分数据,且长度小于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回,而不是等待所有期望数据全部读取后再返回

Socket中有足够数据

如果socket中有数据,且长度大于等于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回。这个情景是最符合我们对Read的期待的了:Read将用Socket中的数据将我们传入的slice填满后返回:n = 10, err = nil。

socket关闭

如果client端主动关闭了socket,那么Server的Read将会读到什么呢?这里分为“有数据关闭”和“无数据关闭”。

golang #networking #public