簡單聊聊Go語言中空結構體和空字符串的特殊之處
在日常的編程過程中,大家應該經(jīng)常能遇到各種”空“吧,比如空指針、空結構體、空字符串……代碼中的這些”空“往往是特例,都有特殊的性質(zhì)。
本文就以 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 個變量的內(nèi)存地址竟然完全一樣!
u 和 v 的內(nèi)存地址相同就已經(jīng)有點出乎意料了,畢竟它們的類型不同,一個是 struct U 的實例(值),一個是 struct V 的實例。但更出乎意料的是,這個內(nèi)存地址竟然還是變量 i 的地址(如下圖)。

這是因為 struct U 和 struct V 都是空結構體這種特殊的結構體,而空結構體的實例,即 struct{}{},不占用任何存儲空間,圖中自然也就找不到存儲著 struct{}{} 的空間。
不占用存儲空間且內(nèi)存地址相同,這就是空結構體這種“空”的特點。
更有意思的是,既然 u (或 v)的地址就是變量 i 的地址,那通過 u 應該也能讀出存儲在 0xc000046730 這個位置的整數(shù) 10吧。 讓我們來試一試。
println(*(*int)(unsafe.Pointer(&u)))
果然可以!
下面我們再來看看另一種“空”——空字符串。
Go 語言中的空字符串
下面這段代碼會輸出什么呢?交替出現(xiàn)的 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
下面就來分析一下背后的原因(從本節(jié)的標題也能猜出吧,八成和title = ""這里的空字符串有關)。
首先,由倒數(shù)第 3 行的 /tmp/sandbox3517788398/prog.go:9 +0x5a 可見,導致 panic 的代碼是第 9 行的 fmt.Println(title)。
而 “破案”的線索就在報錯信息的這一行(第 7 行):
fmt.(*fmt).padString(0x425000000041f01c?, {0x0, 0x4})
接下來我們先找出 fmt.padString() 函數(shù)的定義。
// 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} 對應的正是參數(shù) 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 的空字符串”又是怎么產(chǎn)生的呢?
罪魁禍首就在這一對兒協(xié)程上,
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
而當這一對兒協(xié)程并發(fā)執(zhí)行時,以上 2 組“語句”的執(zhí)行順序是不確定的,完全有可能出現(xiàn)以下二者交替執(zhí)行情況:
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空結構體和空字符串內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
探索Golang?Redis實現(xiàn)發(fā)布訂閱功能實例
這篇文章主要介紹了Golang?Redis發(fā)布訂閱功能實例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01
Golang使用ttl機制保存內(nèi)存數(shù)據(jù)方法詳解
ttl(time-to-live) 數(shù)據(jù)存活時間,我們這里指數(shù)據(jù)在內(nèi)存中保存一段時間,超過期限則不能被讀取到,與Redis的ttl機制類似。本文僅實現(xiàn)ttl部分,不考慮序列化和反序列化2023-03-03

