Go 字符串編碼?UTF-8?Unicode?看完就通!
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. 對(duì)于ASCII(不包含擴(kuò)展128+)字符,UTF-8編碼、Unicode編碼、ASCII碼均相同(即單字節(jié)以0開頭)
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 stringbyte是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{0xe5, 0x8d, 0x97}
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 rune) int {
// 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), nil, false)
}
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]bytestringStructOf 用于將字符串類型轉(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 []byte) string {
/* #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ì)造成影響。
推薦閱讀
