博客 / 詳情

返回

有趣的面試題:Go語言字符串的字節長度和字符個數

背景

哈嘍,大家好,我是asong。今天我們一起來看看Go語言中的rune數據類型,首先從一道面試題入手,你能很快説出下面這道題的答案嗎?
func main()  {
    str := "Golang夢工廠"
    fmt.Println(len(str))
    fmt.Println(len([]rune(str)))
}

運行結果是1515還是159呢?先思考一下,一會揭曉答案。

其實這並不是一道面試題,是我在日常開發中遇到的一個問題,當時場景是這樣的:後端要對前端傳來的字符串做字符校驗,產品的需求是限制為200字符,然後我在後端做校驗時直接使用len(str) > 200來做判斷,結果出現了bug,前端字符校驗沒有超過200字符,調用後端接口確一直是參數錯誤,改成使用len([]rune(str)) > 200成功解決了這個問題。具體原因我們在文中揭曉。

Unicode和字符編碼

在介紹rune類型之前,我們還是要從一些基礎知識開始。 ------ Unicode和字符編碼。

  • 什麼是Unicode

我們都知道計算機只能處理數字,如果想要處理文本需要轉換為數字才能處理,早些時候,計算機在設計上採用8bit作為一個byte,一個byte表示的最大整數就是255,想表示更大的整數,就需要更多的byte。顯然,一個字節表示中文,是不夠的,至少需要兩個字節,而且還不能和ASCII編碼衝突,所以,我國制定了GB2312編碼,用來把中文編進去。但是世界上有很多語言,不同語言制定一個編碼,就會不可避免地出現衝突,所以unicode字符就是來解決這個痛點的。Unicode把所有語言都統一到一套編碼裏。總結來説:"unicode其實就是對字符的一種編碼方式,可以理解為一個字符---數字的映射機制,利用一個數字即可表示一個字符。"

  • 什麼是字符編碼?

雖然unicode把所有語言統一到一套編碼裏了,但是他卻沒有規定字符對應的二進制碼是如何存儲。以漢字“漢”為例,它的 Unicode 碼點是 0x6c49,對應的二進制數是 110110001001001,二進制數有 15 位,這也就説明了它至少需要 2 個字節來表示。可以想象,在 Unicode 字典中往後的字符可能就需要 3 個字節或者 4 個字節,甚至更多字節來表示了。

這就導致了一些問題,計算機怎麼知道你這個 2 個字節表示的是一個字符,而不是分別表示兩個字符呢?這裏我們可能會想到,那就取個最大的,假如 Unicode 中最大的字符用 4 字節就可以表示了,那麼我們就將所有的字符都用 4 個字節來表示,不夠的就往前面補 0。這樣確實可以解決編碼問題,但是卻造成了空間的極大浪費,如果是一個英文文檔,那文件大小就大出了 3 倍,這顯然是無法接受的。

於是,為了較好的解決 Unicode 的編碼問題, UTF-8 UTF-16 兩種當前比較流行的編碼方式誕生了。UTF-8 是目前互聯網上使用最廣泛的一種 Unicode 編碼方式,它的最大特點就是可變長。它可以使用 1 - 4 個字節表示一個字符,根據字符的不同變換長度。在UTF-8編碼中,一個英文為一個字節,一箇中文為三個字節。

Go語言中的字符串

基本概念

先來看一下官方對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

人工翻譯:

string8位字節的集合,通常但不一定代表UTF-8編碼的文本。string可以為空,但不能為nilstring的值是不能改變的

説得通俗一點,其實字符串實際上是隻讀的字節切片,對於字符串底層而言就是一個byte數組,不過這個數組是隻讀的,不允許修改。

寫個例子驗證一下:

func main()  {
    byte1 := []byte("Hl Asong!")
    byte1[1] = 'i'

    str1 := "Hl Asong!"
    str1[1] = 'i'
}

對於byte的操作是可行,而string操作會直接報錯:

cannot assign to str1[1]

所以説string修改操作是不允許的,僅僅支持替換操作。

根據前面的分析,我們也可以得出我們將字符存儲在字符串中時,也就是按字節進行存儲的,所以最後存儲的其實是一個數值。

Go語言的字符串編碼

上面我們介紹了字符串的基本概念,接下來我們看一下Go語言中的字符串編碼是怎樣的。

Go 源代碼為 UTF-8 編碼格式的,源代碼中的字符串直接量是 UTF-8 文本。所以Go語言中字符串是UTF-8編碼格式的。

Go語言字符串循環

Go語言中字符串可以使用range循環和下標循環。我們寫一個例子,看一下兩種方式循環有什麼區別:

func main()  {
    str := "Golang夢工廠"
    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])
    }
}

運行結果:

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

根據運行結果我們可以得出如下結論:

使用下標遍歷獲取的是ASCII字符,而使用Range遍歷獲取的是Unicode字符。

什麼是rune數據類型

官方對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

人工翻譯:

runeint32的別名,在所有方面都等同於int32,按照約定,它用於區分字符值和整數值。

説的通俗一點就是rune一個值代表的就是一個Unicode字符,因為一個Go語言中字符串編碼為UTF-8,使用1-4字節就可以表示一個字符,所以使用int32類型範圍就可以完美適配。

答案揭曉

前面説了這麼多知識點,確實有點亂了,我們現在就根據開始的那道題來做一個總結。為了方便查看,在貼一下這道題:

func main()  {
    str := "Golang夢工廠"
    fmt.Println(len(str))
    fmt.Println(len([]rune(str)))
}

這道題的正確答案是159

具體原因:

len()函數是用來獲取字符串的字節長度,rune一個值代表的就是一個Unicode字符,所以求rune切片的長度就是字符個數。因為在utf-8編碼中,英文佔1個字節,中文佔3個字節,所以最終結果就是159

貼個圖,方便理解:

unicode/utf8

如果大家對rune的使用不是很明確,可以學習使用一下Go標準庫unicode/utf8,其中提供了多種關於rune的使用方法。比如上面這道題,我們就可以使用utf8.RuneCountInString方法獲取字符個數。更多庫函數使用方法請自行解鎖,本篇就不做過多介紹了。

總結

針對全文,我們做一個總結:

  • Go語言源代碼始終為UTF-8
  • Go語言的字符串可以包含任意字節,字符底層是一個只讀的byte數組。
  • Go語言中字符串可以進行循環,使用下表循環獲取的acsii字符,使用range循環獲取的unicode字符。
  • Go語言中提供了rune類型用來區分字符值和整數值,一個值代表的就是一個Unicode字符。
  • Go語言中獲取字符串的字節長度使用len()函數,獲取字符串的字符個數使用utf8.RuneCountInString函數或者轉換為rune切片求其長度,這兩種方法都可以達到預期結果。

好啦,這篇文章就到這裏啦,素質三連(分享、點贊、在看)都是筆者持續創作更多優質內容的動力!

創建了一個Golang學習交流羣,歡迎各位大佬們踴躍入羣,我們一起學習交流。入羣方式:關注公眾號獲取。更多學習資料請到公眾號領取。

我是asong,一名普普通通的程序猿,讓我們一起慢慢變強吧。歡迎各位的關注,我們下期見~~~

推薦往期文章:

  • Go看源碼必會知識之unsafe包
  • 源碼剖析panic與recover,看不懂你打我好了!
  • 空結構體引發的大型打臉現場
  • Leaf—Segment分佈式ID生成系統(Golang實現版本)
  • 面試官:兩個nil比較結果是什麼?
  • 面試官:你能用Go寫段代碼判斷當前系統的存儲方式嗎?
  • 如何平滑切換線上Elasticsearch索引
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.