在日常的编程过程中,大家应该经常能遇到各种”空“吧,比如空指针、空结构体、空字符串……代码中的这些”空“往往是特例,都有特殊的性质。
本文就以 go 语言为例,一起来看看空结构体和空字符串在 go 语言中的特殊之处。
首先是空结构体。
go 语言中的空结构体
我们先来运行这样一段代码。
// https://go.dev/play/p/l2yxor8k6qq
package main
type u struct{}
type v struct{}
func main() {
var i = 10
var u = u{}
var v = v{}
println("i address =", &i)
println("u address =", &u)
println("v address =", &v)
}
// 运行结果
// i address = 0xc000046730
// u address = 0xc000046730
// v address = 0xc000046730
i、u、v 这 3 个变量的内存地址竟然完全一样!
u 和 v 的内存地址相同就已经有点出乎意料了,毕竟它们的类型不同,一个是 struct u 的实例(值),一个是 struct v 的实例。但更出乎意料的是,这个内存地址竟然还是变量 i 的地址(如下图)。

这是因为 struct u 和 struct v 都是空结构体这种特殊的结构体,而空结构体的实例,即 struct{}{},不占用任何存储空间,图中自然也就找不到存储着 struct{}{} 的空间。
不占用存储空间且内存地址相同,这就是空结构体这种“空”的特点。
更有意思的是,既然 u (或 v)的地址就是变量 i 的地址,那通过 u 应该也能读出存储在 0xc000046730 这个位置的整数 10吧。 让我们来试一试。
println(*(*int)(unsafe.pointer(&u)))
果然可以!
下面我们再来看看另一种“空”——空字符串。
go 语言中的空字符串
下面这段代码会输出什么呢?交替出现的 sora 和空行吗?
// https://go.dev/play/p/c1zfchdh0rt
package main
import "fmt"
func main() {
title := ""
go func() {
for {
fmt.println(title)
}
}()
for {
go func() {
title = ""
}()
go func() {
title = "sora"
}()
}
}
竟然 painc 了,意不意外?
panic: runtime error: invalid memory address or nil pointer dereference
[signal sigsegv: segmentation violation code=0x1 addr=0x0 pc=0x462cca]
goroutine 18 [running]:
fmt.(*buffer).writestring(...)
/usr/local/go-faketime/src/fmt/print.go:108
fmt.(*fmt).padstring(0x425000000041f01c?, {0x0, 0x4})
/usr/local/go-faketime/src/fmt/format.go:110 +0x24a
...
fmt.println(...)
/usr/local/go-faketime/src/fmt/print.go:314
main.main.func1()
/tmp/sandbox3517788398/prog.go:9 +0x5a
created by main.main in goroutine 1
/tmp/sandbox3517788398/prog.go:7 +0x66
下面就来分析一下背后的原因(从本节的标题也能猜出吧,八成和title = ""这里的空字符串有关)。
首先,由倒数第 3 行的 /tmp/sandbox3517788398/prog.go:9 +0x5a 可见,导致 panic 的代码是第 9 行的 fmt.println(title)。
而 “破案”的线索就在报错信息的这一行(第 7 行):
fmt.(*fmt).padstring(0x425000000041f01c?, {0x0, 0x4})
接下来我们先找出 fmt.padstring() 函数的定义。
// https://github.com/golang/go/blob/master/src/fmt/format.go#l110
// padstring appends s to f.buf, ...
func (f *fmt) padstring(s string) {
对照着定义,可以猜出 {0x0, 0x4} 对应的正是参数 s string。
那再结合字符串类型 string 在 go 语言中的定义,
// https://github.com/golang/go/blob/master/src/internal/unsafeheader/unsafeheader.go#l34
type string struct {
data unsafe.pointer
len int
}
不难推测出,这里相当于我们将 string{data: 0x0, len: 0x4} 这样一个表示字符串的结构体传递给了 fmt.padstring()。而这是一个长度为 4 的空字符串!
这里没有写错,就是长度为 4 的空字符串。
既然长度为 4,那别管空不空,fmt.println() 就要通过存在于 data 中的指针(地址)取出这“4个字符”——计算机就是这么“诚实”。但 data == 0x0,是空指针,当然就空指针 panic 了,即报错信息中的“invalid memory address or nil pointer dereference”。
“案子”是破了,可“长度为 4 的空字符串”又是怎么产生的呢?
罪魁祸首就在这一对儿协程上,
for {
go func() {
title = ""
}()
go func() {
title = "sora"
}()
}
看似通过 = 一下子就能把字符串赋给变量 title,但实际上不得不依次对 data 和 len 赋值,比如,
go routine1: title.data = <空字符串""的地址> = 0x0
go routine1: title.len = <空字符串""的长度> = 0
go routine2: title.data = <字符串"sora"的地址>
go routine2: title.len = <字符串"sora"的长度> = 4
而当这一对儿协程并发执行时,以上 2 组“语句”的执行顺序是不确定的,完全有可能出现以下二者交替执行情况:
go routine2: title.data = <字符串"sora"的地址>
go routine1: title.data = <空字符串""的地址> = 0x0
go routine1: title.len = <空字符串""的长度> = 0
go routine2: title.len = <字符串"sora"的长度> = 4
于是导致了{data: 0x0, len: 0x4},即长度为 4 的空字符串。
painc 的“案子”终于破了。
本文通过两个小例子简单介绍了 go 语言中的“空”,诸位也可以测试测试其他语言中的“空”有什么特性。
到此这篇关于简单聊聊go语言中空结构体和空字符串的特殊之处的文章就介绍到这了,更多相关go空结构体和空字符串内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论