0.1、索引
https://waterflow.link/articles/1664080524986
1、未知的枚举值
我们现在定义一个类型是unit32的Status,他可以作为枚举类型,我们定义了3种状态
type Status uint32
const (
StatusOpen Status = iota
StatusClosed
StatusUnknown
)
其中我们使用了iota,相关的用法自行google。最终对应的状态就是:
0-开启状态,1-关闭状态,2-未知状态
现在我们假设有一个请求参数过来,数据结构如下:
{
"Id": 1234,
"Timestamp": 1563362390,
"Status": 1
}
可以看到是一个json类型的字符串,其中就包含了Status状态,我们的请求是希望把状态修改为关闭状态。
然后我们在服务端创建一个结构体,方便把这些字段解析出来:
type Request struct {
ID int `json:"Id"`
Timestamp int `json:"Timestamp"`
Status Status `json:"Status"`
}
好了,我们在main中执行下代码,看下解析是否正确:
package main
import (
"encoding/json"
"fmt"
)
type Status uint32
const (
StatusOpen Status = iota
StatusClosed
StatusUnknown
)
type Request struct {
ID int `json:"Id"`
Timestamp int `json:"Timestamp"`
Status Status `json:"Status"`
}
func main() {
js := `{
"Id": 1234,
"Timestamp": 1563362390,
"Status": 1
}`
request := &Request{}
err := json.Unmarshal([]byte(js), request)
if err != nil {
fmt.Println(err)
return
}
}
执行后的结果如下:
go run main.go
&{1234 1563362390 1}
可以看到解析是没问题的。
然而,让我们再提出一个未设置状态值的请求(无论出于何种原因):
{
"Id": 1234,
"Timestamp": 1563362390
}
在这种情况下,请求结构的状态字段将被初始化为其零值(对于 uint32 类型:0)。因此,StatusOpen 而不是 StatusUnknown。
最佳实践是将枚举的未知值设置为 0:
type Status uint32
const (
StatusUnknown Status = iota
StatusOpen
StatusClosed
)
在这里,如果状态不是 JSON 请求的一部分,它将被初始化为 StatusUnknown,正如我们所期望的那样。
2、指针无处不在?
按值传递变量将创建此变量的副本。而通过指针传递它只会复制内存地址。
因此,传递指针总是会更快,对么?
如果你相信这一点,请看看这个例子。这是一个 0.3 KB 数据结构的基准测试,我们通过指针和值传递和接收。 0.3 KB 并不大,但这与我们每天看到的数据结构类型(对于我们大多数人来说)应该相差不远。
当我在本地环境中执行这些基准测试时,按值传递比按指针传递快 4 倍以上。这可能有点违反直觉,对吧?
这其实与 Go 中如何管理内存有关。我们都知道变量可以分配在堆上或栈上,也知道:
- 栈包含给定 goroutine 的正在进行的变量。一旦函数返回,变量就会从堆栈中弹出。
- 堆包含共享变量(全局变量等)。
让我们看下下面这个简单的例子:
type foo struct{}
func getFooValue() foo {
var result foo
// Do something
return result
}
这里,一个结果变量由当前的 goroutine 创建。这个变量被压入当前堆栈。一旦函数返回,客户端将收到此变量的副本。变量本身从堆栈中弹出。它仍然存在于内存中,直到它被另一个变量擦除,但它不能再被访问。
我们现在修改下上面的例子,使用指针:
type foo struct{}
func getFooPointer() *foo {
var result foo
// Do something
return &result
}
结果变量仍然由当前的 goroutine 创建,但客户端将收到一个指针(变量地址的副本)。如果结果变量从堆栈中弹出,则此函数的客户端无法再访问它。
在这种情况下,Go 编译器会将结果变量转移到可以共享变量的地方:堆。
但是,传递指针是另一种情况。例如:
type foo struct{}
func main() {
p := &foo{}
f(p)
}
因为我们在同一个 goroutine 中调用 f,所以 p 变量不需要被转移。它只是被压入堆栈,子函数可以访问它。
比如在 io.Reader 的 Read 方法中接收切片而不是返回切片的直接结果,也不会转移到堆上。
但是返回一个切片(它是一个指针)会将其转移到堆中。
为什么堆栈那么快?主要原因有两个:
- 堆栈不需要垃圾收集器。正如我们所说,一个变量在创建后被简单地压入,然后在函数返回时从堆栈中弹出。无需进行复杂的过程来回收未使用的变量等。
- 堆栈属于一个 goroutine,因此与将变量存储在堆上相比,存储变量不需要同步。这也导致性能增益。
结论就是:
当我们创建一个函数时,我们的默认行为应该是使用值而不是指针。仅当我们想要共享变量时才应使用指针。
最后:
如果我们遇到性能问题,一种可能的优化可能是检查指针在某些特定情况下是否有帮助。使用以下命令可以知道编译器何时将变量转移到堆中:go build -gcflags "-m -m"。(内存逃逸)
3、中断 for/switch 或 for/select
我们看下下面的代码会发生什么:
package main
func f() bool {
return true
}
func main() {
for {
switch f() {
case true:
break
case false:
// Do something
}
}
}
我们将调用 break 语句。但是,这会破坏 switch 语句,而不是 for 循环。
相同的情况还会出现在fo/select中,像下面这样:
package main
import (
"context"
"time"
)
func main() {