写作背景
最近打算写踩坑系列,把我在开发中遇到的坑总结出来,希望对大家有一些帮助。上一篇我写的 golang map 踩坑系列,建议大家回头看看。 golangmap 我在开发中踩过的坑,你是不是都踩了? - 掘金
本文主要讲结构体和 for 循环的坑。
新手必踩几个坑
结构体的空指针和方法覆盖
结构体是实实在在数据结构,一个结构体可以包含若干个字段,每个字段都有确切的名称和数据类型;另外也包含若干个方法。
结构体的坑,常见有以下 2 个。
空指针异常
如果我们定义一个值为 nil 的结构体变量,那么在这个变量上仍然可以调用该结构体的方法吗?你先先思考下?代码如下
go
type Dog struct {
name string
}
func (d *Dog) Print() {
fmt.Println("hello dog...")
}
func (d *Dog) WithName(name string) {
d.name = name
}
上面这段代码比较简单,我们来看看使用方式
go
func TestNilStruct(t *testing.T) {
var u *Dog
u.Print()
u.WithName("dog")
}
定义了指针变量,执行上面这段代码,结果输出如下
go
=== RUN TestNilStruct
hello dog...
--- FAIL: TestNilStruct (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10f88f8]
goroutine 6 [running]:
testing.tRunner.func1.2({0x110bf20, 0x1208b50})
为什么会 panic 呢?并且提示是空指针,空指针位置是 "d.name = name"。
如果细心的同学会发现结果输出打印了"hello dog...",说明值为 nil 的时候也可以调用该结构体的方法,只是有一些限制。
调用的方法不能依赖该对象的属性,panic 原因是因为依赖了 name属性。
所以,大家定义的指针变量一定要初始化呀,避免零值。我在 go 入门阶段做业务时,被这个问题坑了好久。
结构体嵌套
go
type Class struct {
Name string
ID string
}
func (c *Class) String() string {
return fmt.Sprintf("班级ID:%s,班级名称:%s", c.ID, c.Name)
}
type Student struct {
*Class
Name string
ID string
}
func (s *Student) String() string {
return fmt.Sprintf("学生ID:%s,学生名称:%s", s.ID, s.Name)
}
我申明了 2 个结构体,Class 代表班级,它有 2 个字段,ID 代表班级编号,Name 代表班级名称;另外一个接口题 Student 代表学生,它有 3 个字段,ID 代表学生编号,Name 代表学生名称,另外它嵌套了 Class,这也是我们常用的对象组合方式「对象组合不是继承,跟继承没有半毛钱关系」,他们分别都有一个方法 String()。
第一个坑:方法屏蔽问题
css
func main() {
s := &Student{
Class: &Class{
Name: "一班",
ID: "1",
},
Name: "李四",
ID: "2",
}
fmt.Println(s.String())
}
当我执行上面这段代码,输出的结果是什么?你可以思考下
学生ID:2,学生名称:李四
从结果可以看出 Student 的 String 方法会被调用。嵌入字段Class 的 String 方法被"屏蔽"了。
如果你要调用 Class 的方法代码应该怎么写呢?
css
func main() {
s := &Student{
Class: &Class{
Name: "一班",
ID: "1",
},
Name: "李四",
ID: "2",
}
fmt.Println(s.Class.String())
}
可以改成 s.Class.String()。
第二个坑:嵌套字段是指针,并且使用时不初始化
css
func main() {
s := &Student{
Name: "李四",
ID: "2",
}
fmt.Println(s.Class.String())
}
上段代码执行会报 Panic,所以 struct 嵌套是指针时,一定要初始化,为了保险你可以换成非指针方式。
知识点普及
刚好讲了结构体,顺带把结构体的小知识给大家普及了吧,字面量 struct{} 代表了什么?有什么用处?
我举一个场景,开发中你有若干个主键 ID,通过这批 ID 查询了主键 ID 对应的数据。如果你想判断返回的数据是否有缺失?你肯定会构造一个主键 ID 的 map,遍历返回数据,对比这个 map 就可以了。
我看过太多代码,大家喜欢下面这种方式
go
m := map[string]bool{"xxx":true}
如果你是这么写的,那我今天可以告诉你一个更优雅的写法了,看下面这段代码
go
m := map[string]struct{}{"123": {}}
字面量 struct{} 代表空结构体,它是不占内存的,它不占内存。
for 循环下的闭包
在日常开发中,使用Go语言处理迭代时,为了提高处理效率大家可能会尝试使用协程来并行处理数据(包括我在内),那下面这个坑我敢肯定你一定踩过。
go
type User struct {
Name string
}
func TestLoop(t *testing.T) {
var (
wg sync.WaitGroup
users = []*User{&User{Name: "a"}, &User{Name: "b"}, &User{Name: "c"}, &User{Name: "d"}}
)
for _, val := range users {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(val.Name)
}()
}
wg.Wait()
}
上面代码结果输出如下
diff
=== RUN TestLoop
d
d
d
d
--- PASS: TestLoop (0.00s)
PASS
在循环变量迭代结束后仍然保留对循环变量的引用,此时它会接受一个你不想要的新值。简单概述下,val 变量是在 for 循环开始迭代遍历就创建好了,后面每次迭代遍历都沿用这个变量,下一次迭代就会覆盖上一次变量的值。
要解决这个问题很简单,我常用的 3 种方案
方案一 通过传参
代码如下
go
func TestLoop(t *testing.T) {
var (
wg sync.WaitGroup
users = []*User{&User{Name: "a"}, &User{Name: "b"}, &User{Name: "c"}, &User{Name: "d"}}
)
for _, val := range users {
wg.Add(1)
go func(param *User) {
defer wg.Done()
fmt.Println(param.Name)
}(val)
}
wg.Wait()
}
方案二 定义临时变量
代码如下
go
func TestLoop(t *testing.T) {
var (
wg sync.WaitGroup
users = []*User{&User{Name: "a"}, &User{Name: "b"}, &User{Name: "c"}, &User{Name: "d"}}
)
for _, val := range users {
wg.Add(1)
temVal := val // 定时临时变量
go func() {
defer wg.Done()
fmt.Println(temVal.Name)// 通过临时变量处理数据
}()
}
wg.Wait()
}
上面两种方案都能得到正确的结果
css
=== RUN TestLoop
d
a
b
c
--- PASS: TestLoop (0.00s)
PASS
方案 3 官方标准库的支持
前面两种方案以后要过时了,官方在 Go 1.22 版本修复这个问题了。
简单解释下,在之前的版本,for 循环声明的变量只创建一次,并在每次迭代中更新。在Go 1.22 中,循环的每次迭代都会创建新变量,以避免意外的共享错误,那我必须测试下,没有下载最新 go sdk 不打紧,官方提供了在线调试平台,地址:Go Playground - The Go Programming Language
你看看,go 团队还是很贴心,把我们遇到的问题都解决了。
另外,如果你是细心的网友,for 循环还升级一个特性
sql
"For" loops may now range over integers
for 循环可以迭代整数了,案例如下
go
package main
import "fmt"
func main() {
for i := range 4 {
fmt.Println(10 - i)
}
fmt.Println("go1.22 has lift-off!")
}
算我愚钝,这个场景我暂时用不上了。
最后总结
本文主要讲了 go 结构体、for 循环的坑,我觉得有几点比较重要。
-
对于结构体,我们要小心方法"屏蔽"现象,尤其是存在多个嵌入字段或者多层嵌入的时候。"屏蔽"现象可能会导致代码结果与你的预期不符。
-
对于结构体的指针方法,使用时一定要初始化变量,否则容易造成空指针 Panic;或者你在使用时不要引用结构体内部的变量。
-
for 循环下的闭包问题非常常见,官方在1.22已经修复了,以后在升级版本的时候不用开发者关心了。
-
大家一定要善用 struct{} 空结构体,因为它不占内存。