BruceChen7 / gitblog

My blog
6 stars 1 forks source link

golang中的最佳实践 #76

Open BruceChen7 opened 1 year ago

BruceChen7 commented 1 year ago

参考资料

init 使用

错误类型

错误匹配? 错误消息 指导
No static errors.New
No dynamic fmt.Errorf
Yes static top-level var with e rrors.New
Yes dynamic custom error type

几种使用场景

错误包装

一次处理错误

优先使用 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 退出

包名

局部变量的声明

nil 是一个有效的 slice

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

在边界处拷贝 Slices 和 Maps

使用 defer 来释放资源

避免字符串到字节的转换

尽量初始化时指定 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
}

避免使用全局变量

不要 panic

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

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

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
}

这回对于接口类型变量的相等性判断一目了然了 (由 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 将会读到什么呢?这里分为“有数据关闭”和“无数据关闭”。

type/golang #public