未知的Enum值

来看个简单的例子

type Status uint32

const (
    StatusOpen Status = iota
    StatusClose
    StatusUnknown
)

在上面的代码中,使用iota创建了一个enum类型,分别代指下面的状态信息:

StatusOpen    = 0
StatusClose   = 1
StatusUnknown = 2

现在,我们假设Status 是一个 JSON 请求中被Marshalled / Unmarshalled的一个属性,我们可以设计出下面的数据结构:

type Request struct {
    ID         int     `json:"Id"`
    Timestamp  int     `json:"Timestamp"`
    Status     Status  `json:"Status"`
}

然后,假设收到的Request 的接口返回值为:

{
    "Id": 1234,
    "Timestamp": 1563362390,
    "Status": 0
}

到目前为止,没有什么特殊的表达,Status将会被反序列化为StatusOpen,是吧?
好的,我们来看一个未设置 status 返回值的请求(不管是出于什么原因吧)。

{
    "Id": 1234,
    "Timestamp": 1563362390
}

在这个例子中,Request结构体的Status字段将会被初始化为默认零值zeroed value, 对于 uint32 类型来说,值就是0。因此,StatusOpen就替换掉了原本值应该是StatusUnknown。
对于这类场景,把unknown value 设置为枚举类型0 应该比较合适,如下:

type Status uint32

const (
    StatusUnknown Status  = iota
    StatusOpen
    StatusClose
)

这样,即时返回的 JSON 请求中没有Status属性,结构体Request的Status属性也会按我们预期的,被初始化为StatusUnknown。

性能测试

正确地进行性能测试很困难,因为过程中有太多的因素会影响测试结果了。
其中一个最常见的错误就是被一些编译器优化参数糊弄,让我们以teivah/bitvector库中的一个真实案例来进行阐述:

func clear(n uint64, i, j uint8) uint64 {
    return (math.MaxUint64<<j | ((1 << i) - 1)) & n
}

这个函数会清理给定长度n的二进制位,对这个函数进行性能测试的话,我们可能会写出下面的代码:

func BenchmarkWrong(b *testing.B) {
 for i := 0; i < b.N; i++ {
  clear(1221892080809121, 10, 63)
 }
}

在这个性能测试中,编译器发现clear函数是并没有调用其他函数,因此编译器就会进行inline处理。除此之外,编译器还发现这个函数中也没有side-effects。因此,clear就会被删除,不去计算它的耗时,因此这就会导致测试结果的不准确。
一个建议是设置全局变量,如下:

var result uint64

func BenchmarkCorrect(b *testing.B) {
 var r uint64
 for i := 0; i < b.N; i++ {
  r = clear(1221892080809121, 10, 63)
 }
 result = r
}

这样的话,编译器就不知道clear函数是否会造成side-effect了,因此,性能测试的结果就会变得更加准确。

指针,到处都是指针!

值传递的时候,会创建一个同值变量;而指针传递的时候,只是将变量地址进行拷贝。
因此,指针传递总是会很快,是不?

如果你觉得是这样,可以看一下这个例子。在这个性能测试中,一个大小为0.3K的数据结构分别以值传递和指针传递进行测试。0.3K 不大,但是也不能和大部分我们日常用到的场景中的数据结构大小相差甚远,接近即可。

当我在自己的本地环境中执行这个性能测试代码的时候,值传递比指针传递快了4 倍还多,是不是感觉有悖常理?

关于这个现象的解释涉及到了 Go 中的内存管理,我没法解释得像 William Kennedy 解释的那样精炼,一起来整理总结下吧:

变量可以被分配到heap和stack上,粗略解释为:

  • 栈包含哪些分配给了 goroutine 的随时消失的变量,一旦函数返回,变量就会从栈中弹出
  • 堆包含共享变量,比如全局变量等

一起通过一个简单的例子来测试下:

func getFooValue() foo {
    var result foo
    // Do something
    return result
}

result被当前 goroutine 创建,这个变量就会被压入当前运行栈。一旦函数返回,调用方就会收到与此变量的一份拷贝,二者值相同,但是变量地址不同。变量本身会被弹出,此时变量并不会被立即销毁,直到它的内存地址被另一个变量覆盖或者被擦除,这个时候它才是真的再也不会被访问到了。
与此相对,看一个一个指针传递的例子:

func getFooPointer() *foo {
    var result foo
    // Do something
    return &result
}

result依旧是被当前goroutine所创建,但是调用方收到的会是一个指针(指向变量的内存地址)。如果result被栈弹出,那么调用方不可能访问到此变量。

在这个场景下,GO 的编译器会把result放置到可以被共享的变量空间:heap。
下面来看另一个场景,比如:

func main() {
    p := &foo{}
    f(p)
}

f的调用方与 f所属为同一个 goroutine,变量p不会被转换,它只是被简单放回到栈中,因此子函数依旧可以访问到。

举例来说,io.Reader中的Read方法接收指针,而不是返回一个,因为返回一个切片就会被转换到堆中。
为什么栈会这么快?这里有两个主要的原因:

栈不需要垃圾收集。正如我们所说,一个变量创建时被压入栈,函数返回时从栈中弹出。根本不需要复杂的处理来回收未使用的变量。

一个栈隶属于一个 goroutine,与堆中变量相比,不需要同步处理,这同样会使得栈很快。

总结一下,当我们创建一个函数的时候,我们应该使用值传递而不是指针传递。只有我们期待某个变量被共享使用时,才使用指针传递适用。

当我们下次遇到性能优化的问题时,一个可能的优化方向就是检查在某些场景下,指针传递是否真的会有所帮助。一个需要了解的常识是:当使用go build -gcflags “-m -m”时,编译器会默认将一个变量转换到堆中。

再强调下,在日常开发中,应该总是首先考虑值传递。

干掉 for/switch 或者 for/select

如果f函数返回了 true,会发生什么?

for {
    switch f() {
    case true:
        break
    case false:
        // do something
    }
}

break 语句会被调用,这会导致switch语句退出,而不是 loop 退出。再看一个类似问题:

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

break 同样只是退出select语句,而不是 for 循环。
一个可能的解决方案是使用labeled break 标签,例如:

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

转自:guoruibiao.blog.csdn.net/article/details/108054295