詳解 Go 中的 rune 類型


Go語言中文網 2022-03-15 08:52

 

剛接觸 Go 語言時,就聽說有一個叫 rune 的數據類型,即使查閱過一些資料,對它的理解依舊比較模糊,加之對陌生事物的天然排斥,在之后很長一段時間的編程工作中,我都沒有讓它出現在我的代碼里。

逃避雖然有用,但是似乎有些可恥,想要成為一名成熟、優秀的 Go 語言開發工程師,必須要有直面陌生事物並且成功運用的勇氣和能力,帶着這樣的覺悟,讓我們一起走近 rune,直視它!

了解一下,rune類型究竟是什么?

rune 類型是 Go 語言的一種特殊數字類型。在 builtin/builtin.go 文件中,它的定義:type rune = int32;官方對它的解釋是:rune 是類型 int32 的別名,在所有方面都等價於它,用來區分字符值跟整數值。使用單引號定義 ,返回采用 UTF-8 編碼的 Unicode 碼點。Go 語言通過 rune 處理中文,支持國際化多語言。

眾所周知,Go 語言有兩種類型聲明方式:一種叫類型定義聲明,另一種叫類型別名聲明。其中,別名的使用在大型項目重構中作用最為明顯,它能解決代碼升級或遷移過程中可能存在的類型兼容性問題。而rune 跟 byte 是 Go 語言中僅有的兩個類型別名,專門用來處理字符。當然,我們也可以通過 type 關鍵字加等號的方式聲明更多的類型別名。

學習一下,rune類型怎么用?

我們知道,字符串由字符組成,字符的底層由字節組成,而一個字符串在底層的表示是一個字節序列。在 Go 語言中,字符可以被分成兩種類型處理:對占 1 個字節的英文類字符,可以使用 byte(或者 unit8 );對占 1 ~ 4 個字節的其他字符,可以使用 rune(或者 int32 ),如中文、特殊符號等。

下面,我們通過示例應用來具體感受一下。

  • 統計帶中文字符串長度
// 使用內置函數 len() 統計字符串長度
fmt.Println(len("Go語言編程"))  // 輸出:14  

前面說到,字符串在底層的表示是一個字節序列。其中,英文字符占用 1 字節,中文字符占用 3 字節,所以得到的長度 14 顯然是底層占用字節長度,而不是字符串長度,這時,便需要用到 rune 類型。

// 轉換成 rune 數組后統計字符串長度
fmt.Println(len([]rune("Go語言編程")))  // 輸出:6

這回對了。很容易,我們解鎖了 rune 類型的第一個功能,即統計字符串長度。

  • 截取帶中文字符串

如果想要截取字符串中 ”Go語言“ 這一段,考慮到底層是一個字節序列,或者說是一個數組,通常情況下,我們會這樣:

s := "Go語言編程"
// 8=2*1+2*3
fmt.Println(s[0:8])  // 輸出:Go語言

結果符合預期。但是,按照字節的方式進行截取,必須預先計算出需要截取字符串的字節數,如果字節數計算錯誤,就會顯示亂碼,比如這樣:

s := "Go語言編程"
fmt.Println(s[0:7]) // 輸出:Go語�

此外,如果截取的字符串較長,那通過字節的方式進行截取顯然不是一個高效准確的辦法。那有沒有不用計算字節數,簡單又不會出現亂碼的方法呢?不妨試試這樣:

s := "Go語言編程"
// 轉成 rune 數組,需要幾個字符,取幾個字符
fmt.Println(string([]rune(s)[:4])) // 輸出:Go語言    

到這里,我們解鎖了 rune 類型的第二個功能,即截取字符串。

思考一下,為什么 rune 類型可以做到?

通過上面的示例,我們發現似乎在處理帶中文的字符串時,都需要用到 rune 類型,這究竟是為什么呢?除了使用 rune 類型,還有其他方法嗎?

在深入思考之前,我們需要首先弄清楚 string 、byterune 三者間的關系。

字符串在底層的表示是由單個字節組成的一個不可修改的字節序列,字節使用 UTF-8[1] 編碼標識 Unicode[2] 文本。Unicode 文本意味着 .go 文件內可以包含世界上的任意語言或字符,該文件在任意系統上打開都不會亂碼。UTF-8 是 Unicode 的一種實現方式,是一種針對 Unicode 可變長度的字符編碼,它定義了字符串具體以何種方式存儲在內存中。UFT-8 使用 1 ~ 4 為每個字符編碼。

Go 語言把字符分 byte 和 rune 兩種類型處理。byte 是類型 unit8 的別名,用於存放占 1 字節的 ASCII 字符,如英文字符,返回的是字符原始字節。rune 是類型 int32 的別名,用於存放多字節字符,如占 3 字節的中文字符,返回的是字符 Unicode 碼點值。如下圖所示:

s := "Go語言編程"
// byte
fmt.Println([]byte(s)) // 輸出:[71 111 232 175 173 232 168 128 231 188 150 231 168 139]
// rune
fmt.Println([]rune(s)) // 輸出:[71 111 35821 35328 32534 31243]

它們的對應關系如下圖:圖片了解了這些,我們再回過來看看,剛才的問題是不是清楚明白很多?接下來,讓我們再來看看源碼中是如何處理的,以 utf8.RuneCountInString()[3] 函數為例。   

 

示例:

// 統計字符串長度
fmt.Println(utf8.RuneCountInString("Go語言編程")) // 輸出:6

源碼:

// RuneCountInString is like RuneCount but its input is a string.
func RuneCountInString(s string) (n int) {
 // 調用 len() 函數得到字節數
 ns := len(s)
 for i := 0; i < ns; n++ {
  c := s[i]
  // 如碼點值小於 128,則為占 1 字節的 ASCII 字符(或者說英文字符),長度 + 1
  if c < RuneSelf { // RuneSelf = 128
   // ASCII fast path
   i++
   continue
  }
  // 查詢首字節信息表,得到中文占 3 字節,所以這里的 x = 3
  x := first[c]
  // 判斷 x = 3,xx = 241(0xF1)
  if x == xx {
   i++ // invalid.
   continue
  }
  // 提取有效的 UTF-8 字節長度編碼信息,size = 3
  size := int(x & 7)
  if i+size > ns {
   i++ // Short or invalid.
   continue
  }
  // 提取有效字節范圍
  accept := acceptRanges[x>>4]
  // accept.lo,accept.hi,表示 UTF-8 中第二字節的有效范圍
  // locb = 0b10000000,表示 UTF-8 編碼非首字節的數值下限
  // hicb = 0b10111111,表示 UTF-8 編碼非首字節的數值上限
  if c := s[i+1]; c < accept.lo || accept.hi < c {
   size = 1
  } else if size == 2 {
  } else if c := s[i+2]; c < locb || hicb < c {
   size = 1
  } else if size == 3 {
  } else if c := s[i+3]; c < locb || hicb < c {
   size = 1
  }
  i += size
 }
 return n
}

調用該函數時,傳入一個原始的字符串,代碼會根據每個字符的碼點大小判斷是否為 ASCII 字符,如果是,則算做 1 位;如果不是,則查詢首字節表,明確字符占用的字節數,驗證有效性后再進行計數。

小小總結

在我看來,rune 類型只是一種名稱叫法,表示用來處理長度大於 1 字節( 8 位)、不超過 4 字節( 32 位)的字符類型。但萬變不離其宗,我們使用函數時,無論傳入參數的是原始字符串還是 rune,最終都是對字節進行處理。看似陌生的事物,沉下心了解到其本質以后,才發現原來並不陌生,缺少的只是正視它的勇氣!

[1]

UTF-8:https://zh.wikipedia.org/wiki/UTF-8

[2]

Unicode:https://zh.wikipedia.org/wiki/Unicode

[3]

utf8.RuneCountInString():https://golang.org/src/unicode/utf8/utf8.go

 


推薦閱讀

 

福利
我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復 ebook 獲取;還可以回復「進群」,和數萬 Gopher 交流學習


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM