Golang開發(fā)之字符串與切片問題踩坑記錄
背景
在項(xiàng)目中,我們使用mysql來存儲數(shù)據(jù)信息,其中l(wèi)abel表記錄了標(biāo)簽相關(guān)的信息。表結(jié)構(gòu)如下:
CREATE TABLE `label` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `name` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT 'label name', PRIMARY KEY (`id`), UNIQUE KEY `uniq_n` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=7050965 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='label'
其中name字段為varchar類型,190代表字符長度,并且是唯一索引
為什么name字段設(shè)置最大字符長度為190?
name是varchar類型,并且是唯一索引。mysql規(guī)定varchar類型為索引時(shí),最大長度為767字節(jié)
name字段的編碼格式為utf8mb4_unicode_ci,一個(gè)字符最多用4個(gè)字節(jié)來表示,767 / 4 = 191.75,所以只需限制最大字符長度小于191.75即可,項(xiàng)目里取190作為限制
業(yè)務(wù)代碼邏輯很簡單,主要有兩步:
- 依據(jù)name查詢label表,如果查詢到了label信息,則直接返回
- 如果沒有查詢到label信息,則嘗試創(chuàng)建label信息
偽代碼:
const ( // 最大字符長度 LabelNameLengthLimit = 190 ) func GetOrCreateLabel(ctx context.Context, name string) (label dao.Label, err error) { var err error // 依據(jù)name查詢label信息 label, err = db_reader.GetLabel(name) // 沒有找到對應(yīng)的label信息,且字符串長度超過190,則切片后再次查詢 if IsRecordNotFoundError(err) && len(name) > LabelNameLengthLimit { label, err = db_reader.GetLabel(name[:LabelNameLengthLimit]) } // 還是報(bào)錯(cuò),則嘗試創(chuàng)建label信息 if err != nil { label.Name = name err = db_writer.CreateLabel(ctx, &label) // 報(bào)唯一鍵沖突錯(cuò)誤,可能是由于并發(fā)創(chuàng)建導(dǎo)致的問題,再次兜底進(jìn)行查詢 if IsKeyConflict(err) { label, err = db_reader.GetLabel(ctx, name) if err != nil { return label, err } } if err != nil { return label, err } } return label, nil }
問題
部分case,首次執(zhí)行業(yè)務(wù)代碼成功,后續(xù)執(zhí)行業(yè)務(wù)代碼一直報(bào)錯(cuò)
case1:
labelName := "? 2015 Charanga Sí o Ké - Chema Mu?oz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos ? 2015 Charanga Sí o Ké - Sones de Rumba"
執(zhí)行:
- 首次執(zhí)行業(yè)務(wù)代碼,執(zhí)行成功
- 后續(xù)執(zhí)行業(yè)務(wù)代碼,22行穩(wěn)定復(fù)現(xiàn)報(bào)錯(cuò)
分析
后續(xù)執(zhí)行業(yè)務(wù)代碼時(shí)
- 依據(jù)name,一直查詢不到對應(yīng)的label信息,err報(bào)錯(cuò)RecordNotFoundError
- 依據(jù)name[:LabelNameLengthLimit],一直查詢不到對應(yīng)的label信息,err報(bào)錯(cuò)RecordNotFoundError
var err error // 依據(jù)name查詢label信息 label, err = db_reader.GetLabel(name) // 沒有找到對應(yīng)的label信息,且字符串長度超過190,則切片后再次查詢 if IsRecordNotFoundError(err) && len(name) > LabelNameLengthLimit { label, err = db_reader.GetLabel(name[:LabelNameLengthLimit]) }
err != nil時(shí),嘗試創(chuàng)建label信息,此時(shí)報(bào)唯一鍵沖突錯(cuò)誤
線上可能是并發(fā)創(chuàng)建導(dǎo)致的唯一鍵沖突,兜底查詢一次,此時(shí)查詢還是報(bào)錯(cuò)RecordNotFoundError
err = db_writer.CreateLabel(ctx, &label) // 報(bào)唯一鍵沖突錯(cuò)誤,可能是由于并發(fā)創(chuàng)建導(dǎo)致的問題,再次兜底進(jìn)行查詢 if IsKeyConflict(err) { label, err = db_reader.GetLabel(ctx, name) if err != nil { return label, err } }
由于字符長度超過了190,定位到主要的問題是:依據(jù)切片后name[:LabelNameLengthLimit]查詢時(shí),沒有查詢到結(jié)果,但是依據(jù)name去創(chuàng)建label信息時(shí),報(bào)唯一鍵沖突,說明查詢的值和實(shí)際存儲的值不一致
golang string底層實(shí)現(xiàn)go/src/reflect/value.go
// StringHeader is the runtime representation of a string. // It cannot be used safely or portably and its representation may // change in a later release. // Moreover, the Data field is not sufficient to guarantee the data // it references will not be garbage collected, so programs must keep // a separate, correctly typed pointer to the underlying data. type StringHeader struct { Data uintptr Len int }
其中:
- Data 指向底層的[]byte的首地址,string底層其實(shí)是[]byte
- Len 代表字節(jié)切片的長度,避免多次獲取字符串長度時(shí),重復(fù)計(jì)算
- 內(nèi)置函數(shù)len(string),獲取的是字符串字節(jié)長度
- 對字符串進(jìn)行切片labelName[:LabelNameLengthLimit],是按照字節(jié)數(shù)進(jìn)行切片
所以問題就是:查詢和存儲時(shí),截取字符串的標(biāo)準(zhǔn)不一樣
- 當(dāng)字符串字符長度超過190時(shí),查詢時(shí)是按照字節(jié)進(jìn)行截取
- 當(dāng)字符串字符長度超過190時(shí),存儲時(shí)是按照字符進(jìn)行截取
測試代碼:
labelName := "? 2015 Charanga Sí o Ké - Chema Mu?oz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos ? 2015 Charanga Sí o Ké - Sones de Rumba" fmt.Println("原字符串內(nèi)容:", labelName) fmt.Println("字節(jié)長度:", len(labelName)) fmt.Println("按照字節(jié)截取內(nèi)容:", labelName[:LabelNameLengthLimit]) fmt.Println("字符長度:", len([]rune(labelName))) fmt.Println("按照字符截取內(nèi)容:", string([]rune(labelName)[:LabelNameLengthLimit]))
輸出:
原字符串內(nèi)容: © 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga Sí o Ké - Sones de Rumba
字節(jié)長度: 214
按照字節(jié)截取內(nèi)容: © 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga S?
字符長度: 207
按照字符截取內(nèi)容: © 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga Sí o Ké
解決
解決方案:
- 先把string轉(zhuǎn)rune切片,使用字符切片來表示字符串
- 判斷rune切片長度是否超過限制,超過則依據(jù)字符長度進(jìn)行切片
const ( // 最大字符長度 LabelNameLengthLimit = 190 ) func GetOrCreateLabel(ctx context.Context, name string) (label dao.Label, err error) { if rs := []rune(name); len(rs) > LabelNameLengthLimit { name = string(rs[:LabelNameLengthLimit]) } var err error // 依據(jù)name查詢label信息 label, err = db_reader.GetLabel(name) // 還是報(bào)錯(cuò),則嘗試創(chuàng)建label信息 if err != nil { label.Name = name err = db_writer.CreateLabel(ctx, &label) // 報(bào)唯一鍵沖突錯(cuò)誤,可能是由于并發(fā)創(chuàng)建導(dǎo)致的問題,再次兜底進(jìn)行查詢 if IsKeyConflict(err) { label, err = db_reader.GetLabel(ctx, name) if err != nil { return label, err } } if err != nil { return label, err } } return label, nil }
到此這篇關(guān)于Golang開發(fā)之字符串與切片問題踩坑記錄的文章就介紹到這了,更多相關(guān)Go字符串切片內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang中sync.Once只執(zhí)行一次的原理解析
在某些場景下,我們希望某個(gè)操作或者函數(shù)僅被執(zhí)行一次,比如單例模式的初始化,一些資源配置的加載等,golang中的sync.Once就實(shí)現(xiàn)了這個(gè)功能,本文就和大家一起解析sync.Once只執(zhí)行一次的原理,需要的朋友可以參考下2023-09-09Go語言利用time.After實(shí)現(xiàn)超時(shí)控制的方法詳解
最近在學(xué)習(xí)golang,所以下面這篇文章主要給大家介紹了關(guān)于Go語言利用time.After實(shí)現(xiàn)超時(shí)控制的相關(guān)資料,文中通過示例介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08go-cqhttp權(quán)限管理系統(tǒng)的實(shí)現(xiàn)代碼
這篇文章主要介紹了go-cqhttp權(quán)限管理,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09如何在golang中使用shopspring/decimal來處理精度問題
本文主要介紹了如何在golang中使用shopspring/decimal來處理精度問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04