有趣的面試題:Go語言字符串的字節(jié)長(zhǎng)度和字符個(gè)數(shù)
背景
今天我們一起來看看
Go語言中的rune數(shù)據(jù)類型,首先從一道面試題入手,你能很快說出下面這道題的答案嗎?
func main() {
str := "Golang夢(mèng)工廠"
fmt.Println(len(str))
fmt.Println(len([]rune(str)))
}
運(yùn)行結(jié)果是15和15還是15和9呢?先思考一下,一會(huì)揭曉答案。
其實(shí)這并不是一道面試題,是我在日常開發(fā)中遇到的一個(gè)問題,當(dāng)時(shí)場(chǎng)景是這樣的:后端要對(duì)前端傳來的字符串做字符校驗(yàn),產(chǎn)品的需求是限制為200字符,然后我在后端做校驗(yàn)時(shí)直接使用len(str) > 200來做判斷,結(jié)果出現(xiàn)了bug,前端字符校驗(yàn)沒有超過200字符,調(diào)用后端接口確一直是參數(shù)錯(cuò)誤,改成使用len([]rune(str)) > 200成功解決了這個(gè)問題。具體原因我們?cè)谖闹薪視浴?/p>
Unicode和字符編碼
在介紹rune類型之前,我們還是要從一些基礎(chǔ)知識(shí)開始。------ Unicode和字符編碼。
什么是 Unicode?
我們都知道計(jì)算機(jī)只能處理數(shù)字,如果想要處理文本需要轉(zhuǎn)換為數(shù)字才能處理,早些時(shí)候,計(jì)算機(jī)在設(shè)計(jì)上采用8bit作為一個(gè)byte,一個(gè)byte表示的最大整數(shù)就是255,想表示更大的整數(shù),就需要更多的byte。顯然,一個(gè)字節(jié)表示中文,是不夠的,至少需要兩個(gè)字節(jié),而且還不能和ASCII編碼沖突,所以,我國(guó)制定了GB2312編碼,用來把中文編進(jìn)去。但是世界上有很多語言,不同語言制定一個(gè)編碼,就會(huì)不可避免地出現(xiàn)沖突,所以unicode字符就是來解決這個(gè)痛點(diǎn)的。Unicode把所有語言都統(tǒng)一到一套編碼里??偨Y(jié)來說:"unicode其實(shí)就是對(duì)字符的一種編碼方式,可以理解為一個(gè)字符---數(shù)字的映射機(jī)制,利用一個(gè)數(shù)字即可表示一個(gè)字符。"
什么是字符編碼?
雖然unicode把所有語言統(tǒng)一到一套編碼里了,但是他卻沒有規(guī)定字符對(duì)應(yīng)的二進(jìn)制碼是如何存儲(chǔ)。以漢字“漢”為例,它的 Unicode 碼點(diǎn)是 0x6c49,對(duì)應(yīng)的二進(jìn)制數(shù)是 110110001001001,二進(jìn)制數(shù)有 15位,這也就說明了它至少需要 2個(gè)字節(jié)來表示。可以想象,在Unicode 字典中往后的字符可能就需要 3個(gè)字節(jié)或者 4個(gè)字節(jié),甚至更多字節(jié)來表示了。
這就導(dǎo)致了一些問題,計(jì)算機(jī)怎么知道你這個(gè)2個(gè)字節(jié)表示的是一個(gè)字符,而不是分別表示兩個(gè)字符呢?這里我們可能會(huì)想到,那就取個(gè)最大的,假如 Unicode中最大的字符用4 字節(jié)就可以表示了,那么我們就將所有的字符都用4個(gè)字節(jié)來表示,不夠的就往前面補(bǔ)0。這樣確實(shí)可以解決編碼問題,但是卻造成了空間的極大浪費(fèi),如果是一個(gè)英文文檔,那文件大小就大出了3 倍,這顯然是無法接受的。
于是,為了較好的解決Unicode 的編碼問題, UTF-8 和UTF-16 兩種當(dāng)前比較流行的編碼方式誕生了。UTF-8 是目前互聯(lián)網(wǎng)上使用最廣泛的一種Unicode編碼方式,它的最大特點(diǎn)就是可變長(zhǎng)。它可以使用 1 - 4個(gè)字節(jié)表示一個(gè)字符,根據(jù)字符的不同變換長(zhǎng)度。在UTF-8編碼中,一個(gè)英文為一個(gè)字節(jié),一個(gè)中文為三個(gè)字節(jié)。
Go語言中的字符串
基本概念
先來看一下官方對(duì)string的定義:
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
人工翻譯:
string是8位字節(jié)的集合,通常但不一定代表UTF-8編碼的文本。string可以為空,但不能為nil。string的值是不能改變的
說得通俗一點(diǎn),其實(shí)字符串實(shí)際上是只讀的字節(jié)切片,對(duì)于字符串底層而言就是一個(gè)byte數(shù)組,不過這個(gè)數(shù)組是只讀的,不允許修改。

寫個(gè)例子驗(yàn)證一下:
func main() {
byte1 := []byte("Hl Asong!")
byte1[1] = 'i'
str1 := "Hl Asong!"
str1[1] = 'i'
}
對(duì)于byte的操作是可行,而string操作會(huì)直接報(bào)錯(cuò):
cannot assign to str1[1]
所以說string修改操作是不允許的,僅僅支持替換操作。
根據(jù)前面的分析,我們也可以得出我們將字符存儲(chǔ)在字符串中時(shí),也就是按字節(jié)進(jìn)行存儲(chǔ)的,所以最后存儲(chǔ)的其實(shí)是一個(gè)數(shù)值。
Go語言的字符串編碼
上面我們介紹了字符串的基本概念,接下來我們看一下Go語言中的字符串編碼是怎樣的。
Go源代碼為 UTF-8 編碼格式的,源代碼中的字符串直接量是 UTF-8 文本。所以Go語言中字符串是UTF-8編碼格式的。
Go語言字符串循環(huán)
Go語言中字符串可以使用range循環(huán)和下標(biāo)循環(huán)。我們寫一個(gè)例子,看一下兩種方式循環(huán)有什么區(qū)別:
func main() {
str := "Golang夢(mèng)工廠"
for k,v := range str{
fmt.Printf("v type: %T index,val: %v,%v \n",v,k,v)
}
for i:=0 ; i< len(str) ; i++{
fmt.Printf("v type: %T index,val:%v,%v \n",str[i],i,str[i])
}
}
運(yùn)行結(jié)果:
v type: int32 index,val: 0,71
v type: int32 index,val: 1,111
v type: int32 index,val: 2,108
v type: int32 index,val: 3,97
v type: int32 index,val: 4,110
v type: int32 index,val: 5,103
v type: int32 index,val: 6,26790
v type: int32 index,val: 9,24037
v type: int32 index,val: 12,21378
v type: uint8 index,val:0,71
v type: uint8 index,val:1,111
v type: uint8 index,val:2,108
v type: uint8 index,val:3,97
v type: uint8 index,val:4,110
v type: uint8 index,val:5,103
v type: uint8 index,val:6,230
v type: uint8 index,val:7,162
v type: uint8 index,val:8,166
v type: uint8 index,val:9,229
v type: uint8 index,val:10,183
v type: uint8 index,val:11,165
v type: uint8 index,val:12,229
v type: uint8 index,val:13,142
v type: uint8 index,val:14,130
根據(jù)運(yùn)行結(jié)果我們可以得出如下結(jié)論:
使用下標(biāo)遍歷獲取的是
ASCII字符,而使用Range遍歷獲取的是Unicode字符。
什么是rune數(shù)據(jù)類型
官方對(duì)rune的定義如下:
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
人工翻譯:
rune是int32的別名,在所有方面都等同于int32,按照約定,它用于區(qū)分字符值和整數(shù)值。
說的通俗一點(diǎn)就是rune一個(gè)值代表的就是一個(gè)Unicode字符,因?yàn)橐粋€(gè)Go語言中字符串編碼為UTF-8,使用1-4字節(jié)就可以表示一個(gè)字符,所以使用int32類型范圍就可以完美適配。
答案揭曉
前面說了這么多知識(shí)點(diǎn),確實(shí)有點(diǎn)亂了,我們現(xiàn)在就根據(jù)開始的那道題來做一個(gè)總結(jié)。為了方便查看,再貼一下這道題:
func main() {
str := "Golang夢(mèng)工廠"
fmt.Println(len(str))
fmt.Println(len([]rune(str)))
}
這道題的正確答案是15和9。
具體原因:
len()函數(shù)是用來獲取字符串的字節(jié)長(zhǎng)度,rune一個(gè)值代表的就是一個(gè)Unicode字符,所以求rune切片的長(zhǎng)度就是字符個(gè)數(shù)。因?yàn)樵?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">utf-8編碼中,英文占1個(gè)字節(jié),中文占3個(gè)字節(jié),所以最終結(jié)果就是15和9。
貼個(gè)圖,方便理解:

unicode/utf8庫(kù)
如果大家對(duì)rune的使用不是很明確,可以學(xué)習(xí)使用一下Go標(biāo)準(zhǔn)庫(kù)unicode/utf8,其中提供了多種關(guān)于rune的使用方法。比如上面這道題,我們就可以使用utf8.RuneCountInString方法獲取字符個(gè)數(shù)。更多庫(kù)函數(shù)使用方法請(qǐng)自行解鎖,本篇就不做過多介紹了。
總結(jié)
針對(duì)全文,我們做一個(gè)總結(jié):
Go語言源代碼始終為 UTF-8Go語言的字符串可以包含任意字節(jié),字符底層是一個(gè)只讀的byte數(shù)組。Go語言中字符串可以進(jìn)行循環(huán),使用下表循環(huán)獲取的acsii字符,使用range循環(huán)獲取的unicode字符。Go語言中提供了rune類型用來區(qū)分字符值和整數(shù)值,一個(gè)值代表的就是一個(gè)Unicode字符。Go語言中獲取字符串的字節(jié)長(zhǎng)度使用len()函數(shù),獲取字符串的字符個(gè)數(shù)使用utf8.RuneCountInString函數(shù)或者轉(zhuǎn)換為rune切片求其長(zhǎng)度,這兩種方法都可以達(dá)到預(yù)期結(jié)果。
推薦閱讀
