Golang日常开发闭坑指南
从其他语言转到Go语言,往往会有路径依赖;Go语言核心数据结构比较随意的设计风格,初学者往往会陷入很多坑位。
https://go.dev/doc/effective_go golang官方出的快速指南,值得深度。
1. 核心数据结构
-
值类型包括:所有integer、所有float、bool、string(+rune)、数组和 struct
-
指针类型
Go是类C语言,只有值类型和指针类型,没有明确的引用类型的概念, 只不过利用strut类型和指针,能做到C#引用类型的效果。
2. 切片slice是结构体
slice是一个结构体,切片s执行unsafe.Sizeof(s) = 24
type slice struct { array unsafe.Pointer // 底层数组指针 len int // 内容长度 cap int // 底层数组容量 }
s = make([]byte,5)
当我们对s
进一步切片: s =s[2,4]
2.1 append(slice) 不需要显式初始化
slice需要通过make初始化完才可以使用,有一个例外,不初始化就可以使用:append。
但是append有一个陷阱,可能不会原地扩容。
带来的问题是: 如果slice容量不够长, 底层会新产生数组来承载数据,但是append本身不会去更新 slice 结构体中的底层指针,这就很反人类,append函数签名建议用 append的操作结果重新赋值slice。
var slice = make([]int, 5) // 长度为0, 容量为5的切片 fmt.Printf("before append: %p: %v : %d \n", &slice, slice, len(slice)) _ = append(slice, 4, 5, 6, 5, 4) fmt.Printf("after append: %p: %v : %d \n", &slice, slice, len(slice)) output: before append: 0xc0004be048: [] : 0 after append: 0xc0004be048: [] : 0 // 原始切片不受影响
改成slice = append(slice, 4, 5, 6, 5, 4) 才能满足业务意义。
这里最核心的是因为golang append函数是传值, 扩容后的影响作用在副本上, 要达成业务意义,需要使用信道副本切片,故一般的实践是将append副本切片重新赋值给 原切片。
2.2 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 }
3.1 map需要先初始化,才能使用, 否则会panic
可采用字面量或者make关键字初始化
mm := map[string]string{"k1": "v1", "k2": "v2"} // map字面量 m := make(map[string]float64, 5) m["pi"] = 3.14
3.2 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") } }
3.3 对map做for-range输出是随机的, 但是json.Marshal、fmt.Printf是固定排序
说一个题外话: JSON字符串的值可以是null, 当json.Unmarshal反序列化时,null被反序列化为指定字段的零值。
4. channel 是指向hchan结构体的指针
下面演示向nil信道读写, block for ever的情况
func useChan() { var ch chan int go func() { ch = make(chan int) ch <- 1 }() //time.Sleep(time.Second * 1) fmt.Println(<-ch) }
上面的代码会阻塞(即使后面有goroutine产生了非nil的信道);
解除注释代码, 程序不会阻塞。
下面演示信道有值,但是突然关闭信道的情况,再写值会panic
var ch chan int = make(chan int, 10) go func() { for i := 0; i < 20; i++ { ch <- i } }() time.Sleep(time.Second * 1) close(ch) for ele := range ch { fmt.Println(ele) } output: 0,1,2,3...9 panic: send on closed channel
说明能从关闭的信道中读取值,再发送值会panic。
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 }
信道是golang中的顶级公民,分为两种: 无缓冲和有缓冲信道(先入先出)。
分别用于goroutine同步和异步生产消费:
无缓冲信道: 若没有反向的goroutine在做动作, 当前goroutine会阻塞.
有缓冲信道: goroutine 直接面对的是缓冲队列, 队列满则写阻塞, 队列空则读阻塞。
具体实现: 内部是有一个
- 环形队列 (buf、dataqsize、sendx、recvx 圈定了一个有固定长度,有读写指针控制队列数据的环形队列)
- recvq、sendq存放的不是当前通信的goroutine, 而是处于阻塞状态的goroutine, 也就是:
- 如果 qcount<dataqsiz(队列未满),sendq就为空(写就不会阻塞);
- 如果 qcount>0 (队列不为空),recvq就为空(读就不会阻塞)
一旦解除阻塞,读/写动作会给到 先进入阻塞队列的信道,也就是 recvq、sendq也是先进先出。
一个陷阱: 信道被关闭后, 原来的goroutine阻塞状态不会维系, 能从信道读取到零值。
for range可以用于信道 : 一直从指定信道中读值, 没有数据会阻塞, 直到信道关闭会自动退出循环。
var ch chan int = make(chan int, 10) go func() { for i := 0; i < 20; i++ { // 写入的比较快, ch <- i } close(ch) }() time.Sleep(time.Second * 2) for ele := range ch { fmt.Println(ele) } output: 0,1,2,3,4...19
上面的示例描述了信道4个截断:写完10个数据(阻塞写)、暂停2s、读取10个数据(解除阻塞写)、读完20个数据,关闭。
4.1 select case 多路复用
select case default: 用于多个信道的灯控,类似于多路复用。
这也是与信道密切相关的功能: 在遇到信道阻塞时,有办法让当前goroutine继续执行。
select { case <-chan1: fmt.println("recv from chan1 ") case v, ok:= <-chan2: if !ok { fmt.println("chan2 closed") }else{ fmt.printf("recv from chan2: %v \n", v) } default: fmt.println("execute default case.") }
如果都阻塞了走default;
哪一个不阻塞了走哪个case(没有default的情况下);
如果多个case不阻塞,随机取一个case走, 这里面又涉及,在进入select case语法糖的时候,肯定会有一个信道暂停的状态来检测信道是否阻塞。
https://github.com/golang/go/blob/772f024c615ec13c6cd28bf024e9d6be852201b6/src/runtime/select.go#L122
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 ... }
6. struct{} 类型占用空位为0
变量不一定都占用空间,struct{} 类型指向看了一个固定地址, 不开辟空间。
- 定义chan struct{} 信道, 用于协商goroutine的运行,而不发送数据
- 可用于实现 数学概念的集合(无重复元素): 利用map实现, 值为struct{}{}
- 单纯的定义一个只包含方法的结构体
func worker(ch chan struct{}) { <-ch fmt.Println("do something") close(ch) } func main() { ch := make(chan struct{}) go worker(ch) ch <- struct{}{} }
7. string 是不可修改的结构体
在golang中,字符串是utf-8编码的字节序列, 对字符串执行unsafe.Sizeof(s) =16
type stringStruct struct { str unsafe.Pointer // 指向底层字符数组的指针 len int // 底层数组长度 }
7.1 for-range 字符串,默认情况下是迭代rune
uft-8 是一种变长编码方式,一个字符可能由一个或多个字节组成。
rune 表示一个unicode字符, 可以方便的处理 多字节字符, 故在for-range 字符串时默认是迭代rune而不是字节。
同大多数语言一样,golang的string是不可变的, 可尝试通过byte/rune 中转:
[]byte /[]rune 显式转化为string,最常见和安全的方式,适用于大多数场景:在转换时会创建一个新的字符串,并将字节数组的内容复制到新的字符串中。
7.2 都说string()显式转换[]byte性能比不上 指针方式,那什么时候使用不安全指针方式?
指针方式利用unsafe包来避免数据复制,从而提高性能, 适用于对性能有极高要求且能够接受使用不安全代码的场景。
带来的问题:
1> 内存安全性:
unsafe包的使用会破坏Go语言的内存安全保证。如果[]byte在转换后的string仍在使用时被修改,可能导致数据不一致或者未定义的行为,因为string在Go中应该是不可变的。
2> 数据生命周期:
如果[]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
8. 变量
8.1 变量遮蔽
块内声明的变量会遮蔽上层的同名变量n
func main() { n := 0 if true { n := 1 n++ } fmt.Println(n) // 块内:=声明的变量, 遮蔽了外层变量,块内的操作对外层无影响 。 0 }
8.2 可为空类型
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
8.3 变量类型转换
9. nil值比较
issue: 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.
10. defer-panic()-recover()
10.1 defer 用于打扫战场
- defer用于简化执行清理操作的函数, defer 语法将函数压栈,这些函数在包围的函数返回之后开始出栈(注意是包围的函数返回时出栈,不是代码块)
10.2 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>
10.3 recover 用于捕获异常, 重获执行流, 拒绝程序崩溃
recover 用于(正在panic的goroutine)重新获得控制,recover 只能用在defer函数(这意味着,在常规代码块中调用recover,会返回nil并没有任何效果), 当前goroutine正在panic,调用recover会获得panic时候的参数,并重获执行流。
10.4 runtime 中的panic无法被捕获
业务代码的panic可以通过自己编写recover捕获并恢复控制,但runtime源码中主动throw函数抛出的panic是无法在业务代码中通过recover()函数 捕获的,这点最为致命。
-
- 并发读写map: 对于并发读写 map 的地方,应该对 map 加锁。
-
- 堆栈内存耗尽
-
- 将nil作为goroutine启动
-
- goroutine死锁
-
- 线程限制耗尽, GOLANG 官方定义线程数1w
-
- 超出可用内存
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/17208506.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~