<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 字符串編碼?UTF-8?Unicode?看完就通!

          共 13365字,需瀏覽 27分鐘

           ·

          2022-07-04 17:47

          Go byte rune string

          string類型在golang中以u(píng)tf-8的編碼形式存在,而string的底層存儲(chǔ)結(jié)構(gòu),劃分到字節(jié)即byte,劃分到字符即rune。本文將會(huì)介紹字符編碼的一些基礎(chǔ)概念,詳細(xì)講述三者之間的關(guān)系,并提供部分字符串相關(guān)的操作實(shí)踐。

          一、基礎(chǔ)概念

          介紹Unicode,UTF-8之間的關(guān)系與編碼規(guī)則

          1、Unicode

          Unicode是一種在計(jì)算機(jī)上使用的字符編碼。它為每種語言中的每個(gè)字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼,以滿足跨語言、跨平臺(tái)進(jìn)行文本轉(zhuǎn)換、處理的要求。本質(zhì)上Unicode表示了一種字符與二進(jìn)制編碼的一一對(duì)應(yīng)關(guān)系,所以是一種單字符的編碼。

          對(duì)于字符串來說,如果使用Unicode進(jìn)行存儲(chǔ),則每個(gè)字符使用的存儲(chǔ)長度是不固定的,而且是無法進(jìn)行精確分割的。如中文字符“南”使用的Unicode編碼為0x5357,對(duì)于該編碼可以整體理解為一個(gè)字符“南”,也可以理解為0x53(S)和0x57(W)。因而單純使用Unicode是無法進(jìn)行字符串編碼的,因?yàn)橛?jì)算機(jī)無法去識(shí)別要在幾個(gè)字節(jié)處做分割,哪幾個(gè)字節(jié)要組成一個(gè)字符。所以需要一種Unicode之上,存在部分冗余位的編碼方式,以準(zhǔn)確表示單個(gè)字符,并在多個(gè)字符進(jìn)行組合的時(shí)候,能夠正確進(jìn)行分割,即UTF-8。

          2、UTF-8

          UTF-8是針對(duì)Unicode的一種可變長度字符編碼,它可以用來表示Unicode標(biāo)準(zhǔn)中的任何字符。因而UTF-8是Unicode字符編碼的一種實(shí)現(xiàn)方式,Unicode強(qiáng)調(diào)單個(gè)字符的一一對(duì)應(yīng)關(guān)系,UTF-8是Unicode的組合實(shí)現(xiàn)方式,此外還有UTF-16,UTF-32等類似編碼,普適性較UTF-8稍弱。

          編碼規(guī)則

          • ? ASCII字符(不包含擴(kuò)展128+)0000 0000-0000 007F (0~7bit)

            • ? 0xxxxxxx

          • ? 0000 0080-0000 07FF (8~11bit)

            • ? 110xxxxx 10xxxxxx

          • ? 0000 0800-0000 FFFF (12~16bit)

            • ? 1110xxxx 10xxxxxx 10xxxxxx

          • ? 0001 0000-0010 FFFF (17~21bit)

            • ? 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

          總結(jié)

          1. 1. 對(duì)于ASCII(不包含擴(kuò)展128+)字符,UTF-8編碼、Unicode編碼、ASCII碼均相同(即單字節(jié)以0開頭)

          2. 2. 對(duì)于非ASCII(不包含擴(kuò)展128+)字符,若字符有n個(gè)字節(jié)(編碼后)。則首字節(jié)的開頭為n個(gè)1和1個(gè)0,其余字節(jié)均以10開頭。除去這些開頭固定位,其余位組合表示Unicode字符。

          轉(zhuǎn)換(2+字節(jié)UTF-8)

          UTF-8 to Unicode

          將UTF-8 按字節(jié)進(jìn)行分割,以編碼規(guī)則去掉每個(gè)字節(jié)頭部的占位01,剩下位進(jìn)行組合即Unicode字符

          Unicode to UTF-8

          從低位開始每次取6位,前加10組成尾部一個(gè)字節(jié)。直到不足六位,加上對(duì)應(yīng)的n個(gè)1和1個(gè)0,首字節(jié)的大端不足位補(bǔ)0,如補(bǔ)充字節(jié)后位數(shù)不夠則再增加一字節(jié),規(guī)則同上。

          (按規(guī)則預(yù)估字節(jié)數(shù),優(yōu)先寫好每個(gè)字節(jié)的填充位,從末端補(bǔ)充即可)

          實(shí)踐

          UTF-8 to Unicode

          字符”南“,UTF-8十六進(jìn)制編碼為 0xe58d97,二進(jìn)制編碼為 11100101 10001101 10010111

          去掉第一字節(jié)頭部的1110,二三字節(jié)頭部的10,則為 0101 0011 010 10111,Unicode編碼 0x5357

          Unicode to UTF-8

          字符”南“,Unicode十六進(jìn)制編碼為 0x5357,二進(jìn)制編碼為 0101 0011 0101 0111 (15位)。則轉(zhuǎn)換為UTF-8后占用3個(gè)字節(jié),即1110xxxx 10xxxxxx 10xxxxxx。

          從后向前填充:11100101 10001101 10010111

          3、UCA(Unicode Collation Algorithm)

          UCA是Unicode字符的核對(duì)算法,目前最新版本15.0.0(2022-05-03 12:36)。以14.0.0為準(zhǔn),數(shù)據(jù)文件主要包含兩個(gè)部分, 即 allkeys 和 decomps,表示字符集的排序、大小寫、分解關(guān)系等,詳細(xì)信息可閱讀Unicode官方文檔。不同版本之間的UCA是存在差異的,如兩個(gè)字符,在14.0.0中定義了大小寫關(guān)系,但在5.0.0中是不具備大小寫關(guān)系的。在僅支持5.0.0的應(yīng)用中,14.0.0 增加的字符是可能以硬編碼的方式存在的,具體情況要看實(shí)現(xiàn)細(xì)節(jié)。因而對(duì)于跨平臺(tái),多語言的業(yè)務(wù),各個(gè)服務(wù)使用的UCA很可能不是同一個(gè)版本。因而對(duì)于部分字符,其排序規(guī)則、大小寫轉(zhuǎn)換的不同,有可能會(huì)產(chǎn)生不一致的問題。

          二、byte rune string

          1、類型定義

          三者都是Go中的內(nèi)置類型,在 builtin 包中有類型定義

          // byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
          // used, by convention, to distinguish byte values from 8-bit unsigned
          // integer values.
          type byte = uint8

          // 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

          // 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

          byte是uint8類型的別名,通常用于表示一個(gè)字節(jié)(8bit)。

          rune是int32類型的別名,通常用于表示一個(gè)字符(32bit)。

          string是8bit字節(jié)的集合,通常是表示UTF-8編碼的字符串。

          從官方概念來看,string表示的是byte的集合,即八位的一個(gè)字節(jié)的集合,通常情況下使用UTF-8的編碼方式,但不絕對(duì)。而rune表示用四個(gè)字節(jié)組成的一個(gè)字符,rune值為字符的Unicode編碼。

          str := "南"

          對(duì)于一個(gè)字符串“南”,其在UTF-8編碼下有三個(gè)字節(jié)0xe58d97,所以轉(zhuǎn)化為字節(jié)數(shù)組

          byteList := []byte{0xe5,0x8d,0x97}

          三個(gè)字節(jié)共同表示一個(gè)字符,因而rune實(shí)際上為其對(duì)應(yīng)的Unicode對(duì)應(yīng)的編碼0x5357

          runeList := []rune{0x5357}

          上述三段中的str,byteList,runeList雖然分別為字符串、字節(jié)數(shù)組、字符數(shù)組不同類型,但實(shí)際上表示的都是漢字“南”。

          2、類型轉(zhuǎn)換

          類型轉(zhuǎn)換時(shí)候使用的語法,是無法直接定位到具體實(shí)現(xiàn)過程的。需要查看 plan9 匯編結(jié)果以找到類型轉(zhuǎn)換具體調(diào)用的源碼。

          func main() {
              byteList := []byte{0xe50x8d0x97}
              str := string(byteList)
              fmt.Println(str)
          }

          如上示例代碼,定義字節(jié)數(shù)組(表示漢字“南”),轉(zhuǎn)化為string類型后進(jìn)行輸出。

          go tool compile -S -N -l main.go

          命令行對(duì)上述代碼進(jìn)行編譯,禁止內(nèi)聯(lián),禁止優(yōu)化,輸出匯編代碼如下(僅關(guān)注類型轉(zhuǎn)換):

          0x0074 00116 (main.go:7)        MOVD    ZR, 8(RSP)
          0x0078 00120 (main.go:7)        MOVD    R0, 16(RSP)
          0x007c 00124 (main.go:7)        MOVD    R1, 24(RSP)
          0x0080 00128 (main.go:7)        PCDATA  $1, ZR
          0x0080 00128 (main.go:7)        CALL    runtime.slicebytetostring(SB)
          0x0084 00132 (main.go:7)        MOVD    32(RSP), R0
          0x0088 00136 (main.go:7)        MOVD    40(RSP), R1
          0x008c 00140 (main.go:7)        MOVD    R0, "".str-80(SP)
          0x0090 00144 (main.go:7)        MOVD    R1, "".str-72(SP)

          可見,類型轉(zhuǎn)換實(shí)際上是調(diào)用了runtime包中的slicebytetostring方法

          三種類型相互轉(zhuǎn)換均可通過匯編的方式找到源碼位置,此處僅以[]byte->string舉例。

          rune to []byte(string)

          encoderune 函數(shù)接受一個(gè)rune值,通過UTF-8的編碼規(guī)則,將其轉(zhuǎn)化為[]byte并寫入p,同時(shí)返回寫入的字節(jié)數(shù)。

          // encoderune writes into p (which must be large enough) the UTF-8 encoding of the rune.
          // It returns the number of bytes written.
          func encoderune(p []byte, r runeint {
              // Negative values are erroneous. Making it unsigned addresses the problem.
              switch i := uint32(r); {
              case i <= rune1Max:
                  p[0] = byte(r)
                  return 1
              case i <= rune2Max:
                  _ = p[1// eliminate bounds checks
                  p[0] = t2 | byte(r>>6)
                  p[1] = tx | byte(r)&maskx
                  return 2
              case i > maxRune, surrogateMin <= i && i <= surrogateMax:
                  r = runeError
                  fallthrough
              case i <= rune3Max:
                  _ = p[2// eliminate bounds checks
                  p[0] = t3 | byte(r>>12)
                  p[1] = tx | byte(r>>6)&maskx
                  p[2] = tx | byte(r)&maskx
                  return 3
              default:
                  _ = p[3// eliminate bounds checks
                  p[0] = t4 | byte(r>>18)
                  p[1] = tx | byte(r>>12)&maskx
                  p[2] = tx | byte(r>>6)&maskx
                  p[3] = tx | byte(r)&maskx
                  return 4
              }
          }

          rune向byte和string類型的轉(zhuǎn)換實(shí)際上都是基于 encoderune 函數(shù),該函數(shù)通過硬編碼和位運(yùn)算的方式實(shí)現(xiàn)了Unicode值向UTF-8編碼([]byte)的轉(zhuǎn)換。因而不再關(guān)注rune,僅關(guān)注[]byte和string的轉(zhuǎn)換邏輯。

          []byte to string

          // slicebytetostring converts a byte slice to a string.
          // It is inserted by the compiler into generated code.
          // ptr is a pointer to the first element of the slice;
          // n is the length of the slice.
          // Buf is a fixed-size buffer for the result,
          // it is not nil if the result does not escape.
          func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
              /*
                  部分情況(race、msan、n=0,1等不關(guān)注)
              */


              var p unsafe.Pointer
              if buf != nil && n <= len(buf) {
                  p = unsafe.Pointer(buf)
              } else {
                  p = mallocgc(uintptr(n), nilfalse)
              }
              stringStructOf(&str).str = p
              stringStructOf(&str).len = n
              memmove(p, unsafe.Pointer(ptr), uintptr(n))
              return
          }

          *tmpBuf是一個(gè)定長為32的字節(jié)數(shù)組,當(dāng)長度超過32,無法直接通過tmpBuf進(jìn)行承接,則需要重新分配一塊內(nèi)存去存儲(chǔ)string。

          const tmpStringBufSize = 32

          type tmpBuf [tmpStringBufSize]byte

          stringStructOf 用于將字符串類型轉(zhuǎn)為string內(nèi)置的stringStruct類型,以設(shè)置字符串指針與len。

          type stringStruct struct {
             str unsafe.Pointer
             len int
          }

          func stringStructOf(sp *string) *stringStruct {
              return (*stringStruct)(unsafe.Pointer(sp))
          }

          無論使用tmpBuf還是在堆上新分配,都需要通過memmove進(jìn)行底層數(shù)據(jù)拷貝。

          string to []byte

          func stringtoslicebyte(buf *tmpBuf, s string) []byte {
             var b []byte
             if buf != nil && len(s) <= len(buf) {
                *buf = tmpBuf{}
                b = buf[:len(s)]
             } else {
                b = rawbyteslice(len(s))
             }
             copy(b, s)
             return b
          }

          本質(zhì)上也是基于string的len,選擇性使用tmpBuf或新分配內(nèi)存,后使用copy進(jìn)行底層數(shù)據(jù)拷貝

          三、操作實(shí)踐

          1、類型轉(zhuǎn)換性能優(yōu)化

          Go底層對(duì)[]byte和string的轉(zhuǎn)化都需要進(jìn)行內(nèi)存拷貝,因而在部分需要頻繁轉(zhuǎn)換的場景下,大量的內(nèi)存拷貝會(huì)導(dǎo)致性能下降。

          type stringStruct struct {
             str unsafe.Pointer
             len int
          }

          type slice struct {
             array unsafe.Pointer
             len   int
             cap   int
          }

          本質(zhì)上底層數(shù)據(jù)存儲(chǔ)都是基于uintptr,可見string與[]byte的區(qū)別在于[]byte額外有一個(gè)cap去指定slice的容量。所以string可以看作[2]uintptr,[]byte看作[3]uintptr,類型轉(zhuǎn)換只需要轉(zhuǎn)換成對(duì)應(yīng)的uintptr數(shù)組即可,不需要進(jìn)行底層數(shù)據(jù)的頻繁拷貝。

          以下是fasthttp基于此思想提供的一個(gè)解決方案,用于string與[]byte的高性能轉(zhuǎn)換。

          // b2s converts byte slice to a string without memory allocation.
          // See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
          //
          // Note it may break if string and/or slice header will change
          // in the future go versions.
          func b2s(b []bytestring {
              /* #nosec G103 */
              return *(*string)(unsafe.Pointer(&b))
          }

          // s2b converts string to a byte slice without memory allocation.
          //
          // Note it may break if string and/or slice header will change
          // in the future go versions.
          func s2b(s string) (b []byte) {
              /* #nosec G103 */
              bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
              /* #nosec G103 */
              sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
              bh.Data = sh.Data
              bh.Cap = sh.Len
              bh.Len = sh.Len
              return b
          }

          由于[]byte轉(zhuǎn)換到string時(shí)直接拋棄cap即可,因而可以直接通過unsafe.Pointer進(jìn)行操作。

          string轉(zhuǎn)換到[]byte時(shí),需要進(jìn)行指針的拷貝,并將Cap設(shè)置為Len。此處是該方案的一個(gè)細(xì)節(jié)點(diǎn),因?yàn)閟tring是定長的,轉(zhuǎn)換后data后續(xù)的數(shù)據(jù)是否可寫是不確定的。如果Cap大于Len,在進(jìn)行append的時(shí)候不會(huì)觸發(fā)slice的擴(kuò)容,而且由于后續(xù)內(nèi)存不可寫,就會(huì)在運(yùn)行時(shí)導(dǎo)致panic。

          2、UCA不一致

          UCA定義在 unicode/tables.go 中,頭部即定義了使用的UCA版本。

          // Version is the Unicode edition from which the tables are derived.
          const Version = "13.0.0"

          經(jīng)過追溯,go 1 起的tables.go即使用了6.0.0的版本,位置與現(xiàn)在稍有不同。

          根據(jù)MySQL官方文檔關(guān)于UCA的相關(guān)內(nèi)容

          MySQL使用不同編碼,UCA的版本并不相同,因而很大概率會(huì)存在底層數(shù)據(jù)庫使用的UCA與業(yè)務(wù)層使用的UCA不一致的情況。在一些大小寫不敏感的場景下,可能會(huì)出現(xiàn)字符的識(shí)別問題。如業(yè)務(wù)層認(rèn)為兩個(gè)字符為一對(duì)大小寫字符,而由于MySQL使用的UCA版本較低,導(dǎo)致MySQL通過小寫進(jìn)行不敏感查詢無法查詢到大寫的數(shù)據(jù)。

          由于常用字符集基本不會(huì)發(fā)生變化,所以對(duì)于普通業(yè)務(wù),UCA的不一致基本不會(huì)造成影響。



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。


          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  五月天婷婷色亚洲丁香 | 丁香婷婷成人小说 | 哪里可以免费看A片 | 黄片毛片视频 | 人妻日逼 |