当前位置: 首页 > news >正文

go资深之路笔记(六)坑点

一、 接口(Interface)的内部表示与 nil 的陷阱

interface的内部是有两部分构成,它是一个二元组 (动态类型, 动态值),这样就会出现一个问题,当他赋值一个 nil值时,有类型但是无值,这个接口就不是nil
代码:

func Test22(t *testing.T) {var a anyfmt.Println(a == nil)		// true	// a是的内部二元组是(nil, nil)var m map[string]intfmt.Println(m == nil)		// truea = mfmt.Println(a == nil)	 // false, 因为 a是的内部二元组是(map[string]int, nil)
}

当 需要返回一个接口的时候,如果它本身的值是nil,那应该直接返回nil,而不是接口

func Resp() any {var	res anyvar a map[string]int{}...res = a
//	return res 		// 错误return nil	//正确
}

二、defer 参数传递

defer 函数 传递的是值时,不会受变量修改影响,而传递的是指针就会受变量修改影响。

package mainimport "fmt"func Print(p *int) {fmt.Println("Print value:", *p) // 解引用指针p
}func main() {x := 10xx := &x // xx是指向x的指针// 示例1:defer fmt.Println("Defer value:", *xx)defer fmt.Println("Defer value:", *xx) 		// 打印 10// 示例2:defer Print(xx)defer Print(xx) 		// 打印 20// 修改x的值(xx指向x,所以*xx会变化)x = 20 
}

本质: defer 的运行机制本质上是 “声明时求值参数,退出时执行函数”

三、切片赋值问题

问题一:切片赋值是共享底层数组的,新切片修改值也会影响到初始切片
问题二:但是新切片触发扩容的话将不会是同一个切片
代码:

func TestSlice(t *testing.T) {s1 := []int{1, 2, 3}s2 := s1[:3]                          // s1 和 s2 共享底层数组 [1, 2, 3, 4, 5]fmt.Printf("%p,%p\n", &s1[0], &s2[0]) // // 指针一致s1[1] = 555fmt.Println(s1, s2) // [1 555 3] [1 555 3]   问题一:同步修改// s2 的容量足够追加,不会扩容,直接在原数组上修改s2 = append(s2, 100)fmt.Println(cap(s1), cap(s2))fmt.Printf("%p,%p\n", &s1[0], &s2[0]) // 问题二:指针不一致(扩容了)fmt.Println(s1, s2)                   // [1 555 3] [1 555 3,100]
}

执行结果:
在这里插入图片描述

解决方法:
问题1:

// 使用 copy
s2 := make([]int, 3)	// 新开辟内存空间,保证互不影响
copy(s2, s1[:3])	// 这里是值传递(ps:如果是 *int,那就要一个个取了)

问题2:

	int n = 9999	// 预设一个足够的值,这个根据实际需求,定s1 := make([]int, n)...s2 := s1[:3]    

对于问题2其实尽量不考虑新切片扩容还要保持一致的问题,既然是新切片那就应该是值传递的。那么说,其实问题1的方法也能解决问题2。

四、 slice 和map初始化

“引用”类型的变量定义后是nil, 对其操作会报错

var s []int				// s== nil
var m map[int]int	// m== nil
fmt.Println(s[0])		// panic 指针错误
fmt.Println(m[0])

通过make之后才开始分配内存空间

s := make([]int,3)
fmt.Println(s[0])
m := make(map[int]int)
fmt.Println(m[0])

但是有一点比较特殊的是, append对切片做了封装:

var s []int	
s = append(s,10)
fmt.Println(s[0])		// 打印10
//大概原理是 append 会判断 s是否nil 然后给它初始化,它还有自动扩容的功能
// 不清楚这个原理的程序员,写 struct字段里有 切片和map的,有时就会疑惑,为什么同样切片没事,但是map有事。

五、嵌入重名

type A struct {Name string
}
type B struct {Name string
}
type C struct {AB
}func main() {c := C{}// c.Name = "name" // 编译错误:ambiguous selector c.Namefmt.Println(c.A.Name) // 必须显式指定嵌入类型的名字
}

其实c.Name的访问只是语法糖而已,如果没有重复的话,那就不需要指定嵌入类型。

六、字符串(string)与字节切片([]byte)的转换开销

str := "Hello World"
bytes := []byte(str) // 可能发生一次内存分配和复制
str2 := string(bytes) // 同样可能发生一次内存分配和复制

原因是他们底层结构不一样, string 相当于一个数组,不可变;切片容量可变

// string 的底层结构(reflect.StringHeader)
type StringHeader struct {Data uintptr  // 第1个字段:指向字节数组的指针Len  int      // 第2个字段:长度(字节数)
}// []byte 的底层结构(reflect.SliceHeader)
type SliceHeader struct {Data uintptr  // 第1个字段:指向字节数组的指针(与 StringHeader.Data 位置相同)Len  int      // 第2个字段:长度(与 StringHeader.Len 位置相同)Cap  int      // 第3个字段:容量(string 不需要这个字段)
}

其实一般业务代码怎么写是完全没有问题的,但是如果对性能要求特别可以采用类型转化的方式

import "unsafe"func StringToBytes(s string) []byte {return *(*[]byte)(unsafe.Pointer(&struct {stringint}{s, len(s)}))
}func BytesToString(b []byte) string {return *(*string)(unsafe.Pointer(&b))
}
// 警告:使用此方法后,绝对不能再修改原始的 []byte,否则会破坏 string 不可变的约定。(string底层是数组)

StringToBytes讲解:

unsafe.Pointer(&struct {stringint}{s, len(s)}

底层表示

struct.string = StringHeader(uintptr+int)
+struct.int 
//相当于
SliceHeader  = (uintptr+int+int)

所以这个结构体就满足 了转化成 SliceHeader 的结构。
BytesToString讲解

SliceHeader(uintptr+int+int)
// 转化成
StringHeader(uintptr+int)
//不需要做额外处理,已经满足了(uintptr+int) |多的cap int 弃用即可

七、方法接收者(Method Receiver)的选择

方法接收者一般都用指针,因为用值的话,他会拷贝一份,影响性能:
比如下面的代码两个指针的打印是不同的,就证明他们发生了深拷贝:

func Test000(t *testing.T) {a := A{Name: "lll"}fmt.Printf("%p\n", &a)a.print()
}type A struct {Name string
}func (a A) print() {println(a.Name)fmt.Printf("%p\n", &a)
}

原因: 因为方法接受者其实也是一个参数:

func (a A) print() {println(a.Name)fmt.Printf("%p\n", &a)
}
// 等价于
func  print(a A){println(a.Name)fmt.Printf("%p\n", &a)
}

值传递一般用作:不修改原始数据的情况
其实指针传递也可以做到不修改原始值的情况,比如:

func (a *A) print() {a2 := *aa2.Name = "name"		// 不影响原始值println(a.Name)fmt.Printf("%p\n", &a)
}

八、 time.After 可能导致的内存泄漏

time.After 一般只适用简单+单次的计时器使用,如果在循环中使用可能创建大量的 计时器,比如:

for {select {case <-time.After(5 * time.Minute): // 每次循环都创建一个新的 5min Timer!returncase <-someOtherChan:// do something}
}

为什么会创建大量的计时器:

  1. select 运行时就会执行 <-time.After 从而创建计时器
  2. <-time.After 没有取消功能,所以创建后必须到期才会销毁。
  3. for 会一直循环。

解决方法: 改成可复用的 time.NewTimer

// 正确的做法
timer := time.NewTimer(5 * time.Minute)
defer timer.Stop() // 确保Timer被停止,以便资源得以释放for {timer.Reset(5 * time.Minute) // 关键:在每次循环开始时重置Timerselect {case <-timer.C:returncase <-someOtherChan:// do something}
}

九、在关键函数上 defer recover

有时候创建很多子协程工作,某个子协程崩溃会导致所有协程都结束工作(传到主协程,然后终止自身和所有子协程),如果只是加打印未必能看出问题来, 因为有可能你在a协程打印出一半的内容,b协程panic了然后全部挂掉了。最好的做法就是,给关键函数加上 defer recover,然后再排查问题。之后在对容易panic的函数加 defer recover,比如:

func checkPanic(){	//	封装处理 recover的容器。if err := recover(); r!=nil{...}
}func main(){defer checkPanic()...
}

十、 切片作为函数参数时,len和cap的传递语义

直接看代码

func appendToSlice(s []int) {s = append(s, 100) // 这里可能修改 s 的底层数组,但外部的 s1 的 len 不会改变s[0] = 999         // 这个修改会影响底层数组,外部能看到
}func main() {s1 := make([]int, 1, 5) // len=1, cap=5s1[0] = 1appendToSlice(s1)fmt.Println(s1) // 输出: [999],而不是 [999, 100]// s1 的 len 仍然是 1,因为函数内对切片头部的修改是局部的
}

底层原理:
切片实际上就是三个字段组成的结构体,

type SliceHeader struct {Data uintptr  // 值传递的时候Data 的值不变,指向同一块内存,所以可以做到同步修改.Len  int     	// 这个值不是指针,所以分出来的切片不共享 lenCap  int    		// 同上
}

解决方法:

// 方法一: 返回切片
func appendToSliceFixed(s []int) []int {return append(s, 100) // 返回新的切片头部
}
// 方法二: 切片指针
func aa(s *[]int) {*s = append(*s, 100) // 返回新的切片头部
}

十一: 使用 sync.Map 的适用场景误区

陷阱:sync.Map 并不是 map 的并发安全替代品。它针对特定场景优化:读多写少,且 key 相对稳定。在错误的场景下使用,性能可能比 map + sync.RWMutex 更差。

适用场景对比:
sync.Map 适用:大量并发读,写操作很少(如缓存预热后很少更新,常见配置)
map + sync.RWMutex 适用:读写操作相对均衡,或者需要复杂的原子操作
ps: 以上两种都是并发安全的, 而 map本身不是并发安全,如果有并发操作 map 可能会引发panic

十二: 编译器优化导致的基准测试(Benchmark)误差

陷阱:Go 编译器非常激进,如果基准测试编写不当,编译器可能会优化掉你想要测试的代码,导致结果严重失真。

// 错误的基准测试:结果会严重失真
func BenchmarkSum(b *testing.B) {numbers := make([]int, 1000)for i := range numbers {numbers[i] = i}b.ResetTimer()for i := 0; i < b.N; i++ {sum := 0for _, n := range numbers {sum += n // 编译器可能发现 sum 未被使用,直接优化掉整个循环!}}
}// 正确的基准测试:阻止编译器优化
func BenchmarkSumFixed(b *testing.B) {numbers := make([]int, 1000)for i := range numbers {numbers[i] = i}b.ResetTimer()for i := 0; i < b.N; i++ {sum := 0for _, n := range numbers {sum += n}// 阻止优化:将结果赋值给包级变量或调用 runtime.KeepAliveruntime.KeepAlive(sum)}
}

十三: 字符串长度与字节长度的区别

陷阱:len() 函数对字符串返回的是字节数,不是字符数(对于Unicode字符)。

s := "Hello, 世界"
fmt.Println("Bytes:", len(s))    // 输出: 13 (UTF-8编码的字节数)
fmt.Println("Runes:", utf8.RuneCountInString(s)) // 输出: 9 (实际字符数)// 遍历字符串时的陷阱
for i := 0; i < len(s); i++ {fmt.Printf("%c ", s[i]) // 错误:按字节遍历,会输出乱码
}
// 正确遍历方式
for _, r := range s {fmt.Printf("%c ", r) // 按rune遍历
}

十四: 初始化循环

陷阱:包级变量的初始化顺序可能导致循环依赖,编译期无法检测,运行时panic。

var a = b + 1
var b = a + 1 // 运行时panic: initialization cycle// 更隐蔽的版本
var config = loadConfig()
func loadConfig() Config {return Config{Timeout: config.Timeout} // 间接循环引用
}

原理解析:
这两个变量声明初始化过程:

var a = b + 1
var b = a + 1
  1. 声明 变量 a,b 并分配内存 a=0,b=0
  2. 初始化a = b+1,这个时候b没有初始化(注意有默认值!=初始化),先判断a依赖b
  3. 初始化b = a+1,这个时候a没有初始化,检查 a,b循环依赖,panic!

config 同理

解决方法:
使用init

var a,b int	// 这个时候已经用默认值初始化了
func init(){a = b + 1b = a + 1
}

关于类型确定:
声明阶段,哪怕是存在循环依赖,但是知道 a,b互相依赖肯定是同一类型,会自动推导为默认类型 int

十五:接口比较的隐式Panic

陷阱: 比较两个接口值时,如果它们的动态类型不可比较(如slice、map、function),会引发运行时panic。

var x interface{} = []int{1, 2, 3}
var y interface{} = []int{1, 2, 3}
fmt.Println(x == y) // panic: runtime error: comparing uncomparable type []int

如果去掉 interface{} 编译阶段过不了,而有接口的话比较隐晦看不出来。
slice、map、function 是不可比较类型,任何包含这个三个类型的变量都不可比较

十六、comma-ok语法判断

比如:从已关闭的通道读取不会阻塞,而是立即返回零值,需要用comma-ok语法判断通道是否关闭。

ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)for i := 0; i < 3; i++ {value, ok := <-chif !ok {fmt.Println("Channel closed!")break}fmt.Println("Received:", value)
}
// 输出: 
// Received: 1
// Received: 2  
// Channel closed!

除了读取通道外,还有:

  1. 类型断言
  2. map读取
  3. switch v := i.(type) { 这里是隐形comma-ok 断言

十七、方法值(Method Value)的延迟绑定

陷阱:方法值在创建时就绑定了接收者,后续对原变量的修改不会影响已创建的方法值。

type Counter struct{ count int }
func (c *Counter) Inc() { c.count++ }c1 := &Counter{count: 1}
c2 := &Counter{count: 2}method := c1.Inc // 绑定到c1
c1 = c2          // 改变c1的指向method()         // 仍然影响原来的c1指向的对象?不,影响的是创建时绑定的对象
fmt.Println(c2.count) // 输出: 2 (未改变)

扩展:方法 method 的底层原理
当执行 method := c1.Inc 时,编译器会生成一个闭包结构体,用于存储:
原方法的函数指针(即上述 Inc 函数的地址)
绑定的接收者实例(c1 的指针)
伪代码:

// 编译器生成的闭包结构体(伪代码)
type methodValue struct {fn   func(*Counter)  // 原方法的函数指针(Inc)recv *Counter        // 绑定的接收者(c1)
}

method()相当于:

// 调用 method() 时,编译器实际执行的逻辑(伪代码)
methodValue.fn(methodValue.recv)  // 即 Inc(c1),也就是 c1.Inc()

方法值的底层实现本质是通过闭包 “捕获” 接收者,从而实现:
简化调用:调用时无需显式传递接收者(已绑定)
函数化传递:让方法可以像普通函数一样作为参数传递(例如作为回调函数)

十八:切片扩容的容量策略不确定性

陷阱:切片扩容时的新容量取决于当前容量和追加的元素数量,不同Go版本策略可能不同。

func  testSlice(){s := make([]int, 1023, 1024)fmt.Println("Before:", len(s), cap(s)) // 2, 3s = append(s, 1, 2)                   // 需要扩容fmt.Println("After:", len(s), cap(s)) // 1025,cap可能等于1536(1.5),也可能等于 1280(1.25),或者2048(2)
}

所以代码里面千万不要根据具体扩容值来写代码,不然版本改变可能有意想不到的bug
扩容倍数历史:

1.5  扩容两倍
1.6-1.17 : <=1024 两倍>1024 1.251.18+继承 1.6做内存对齐优化
1.20+继承 1.18采取更平缓的扩容曲线

十九: 零值(Zero Value)的合理使用与误用

陷阱:Go的零值机制很实用,但有时会导致意外的行为。

type Config struct {Timeout time.DurationRetries int
}func main() {var cfg Config// cfg.Timeout是0,表示无限等待?这可能不是想要的行为if cfg.Timeout == 0 {cfg.Timeout = time.Second * 30 // 需要显式设置默认值}
//     或者if cfg.Timeout == 0 {
//       ... 无限等待逻辑}
}

总结:就是对于默认零值要做特殊处理,避免直接使用的习惯。

二十: 位运算的优先级陷阱

陷阱:位运算符的优先级不同于其他语言,容易写错复杂的表达式。

// 常见的错误
x := 1 | 2 == 3   // 等价于 1 | (2 == 3),结果是 1 | false = 1
y := (1 | 2) == 3 // 正确写法,结果是 truefmt.Println(x, y) // 输出: 1 true

解决方案:不确定时多用括号

二十一:类型断言的性能与安全

陷阱:类型断言在失败时有两种行为,选择不当会影响性能或安全性。

var i interface{} = "hello"// 方式1:安全但性能稍差(需要返回bool)
if s, ok := i.(string); ok {fmt.Println(s)
}// 方式2:不安全但简洁(失败时panic)
s := i.(string)
fmt.Println(s)// 方式3:type switch,处理多种类型
switch v := i.(type) {		// 这里采用隐形 comma-ok语法判断,不会panic.
case string:fmt.Println("string:", v)
case int:fmt.Println("int:", v)
default: //断言失败fmt.Println("unknown")	
}

二十一:函数参数求值顺序的不确定性

陷阱:函数参数的求值顺序在Go语言规范中是未定义的,不同编译器可能不同。

func Test000(t *testing.T) {i := 0printValues(i, i+1) //这里绝对是0,1(不管哪个表达式先求值都一样,因为i不变)// 但如果是函数调用:printValues(getValue(), getValue()) // 两个getValue()的调用顺序不确定 结果可能是2,3 也可能是3,2
}
func printValues(a, b int) {fmt.Println(a, b)
}func getValue() int {a++return a
}

为什么参数表达式的计算顺序不一致:
Go 允许编译器自由选择参数求值顺序,主要是为了优化编译效率。编译器可以根据上下文选择最高效的求值顺序(如利用 CPU 缓存、减少临时变量等),无需严格遵守从左到右的顺序。


func Test000(t *testing.T) {i := 0printValues(i, i+1) //这里绝对是0,1(不管哪个表达式先求值都一样,因为i不变)// 但如果是函数调用:a:=	getValue()b:= getValue()printValues(a,b) 
}

二十二:递归类型的定义限制

陷阱:在定义递归类型时,不能直接包含自身,必须通过指针间接引用。

// 错误定义
type TreeNode struct {Value    intChildren []TreeNode // 编译错误:invalid recursive type TreeNode
}// 正确定义
type TreeNode struct {Value    intChildren []*TreeNode // 通过指针间接引用
}

原因:值类型嵌套不好确定内存布局
所以一半都是 用指针类型
常见场景:

  1. 链表
  2. JSON/XML 解析模型

二十三: 浮点数的精度与比较问题

陷阱:浮点数有精度限制,直接比较可能得到意外结果。
解决方法:使用误差范围

a := 0.1
b := 0.2
c := 0.3fmt.Println(a+b == c) // 输出: false!
fmt.Println(a + b)    // 输出: 0.30000000000000004// 正确比较方式
fmt.Println(math.Abs((a+b)-c) < 1e-9) // 使用误差范围

**扩展:**如果需要完全精确可以使用高精度十进制库(适合严格精度场景)
安装依赖 decimal

go install github.com/shopspring/decimal

示例代码:

package mainimport ("fmt""github.com/shopspring/decimal"
)func main() {a, _ := decimal.NewFromString("0.1")b, _ := decimal.NewFromString("0.2")c, _ := decimal.NewFromString("0.3")sum := a.Add(b)fmt.Println(sum.Equal(c)) // 输出 true(精确计算)
}

源码浅析:

type Decimal struct {value *big.Int		// 整数部分//注意(vadim):这必须是一个int32,因为我们在计算过程中将其转换为float64。如果exp是64位,我们可能会失去精度。如果我们关心能够表示每一个可能的小数,我们可以使exp a*big.Int,但这会损害性能,这样的数字是不切实际的。exp int32		//小数部分
}

原理就是 value 是整数部分,exp是小数部分计算的时候变成 float32计算值

二十四:标签(Struct Tags)的解析规则

陷阱:结构体标签的解析有严格的语法规则,微小的格式错误会导致标签被忽略。

type User struct {Name string `json: "name"`   // 错误!冒号后不能有空格Age  int    `json:"age"`     // 正确City string `json:"city,omitempty"` // 正确:带选项
}// 反射读取标签
t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag.Get("json")) // 输出空字符串(因为标签解析失败)

扩展:tag标签可以自定义,但是约定成俗的规矩是 grom 用 grom:column, 序列化用json

二十五:运行时类型识别(RTTI)的性能代价

陷阱:过度使用反射或类型断言会影响性能,特别是在热点代码路径中。

// 反射的性能代价
func usingReflection(v interface{}) {rv := reflect.ValueOf(v)if rv.Kind() == reflect.String {s := rv.String() // 反射操作比直接调用慢10-100倍}
}// 类型断言的性能代价(相对较小,但仍需注意)
func usingAssertion(v interface{}) {if s, ok := v.(string); ok {_ = s // 类型断言有开销,但比反射小得多}
}

优化策略:在性能敏感处避免反射,使用代码生成或具体类型。
代码生成:比如 protobuf的grpc生成代码,虽然代码很多,但是运行很快。属于空间换时间(这点空间微不足道,但是时间很重要)

http://www.dtcms.com/a/412042.html

相关文章:

  • 网站的设计特点有哪些淘宝购物网站
  • question:setTimeOut的精确调用
  • 一般网站建设公司wordpress中文4.8
  • 青岛建设大学招聘信息网站网站运营是做什么的
  • 湖南住房城乡建设厅官方网站房产网站建设
  • 做企业网站用哪个cms哈尔滨模板建站公司
  • 采购寺庙管理软件,技术架构与部署模式很关键
  • 手机制作表白网站html网站建设购物案例
  • Powershell 入门
  • 网站建设一般多少钱一年苏州做网站要多少钱
  • 企业网站能个人备案吗html语言大型网站开发
  • 做服装公司需要什么网站大型 网站的建设 阶段
  • 自己有域名怎么建网站it培训机构培训费用
  • 网站建设虚拟空间东莞软件
  • FC游戏大全(经典+中文+修改版)+高清封面
  • 网站制作咨询莱芜0634技术支持 宿州网站建设
  • Linux驱动:系统分配设备号、杂项设备驱动、编译模块(动态加载)、驱动和设备分离
  • 站长工具seo综合查询引流网站建设一般花多少费用
  • 李继红跪舔坊网站建设南通seo网站价格
  • 梦回童年,将JSNES 游戏模拟器移植到 HarmonyOS 移植指南
  • vue中process.env.NODE_ENV设置方法
  • 数据库造神计划第二十一天---JDBC编程
  • 广州找公司建网站郑州网红打卡地
  • 百度网站托管成都高端网站建设公司哪家好
  • 【STM32H7】QuadSPI读写W25Q64JV
  • 成都青羊网站建设网页升级防问广大
  • Altium Designer 21 (十)DRC验证和设计文件输出
  • dw手机销售网站制作seo 网站制作
  • 【精品资料鉴赏】104页银行核心业务流程梳理和优化方案
  • 磁共振成像原理(理论)13:选层 (Slice Select) -布洛赫方法