Golang的strings.Split()踩坑記錄
背景
工作中,當我們需要對字符串按照某個字符串切分成字符串數(shù)組數(shù)時,常用到strings.Split()
最近在使用過程中踩到了個坑,后對踩坑原因做了分析,并總結(jié)了使用string.Split可能踩到的坑。最后寫本篇文章做復(fù)盤總結(jié)與分享
場景
當時是需要取某個結(jié)構(gòu)體的某個屬性,并將其按,切分 整體邏輯類似這樣的
type Info struct{
Ids string // Ids: 123,456
}
func test3(info Info){
ids := info.Ids
idList := strings.Split(ids , ",")
if len(idList) < 1 {
return
}
log.Println("ids-not-empty")
// ***
}
當ids = "" 時,控制臺打印了 ids-not-empty ,當時百思不得其解,按理來說應(yīng)該直接走return 這個問題激發(fā)了我的好奇心,決定認真排查一下
前置
在排查之前,先大概講講 Go 中string的基本結(jié)構(gòu)
golang的string它的運行時的數(shù)據(jù)結(jié)構(gòu)位于reflect.StringHeader
type stringHeader struct {
Data unsafe.Pointer
Len int
}其中Data指向數(shù)據(jù)數(shù)組的指針 ,Len為數(shù)組的長度
排查
驗證
既然代碼中的 if 判斷為false,那么就實際打印一下 isList的長度看看呢
func test3(info Info){
ids := info.Ids
idList := strings.Split(ids, ",")
log.Printf("idList長度: [%d], idList: [%v]", len(idList), idList)
for index, _ := range idList {
log.Printf("idList[%d]:[%v]", index, idList[index])
}
// ***
}
打印底層信息
好奇心加深,打印一下ids和idList的信息
const (
basePrintInfoV3 = "%s 字符串的指針地址:[%v],字符串buf數(shù)組地址:[%v] ,Len字段的地址:[%p] ,Len字段值:[%v]"
basePrintInfoV2 = "%s切片的指針地址:[%p],切片數(shù)組地址:[%p], Len字段的地址:[%p], Len字段的值:[%v]"
)
func test3(info Info) {
ids := info.Ids
idList := strings.Split(ids, ",")
getStringPtr("ids ", &ids)
getStringSliceAllPtr("idList ", &idList)
// ***
}
func getStringPtr(name string, str *string) {
s2 := (*reflect.StringHeader)(unsafe.Pointer(str))
log.Printf(basePrintInfoV3, name, unsafe.Pointer(str), unsafe.Pointer(s2.Data), unsafe.Pointer(&s2.Len), s2.Len)
}
func getStringSliceAllPtr(name string, s1 *[]string) {
s2 := (*reflect.StringHeader)(unsafe.Pointer(s1))
log.Printf(basePrintInfoV2, name, unsafe.Pointer(&s1), unsafe.Pointer(s2.Data), unsafe.Pointer(&s2.Len), s2.Len)
}
追源碼
ids 經(jīng)過 split 之后的數(shù)組和預(yù)期的不一樣,看來應(yīng)該是 split 源碼有特殊處理了,那追一下源碼吧
func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) }大概讀一遍源碼能夠理清楚genSplit思路
- 預(yù)先確定s 能夠被切分成
n份 - 創(chuàng)建長度為
n的數(shù)組 - 遍歷 s ,將每片數(shù)據(jù)放入數(shù)組中
- 返回
func genSplit(s, sep string, sepSave, n int) []string {
if n == 0 {
return nil
}
if sep == "" {
return explode(s, n)
}
if n < 0 {
// 計算 s 按照 seq 能被切成多少份
n = Count(s, sep) + 1
}
a := make([]string, n)
n--
i := 0
for i < n {
// 定位 s里的第一個 sep 所在的位置
m := Index(s, sep)
if m < 0 {
break
}
// 放入返回的數(shù)組
a[i] = s[:m+sepSave]
// 切割s
s = s[m+len(sep):]
i++
}
a[i] = s
return a[:i+1]
}那么問題應(yīng)該出就出在 Count 函數(shù)中
跟進看看 count 函數(shù)會計算 s 字符串中包含了多少個 subStr
func Count(s, substr string) int {
// special case
if len(substr) == 0 {
return utf8.RuneCountInString(s) + 1
}
if len(substr) == 1 {
return bytealg.CountString(s, substr[0])
}
n := 0
for {
i := Index(s, substr)
if i == -1 {
return n
}
n++
s = s[i+len(substr):]
}
}Count 中會走 len(substr) == 1這個邏輯,其中的CountString計算s中存在多少個 substr[0],當時跟進,返回的結(jié)果是0 ,這里符合預(yù)期 。
再結(jié)合 genSplit 中的 n = Count() + 1 我們可以發(fā)現(xiàn),在genSplit時,預(yù)先創(chuàng)建的數(shù)組長度就為0 + 1 = 1 ! 問題迎刃而解
類似情況
經(jīng)過查閱,這里再總結(jié)一下其他使用strings.Split可能遇到的坑
s := strings.Split("", "")
fmt.Println(s, len(s)) // [] 0 //返回空數(shù)組
s = strings.Split("abc,abc", "")
fmt.Println(s, len(s)) // [a b c , a b c] 7 //返回7個數(shù)組元素
s = strings.Split("", ",")
fmt.Println(s, len(s)) // [] 1
s = strings.Split("abc,abc", ",")
fmt.Println(s, len(s)) // [abc abc] 2
s = strings.Split("abc,abc", "|")
fmt.Println(s, len(s)) // [abc,abc] 1
fmt.Println(len("")) // 0
fmt.Println(len([]string{""})) // 1
str := ""
fmt.Println(str[0]) // panic總結(jié)
這次小小的踩坑其實也算是繞了一點點彎路,直接讀源碼就好了 hhhhhh
到此這篇關(guān)于Golang的strings.Split()踩坑記錄的文章就介紹到這了,更多相關(guān)Golang strings.Split()內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go編寫定時器與定時任務(wù)詳解(附第三方庫gocron用法)
當需要每天執(zhí)行定時任務(wù)的時候就需要定時器來處理了,周期任務(wù),倒計時任務(wù),定點任務(wù)等,下面這篇文章主要給大家介紹了關(guān)于Go編寫定時器與定時任務(wù)的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-07-07

