golang日常开发常见的坑位解析
前言
主要解析了 Go 语言日常开发中的常见坑位,包括核心数据结构(值类型、指针类型、切片、映射、通道、数组等)、变量(遮蔽、可为空类型、类型转换)、字符串特性、nil 值比较,以及 defer、panic、recover 的使用和相关注意事项,还提及了一些运行时的致命错误情况。
1. 核心数据结构
值类型包括: 所有integer、所有float、bool、string(+rune)、array 和 struct
指针类型: Go是类C语言,只有值类型和指针类型,没有明确的引用类型的概念, 只不过利用strut类型和指针,能做到C#引用类型的效果。
2. 切片slice是结构体
slice
是一个结构体,切片s
执行unsafe.Sizeof(s) = 24
type slice struct {
array unsafe.Pointer // 底层数组指针
len int // 内容长度
cap int // 底层数组容量
}
append(slice)
不需要显式初始化
slice
需要通过make初始化完才可以使用,有一个例外,不初始化就可以使用:append
sort.Slice([] s)
是原地排序
var person = []RouteInfo { RouteInfo{}, RouteInfo{} }
sort.Slice(person, func(i, j int) bool { // 原地降序
// return person[i].Id > person[j].Id && person[i].Prefix > person[j].Prefix // 这个是做不到的先按照id,再按照Prefix排序的
if person[i].Id == person[j].Id {
return person[i].Prefix > person[j].Prefix
}
return person[i].Id > person[j].Id
})
3. map是指针
map
是指向一个hmap
结构体的指针,对字典m
执行unsafe.Sizeof(m) =8
Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.
根据源代码中的注释描述 :
map
是由一组哈希槽实现 ,每个哈希槽包含8个键值对; 低位hashcode
用于定位哈希槽,高位哈希值用于在槽内定位每个entry
- 槽内超过8条目,该槽会链接溢出槽
- 触发扩容, 2倍原始大小,增量拷贝。
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
map
需要先初始化,才能使用, 否则会panic
可采用字面量或者make关键字初始化
mm := map[string]string{"k1": "v1", "k2": "v2"} // map字面量
m := make(map[string]float64, 5)
m["pi"] = 3.14
map
中取得不存在的键,会返回零值, 会误导对于该值存在性的判断
func main() {
x := map[string]string{"k1":"v1","k2":"v2"}
if v := x["k3"]; v == "" { // 不存在该键值对,会返回值类型的零值
fmt.Println("①字典该key对应值为"" ②字典不存在该key")
}
}
使用map
取值的返回参数2 bool
值来判断
func main() {
x := map[string]string{"k1":"v1","k2":"v2","k3":""}
if _,ok := x["k3"]; !ok {
fmt.Println("字典不存在该key")
}
}
对map
做for-range
输出是随机的, 但是json.Marshal
、fmt.Printf
是固定排序
说一个题外话: JSON字符串的值可以是
null
, 当json.Unmarshal
反序列化时,null
被反序列化为指定字段的零值。
4.channel 是指向hchan结构体的指针
type hchan struct {
qcount uint // 队列中已有的缓存元素的长度
dataqsiz uint // 环形队列的长度
buf unsafe.Pointer // 环形队列的地址
elemsize uint16
closed uint32
elemtype *_type // 元素类型
sendx uint // 待发送的元素索引
recvx uint // 待接受元素索引
recvq waitq // 阻塞等待的goroutine
sendq waitq // 阻塞等待的gotoutine
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
5. 数组array 是值类型
将数组array作为函数参数传递,函数内的修改不会体现在 原始数组上。
package main
import "fmt"
func changeFunc(arr [3]int) {
arr[0] = 222
}
func main() {
var arr [3]int = [3]int{1, 2, 3}
changeFunc(arr)
for i, item := range arr {
fmt.Printf("index : %d, item: %d \n\r", i, item) // 输出 1,2,3
}
}
6. for-range 语法糖副本/ 全局变量陷阱
golang中除了经典的三段式for
循环外,还有帮助快速遍历 slice
array
map
channel
的 for range
循环。
陷阱的来源:
slice
map
channel
是引用, array
是值。
for-range
语法糖,Golang会在编译期对原切片/数组赋复制给一个新变量 ha,在赋值的过程中就发生了拷贝,且for-range内部的迭代变量是一个全局变量(1.21 之前), 给迭代变量赋值也会形成拷贝。
ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}
7. struct{} 类型占用空间为0
变量不一定都占用空间,struct{}
类型指向看了一个固定地址, 不开辟空间。
- 定义
chan struct{}
信道, 用于协商goroutine
的运行,而不发送数据 - 可用于实现数学概念的集合(无重复元素): 利用
map
实现, 值为struct{}{}
, 只关注key
- 单纯的定义一个只包含方法的结构体
func worker(ch chan struct{}) {
<-ch
fmt.Println("do something")
close(ch)
}
func main() {
ch := make(chan struct{})
go worker(ch)
ch <- struct{}{}
}
8. string 是不可修改的结构体
在golang中,字符串是utf-8
编码的字节序列, 对字符串执行unsafe.Sizeof(s) =16
type stringStruct struct {
str unsafe.Pointer // 指向底层字符数组的指针
len int // 底层数组长度
}
for-range
字符串,默认情况下是迭代rune
uft-8
是一种变长编码方式,一个字符可能由一个或多个字节组成。 rune
表示一个unicode
字符, 可以方便的处理 多字节字符, 故在for-range
字符串时默认是迭代rune而不是字节。
同大多数语言一样,golang的string是不可变的, 可尝试通过byte/rune
中转:
[]byte
/[]rune
显式转化为string,最常见和安全的方式,适用于大多数场景:在转换时会创建一个新的字符串,并将字节数组的内容复制到新的字符串中。
- 都说
string()
显式转换[]byte
性能比不上 指针方式,那什么时候使用不安全指针方式?
指针方式利用 unsafe包来避免数据复制,从而提高性能, 适用于对性能有极高要求且能够接受使用不安全代码的场景。 带来的问题:
- 内存安全性:
unsafe
包的使用会破坏Go语言的内存安全保证。如果[]byte
在转换后的string仍在使用时被修改,可能导致数据不一致或者未定义的行为,因为string在Go中应该是不可变的。
- 数据生命周期:
如果[]byte
的底层数组在string仍在使用时被垃圾回收或者被其他数据覆盖,可能导致string指向无效内存区域。这会引发程序崩溃或产生不可预测的行为。
import (
"reflect"
"unsafe"
)
func UnsafePointer() {
var b = []byte{'h', 'e', 'l', 'l', 'o'}
var s string = *(*string)(unsafe.Pointer(&b))
fmt.Printf("%s\n", s) // 这里演示了通过unsafe.pointer 将[]byte 转换为string的效果
b[0] = 'H'
fmt.Printf("%s\n", s)
s = "我们都有一个家" // string 是不可修改的,直接对string赋值实际是新申请空间去存储
for _, c := range s { // 这里编译器会提示迭代的元素类型是 rune
fmt.Printf("%c", c)
}
for _, c := range b {
fmt.Printf("%c", c)
}
}
-- output
hello
Hello
我们都有一个家Hello
9. 变量
- 变量遮蔽
块内声明的变量会遮蔽上层的同名变量n
func main() {
n := 0
if true {
n := 1
n++
}
fmt.Println(n) // 块内:=声明的变量, 遮蔽了外层变量,块内的操作对外层无影响 。 0
}
- 可为空类型
golang 没有可为空类型这样的说法, 但是实际业务中我们要体现 null
true
false
这样的可为空值, 这在序列化时 null
值会被忽略KV。
golang 可以使用指针类型达到C#可为空类型的效果:
// 定义
ControlByJean *bool `json:"controlByJean,omitempty" bson:"controlByJean,omitempty" cfg:"controlByJean" web:"健康检查规则由jean控制"`
具体赋值时:
t = false
ControlByJean = &t
ControlByJean = nil
10. nil值比较
golang中:一个接口等于另一个接口,前提是它们的类型和动态值相同。这同样适用于nil值。
func Foo() error {
var err *os.PathError = nil
return err
}
func main() {
err := Foo()
fmt.Println(err) // print: <nil>
fmt.Println(err == nil) // print: false
}
solution
: 强转为同一类型
fmt.Println(err == (*os.PathError)(nil)) // print: true
或者显式返回nil error
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
在底层,接口被实现为两个元素,一个类型T和一个值V,V是一个具体的值,比如int、结构体或指针,而不是接口本身,它的类型是T
, 上面的错误示例中: err 具备了T=*MyError, V=nil
的实现,故与nil
不等。
只要记住,如果接口中存储了任何具体的值,该接口将不会为nil.
11. defer-panic()-recover()
defer 用于打扫战场
defer
用于简化执行清理操作的函数, defer
语法将函数压栈,这些函数在包围的函数返回之后开始出栈(注意是包围的函数返回时出栈,不是代码块)
panic 系统异常/也可主动触发, 向上冒泡
-
panic
终止原始控制并开始panic
,当F函数调用panic
时, F函数执行终止,包围F函数的defer
函数正常执行,之后执行流返回给上层调用者,对于上层调用者而言,F函数现在就像是panic()
。 -
以上过程持续向上冒泡,直到当前
goroutine
中所有函数都能返回,这个时候程序崩溃, 所以要注意goroutine
的健壮性。
panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses
一个比较好的实践是:打印错误和堆栈
func testDefer() *int64 {
defer func() {
if err := recover(); err != nil {
fmt.Println("stacktrace from panic:" + string(debug.Stack())) // 打印错误堆栈
}
}()
var a int64 = 1
panic("panic....")
return &a
}
func main() {
var ss *int64 = testDefer()
fmt.Printf("ss=%+v\n", ss)
}
注意: testDefert函数内panic
之后, 执行流立即回到defer recover()
, return &a
不会得到执行, 那么定义的临时返回变量将保持指针的零值, 未被赋值。
output:
stacktrace from panic:goroutine 1 [running]:
runtime/debug.Stack()
/usr/local/go/src/runtime/debug/stack.go:24 +0x65
main.testDefer.func1()
/Users/admin/test/test_sync_one/main.go:37 +0x2f
panic({0x10987c0, 0x10c9680})
/usr/local/go/src/runtime/panic.go:884 +0x213
main.testDefer()
/Users/admin/test/test_sync_one/main.go:41 +0x65
main.main()
/Users/admin/test/test_sync_one/main.go:48 +0x2b
ss=<nil>
recover 用于捕获异常, 重获执行流, 拒绝程序崩溃
recover
用于(正在panic
的goroutine
)重新获得控制,recover
只能用在defer
函数(这意味着,在常规代码块中调用recover
,会返回nil
并没有任何效果), 当前goroutine
正在panic
,调用recover
会获得panic
时候的参数,并重获执行流。
runtime 中的panic无法被捕获
业务代码的panic
可以通过自己编写recover
捕获并恢复控制,但runtime
源码中主动throw
函数抛出的panic
是无法在业务代码中通过recover()
函数捕获的,这点最为致命。
- 并发读写
map
: 对于并发读写map
的地方,应该对map
加锁。 - 堆栈内存耗尽
- 将
nil
作为goroutine
启动 goroutine
死锁- 线程限制耗尽, Golang官方定义线程数1w
- 超出可用内存