golang字符編碼的實(shí)現(xiàn)
1、 Golang 字符編碼
Golang
的代碼是由 Unicode
字符組成的,并由 Unicode
編碼規(guī)范中的 UTF-8
編碼格式進(jìn)行編碼并存儲。
Unicode
是編碼字符集,囊括了當(dāng)今世界使用的全部語言和符號的字符。有三種編碼形式:UTF-8
,UTF-16
,UTF-32
。(UTF: Unicode Transformation Format
,統(tǒng)一碼轉(zhuǎn)換格式)
在這幾種編碼格式的名稱中,-
右邊的整數(shù)的含義是,以多少個(gè)比特作為一個(gè)編碼單元。以 UTF-8
為例,它會以 8
個(gè)比特也就是一個(gè)字節(jié),作為一個(gè)編碼單元。并且,它與標(biāo)準(zhǔn)的 ASCII
編碼是完全兼容的。也就是說,在 [0x00, 0x7F]
的范圍內(nèi),這兩種編碼表示的字符都是相同的,這也是 UTF-8
編碼格式的一個(gè)巨大優(yōu)勢(這里不探討 UTF-16
及 UTF-32
)。
UTF-8
是一種可變長的編碼方案。換句話說,它會用一個(gè)或多個(gè)字節(jié)來表示某個(gè)字符,最多使用四個(gè)字節(jié)。比如,對于一個(gè)英文字符,它僅用一個(gè)字節(jié)就可以表示,而對于一個(gè)中文字符,它需要使用三個(gè)字節(jié)才能夠表示。不論怎樣,一個(gè)受支持的字符總是可以由 UTF-8
編碼為一個(gè)字節(jié)序列。以下會簡稱后者為 UTF-8
編碼值。
從上圖可知 UTF-8
的編碼方式:
- 什么時(shí)候讀
1
個(gè)字節(jié)的字符?
字節(jié)的第一位為0
,后面7
位為符號的unicode
碼。所以這樣看,英語字母的utf-8
和ascii
一致。
什么時(shí)候讀多個(gè)字節(jié)的字符?
對于有n
個(gè)字節(jié)的字符,(n>1
)…. 其中第一個(gè)字節(jié)的高n位就為1,換句話說:
- 第一個(gè)字節(jié)讀到
0
,那就是讀1
個(gè)字節(jié) - 第一個(gè)字節(jié)讀到
n
個(gè)1
,就要讀n
個(gè)字節(jié)
0xxxxxxx # 讀1個(gè)字節(jié) 110xxxxx 10xxxxxx # 讀2個(gè)字節(jié) 1110xxxx 10xxxxxx 10xxxxxx #讀3個(gè)字節(jié) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx #讀4個(gè)字節(jié) Unicode符號范圍 | UTF-8編碼方式 (十六進(jìn)制) | (二進(jìn)制) ------------------ -+--------------------------------------------- 0000 0000-0000 007F | 0xxxxxxx 0000 0080-0000 07FF | 110xxxxx 10xxxxxx 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
那 Unicode
是如何填充UTF-8
各個(gè)字節(jié)的呢?
比如 碼
這個(gè)漢字,對應(yīng)的 unicode
編碼為 U+7801
- 對應(yīng)的十六進(jìn)制處于
0000 0800-0000 FFFF
中,也就是3
個(gè)字節(jié),相應(yīng)的二進(jìn)制為1110xxxx 10xxxxxx 10xxxxxx
碼
的unicode
編碼為U+7801
對應(yīng)的二進(jìn)制為111100000000001
,為了和接下來填充字節(jié)方便,這里做個(gè)格式優(yōu)化111 100000 000001
- 從后向前填充,高位不夠的補(bǔ)
0
000001
填充第三個(gè)字節(jié)(從左往右數(shù))10000001
100000
填充第二個(gè)字節(jié)10100000
111
填充第一個(gè)字節(jié),高位不夠的就補(bǔ)0
,為11100111
- 最終結(jié)果為
11100111 10100000 10000001
(對應(yīng)的十六進(jìn)制分別對應(yīng)e7 a0 81
)
func TestInt(t *testing.T) { s1 := "碼" for i := 0; i < len(s1); i++ { fmt.Printf("%x ", s1[i]) } }
打印的結(jié)果為 e7 a0 81
,和上面演算的一致。
2、string 數(shù)據(jù)結(jié)構(gòu)
先來看看 Golang
的 string
的數(shù)據(jù)結(jié)構(gòu)
type StringHeader struct { Data uintptr Len int }
其中包含指向字節(jié)數(shù)組的指針 Data
和數(shù)組的大小 Len
,后者 Len
方便在 len()
時(shí)可以 O(1)
時(shí)間給出大小,就是常見的以空間換時(shí)間。字符串由字符組成,字符的底層由字節(jié)組成,而一個(gè)字符串在底層的表示是一個(gè)字節(jié)序列,這個(gè)字節(jié)序列就存儲在 Data
里,不過是只讀的。
import ( "fmt" "testing" ) func TestStr(t *testing.T) { str := "Hello World" fmt.Println(str) }
把上面代碼 go tool compile -S str_test.go > str_test.S
生成匯編代碼,然后找到
go.string."Hello World" SRODATA dupok size=11 0x0000 48 65 6c 6c 6f 20 57 6f 72 6c 64 Hello World
能夠看到 Hello World
旁有一個(gè) SRODATA
的標(biāo)記,在 Golang
中編譯器會將只讀數(shù)據(jù)標(biāo)記成 SRODATA
。
再來看看 slice
的數(shù)據(jù)結(jié)構(gòu)
type SliceHeader struct { Data uintptr Len int Cap int }
相比 string
多了個(gè) Cap
,因此在 Golang
中,字符串實(shí)際上是只讀的字節(jié)切片。
那么對于只讀的 string
,若是想要改值應(yīng)該怎么弄呢?
func TestModifyString(t *testing.T) { str := "golang編程" l := []byte(str) l[0] = 'G' fmt.Println(string(l)) // Golang編程 }
轉(zhuǎn)成相應(yīng)的字節(jié)數(shù)組,然后以索引的形式更新值。
3、string 編碼方式
前面說過,字符串由字符組成,字符的底層由字節(jié)組成,而一個(gè)字符串在底層的表示是一個(gè)字節(jié)序列。在 Golang
中,字符可以被分成兩種類型處理:對占 1
個(gè)字節(jié)的英文類字符,可以使用 byte
(或者 unit8
);對占 1 ~ 4
個(gè)字節(jié)的其他字符,可以使用 rune
(或者int32
),如中文、特殊符號等。
// 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
可以看到 byte
、 rune
其實(shí)分別就是 uint8
、int32
的別名,byte
占 1
個(gè)字節(jié), rune
占 4
個(gè)字節(jié)。
func TestStrLen(t *testing.T) { str1 := "go" str2 := "go編程" fmt.Printf("%v len is %d\n", str1, len(str1)) fmt.Printf("%v len is %d\n", str2, len(str2)) }
運(yùn)行后,發(fā)現(xiàn) str1
長度為 2
這個(gè)沒問題,但 str2
的長度不是 4
而是 8
,這是什么原因呢?
先不著急找答案,看看下面的代碼
func printBytes(s string) { fmt.Printf("Bytes: ") for i := 0; i < len(s); i++ { fmt.Printf("%x ", s[i]) // 按十六進(jìn)制輸出 } fmt.Printf("\n") } func printChars(s string) { fmt.Printf("Charaters: ") for i := 0; i < len(s); i++ { fmt.Printf("%c ", s[i]) // 將數(shù)字轉(zhuǎn)換成它對應(yīng)的 Unicode 字符 } fmt.Printf("\n") } func TestInt(t *testing.T) { s1 := "go編程" fmt.Printf("s1: %s, bytes len(s1)=%d\n", s1, len(s1)) fmt.Printf("s1: %s, rune len(s1)=%d\n", s1, len([]rune(s1))) printBytes(s1) printChars(s1) }
運(yùn)行后打印如下
s1: go編程, bytes len(s1)=8
s1: go編程, rune len(s1)=4
Bytes: 67 6f e7 bc 96 e7 a8 8b
Charaters: g o ç ¼ – ç ¨
仔細(xì)看,發(fā)現(xiàn) rune
類型的輸出了 4
,另外 printChars
輸出亂碼了。
先來看看 rune
類型,是 int32
的別名,也就是說,一個(gè) rune
類型的值會由 4
個(gè)字節(jié)寬度的空間來存儲。它的存儲空間總是能夠存下一個(gè) UTF-8
編碼值。一個(gè) rune
類型的值在底層其實(shí)就是一個(gè) UTF-8
編碼值。前者是(便于我們?nèi)祟惱斫獾模┩獠空宫F(xiàn),后者是(便于計(jì)算機(jī)系統(tǒng)理解的)內(nèi)在表達(dá)。
Golang
中常用 rune
類型來處理中文。printChars
之所以輸出亂碼,是因?yàn)樵诘谝还?jié)中提到的在 UTF-8
中漢字是以三個(gè)字節(jié)存儲的,len()
是按單字節(jié)來計(jì)算長度,因此對于三個(gè)字節(jié)的中文來說輸出三分之鐵定亂碼。那么如何輸出才不亂碼呢?
func TestRune(t *testing.T) { str := "golang編程" l := []rune(str) for i := 0; i < len(l); i++ { fmt.Printf("%c ", l[i]) } }
打印輸出 g o l a n g 編 程
。
當(dāng)然了,還可以使用 for range
來打印字符串里的中文。
func TestRange(t *testing.T) { str := "golang編程" for i, s := range str { fmt.Printf("%d: %c\n", i, s) } }
打印輸出
0: g
1: o
2: l
3: a
4: n
5: g
6: 編
9: 程
那為什么會這樣呢?原因就在 Golang
中,會把 for range
結(jié)構(gòu)轉(zhuǎn)換成如下所示的形式
// Transform string range statements like "for v1, v2 = range a" into ha := a for hv1 := 0; hv1 < len(ha); { hv1t := hv1 hv2 := rune(ha[hv1]) if hv2 < utf8.RuneSelf { hv1++ } else { hv2, hv1 = decoderune(ha, hv1) } v1, v2 = hv1t, hv2 // original body }
for range
循環(huán)在迭代字符串時(shí)會逐個(gè)處理字符串中的 Unicode
碼點(diǎn)(rune
),而不是字節(jié)。由于 Golang
的原生字符串類型是以 UTF-8
編碼的,UTF-8
是一種能夠表示 Unicode
碼點(diǎn)的變長編碼方式,for range
循環(huán)能夠正確處理這種編碼。
通俗點(diǎn)就是 for range
會先把被遍歷的字符串值拆成一個(gè)字節(jié)序列,然后再試圖找出這個(gè)字節(jié)序列中包含的每一個(gè) UTF-8
編碼值,或者說每一個(gè) Unicode
字符。
func TestRange(t *testing.T) { str := "golang編程" for i, s := range str { fmt.Printf("%d: %c [% x]\n", i, s, []byte(string(s))) } }
打印輸出
0: g [67]
1: o [6f]
2: l [6c]
3: a [61]
4: n [6e]
5: g [67]
6: 編 [e7 bc 96]
9: 程 [e7 a8 8b]
由此可以看出,字符串中相鄰 Unicode
字符的索引值不一定是連續(xù)的。 這取決于前一個(gè) Unicode
字符是否為單字節(jié)字符(byte
)。Golang
中的一個(gè) string
類型值會由若干個(gè) Unicode
字符組成,每個(gè) Unicode
字符都可以由一個(gè) rune
類型的值來承載。這些字符在底層都會被轉(zhuǎn)換為 UTF-8
編碼值,而這些 UTF-8
編碼值又會以字節(jié)序列的形式表達(dá)和存儲。因此,一個(gè)string
類型的值在底層就是一個(gè)能夠表達(dá)若干個(gè) UTF-8
編碼值的字節(jié)序列。
ok,到這里了,發(fā)現(xiàn)兩種不同的 for
循環(huán)在輸出字符串的字符時(shí)會有所不同,這里做個(gè)歸類
for-standalone
會遍歷字符串的每一個(gè)字節(jié)(Byte
類型),在遇到字符串中有漢字時(shí)會亂碼for-range
會遍歷字符串的每一個(gè)Unicode
字符(Rune
類型) ,在遇到字符串中有漢字時(shí)不會亂碼
最后說說 string
、byte
和 rune
三者之間的關(guān)系。
string
在底層的表示是由單個(gè)字節(jié)組成的只讀的字節(jié)序列,Golang
的字符串是以UTF-8
編碼存儲的,這意味著它們可以包含任意的Unicode
字符。Golang
把字符分byte
和rune
兩種類型處理。byte
是類型unit8
的別名,用于存放占1
個(gè)字節(jié)的ASCII
字符,如英文字符,返回的是字符原始字節(jié)。由于Golang
的字符串是以UTF-8
編碼的,一個(gè)byte
可能表示一個(gè)字符的一部分(對于多字節(jié)字符如中文字符),也可能表示一個(gè)完整的字符(對于ASCII
字符)。rune
是類型int32
的別名,用于存放多字節(jié)字符,如占3
字節(jié)的中文字符,返回的是字符Unicode
碼點(diǎn)值(或者說它代表一個(gè)Unicode
碼點(diǎn))。在處理字符串時(shí),rune
用于表示字符串中的一個(gè)完整的Unicode
字符,無論這個(gè)字符是由多少個(gè)字節(jié)組成的。rune
類型的變量可以存儲任何Unicode
字符,包括那些由多個(gè)字節(jié)表示的字符。
等等,等等,到這里,不妨再多看看。那么如果計(jì)算一個(gè)字符串的長度呢,用自帶的 len()
函數(shù)對于單字節(jié)的字符串來說是準(zhǔn)確的,若是帶有中文字符這種多字節(jié)的字符串就不準(zhǔn)確了,這時(shí)除了自己造輪子外,其實(shí)可以用 Golang
內(nèi)置的 utf8.RuneCountInString
來統(tǒng)計(jì)。
func TestCountStr(t *testing.T) { str := "golang編程" fmt.Println(utf8.RuneCountInString(str)) // 8 }
有興趣的讀者可以看看其內(nèi)部實(shí)現(xiàn)。
// RuneCountInString is like RuneCount but its input is a string. func RuneCountInString(s string) (n int) { ns := len(s) for i := 0; i < ns; n++ { c := s[i] if c < RuneSelf { // ASCII fast path i++ continue } x := first[c] if x == xx { i++ // invalid. continue } size := int(x & 7) if i+size > ns { i++ // Short or invalid. continue } accept := acceptRanges[x>>4] 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 }
到此這篇關(guān)于golang字符編碼的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)golang字符編碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go?Excelize?API源碼閱讀SetSheetViewOptions示例解析
這篇文章主要為大家介紹了Go-Excelize?API源碼閱讀SetSheetViewOptions示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08golang-gin-mgo高并發(fā)服務(wù)器搭建教程
這篇文章主要介紹了golang-gin-mgo高并發(fā)服務(wù)器搭建教程,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12golang API開發(fā)過程的中的自動重啟方式(基于gin框架)
這篇文章主要介紹了golang API開發(fā)過程的中的自動重啟方式(基于gin框架),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12Golang新提案:panic?能不能加個(gè)?PanicError?
這篇文章主要為大家介紹了Golang的新提案關(guān)于panic能不能加個(gè)PanicError的問題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12golang通過反射手動實(shí)現(xiàn)json序列化的方法
在 Go 語言中,JSON 序列化和反序列化通常通過標(biāo)準(zhǔn)庫 encoding/json 來實(shí)現(xiàn),本文給大家介紹golang 通過反射手動實(shí)現(xiàn)json序列化的方法,感興趣的朋友一起看看吧2024-12-12GoLang抽獎系統(tǒng)簡易實(shí)現(xiàn)流程
這篇文章主要介紹了GoLang抽獎系統(tǒng)實(shí)現(xiàn)流程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-12-12Golang中map的三種聲明定義方式實(shí)現(xiàn)
本文主要介紹了Golang中map的三種聲明定義方式實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02Golang 語言控制并發(fā) Goroutine的方法
本文我們介紹了不同場景中分別適合哪種控制并發(fā) goroutine 的方式,其中,channel 適合控制少量 并發(fā) goroutine,WaitGroup 適合控制一組并發(fā) goroutine,而 context 適合控制多級并發(fā) goroutine,感興趣的朋友跟隨小編一起看看吧2021-06-06