golang字符編碼的實(shí)現(xiàn)
1、 Golang 字符編碼
Golang 的代碼是由 Unicode 字符組成的,并由 Unicode 編碼規(guī)范中的 UTF-8 編碼格式進(jìn)行編碼并存儲(chǔ)。
Unicode 是編碼字符集,囊括了當(dāng)今世界使用的全部語(yǔ)言和符號(hào)的字符。有三種編碼形式:UTF-8,UTF-16,UTF-32。(UTF: Unicode Transformation Format,統(tǒng)一碼轉(zhuǎn)換格式)
在這幾種編碼格式的名稱中,- 右邊的整數(shù)的含義是,以多少個(gè)比特作為一個(gè)編碼單元。以 UTF-8 為例,它會(huì)以 8 個(gè)比特也就是一個(gè)字節(jié),作為一個(gè)編碼單元。并且,它與標(biāo)準(zhǔn)的 ASCII 編碼是完全兼容的。也就是說(shuō),在 [0x00, 0x7F]的范圍內(nèi),這兩種編碼表示的字符都是相同的,這也是 UTF-8 編碼格式的一個(gè)巨大優(yōu)勢(shì)(這里不探討 UTF-16 及 UTF-32)。
UTF-8 是一種可變長(zhǎng)的編碼方案。換句話說(shuō),它會(huì)用一個(gè)或多個(gè)字節(jié)來(lái)表示某個(gè)字符,最多使用四個(gè)字節(jié)。比如,對(duì)于一個(gè)英文字符,它僅用一個(gè)字節(jié)就可以表示,而對(duì)于一個(gè)中文字符,它需要使用三個(gè)字節(jié)才能夠表示。不論怎樣,一個(gè)受支持的字符總是可以由 UTF-8 編碼為一個(gè)字節(jié)序列。以下會(huì)簡(jiǎn)稱后者為 UTF-8 編碼值。

從上圖可知 UTF-8 的編碼方式:
- 什么時(shí)候讀
1個(gè)字節(jié)的字符?
字節(jié)的第一位為0,后面7位為符號(hào)的unicode碼。所以這樣看,英語(yǔ)字母的utf-8和ascii一致。
什么時(shí)候讀多個(gè)字節(jié)的字符?
對(duì)于有n個(gè)字節(jié)的字符,(n>1)…. 其中第一個(gè)字節(jié)的高n位就為1,換句話說(shuō):
- 第一個(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符號(hào)范圍 | 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è)漢字,對(duì)應(yīng)的 unicode編碼為 U+7801
- 對(duì)應(yīng)的十六進(jìn)制處于
0000 0800-0000 FFFF中,也就是3個(gè)字節(jié),相應(yīng)的二進(jìn)制為1110xxxx 10xxxxxx 10xxxxxx 碼的unicode編碼為U+7801對(duì)應(yīng)的二進(jìn)制為111100000000001,為了和接下來(lái)填充字節(jié)方便,這里做個(gè)格式優(yōu)化111 100000 000001- 從后向前填充,高位不夠的補(bǔ)
0 000001填充第三個(gè)字節(jié)(從左往右數(shù))10000001100000填充第二個(gè)字節(jié)10100000111填充第一個(gè)字節(jié),高位不夠的就補(bǔ)0,為11100111- 最終結(jié)果為
11100111 10100000 10000001(對(duì)應(yīng)的十六進(jìn)制分別對(duì)應(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)
先來(lái)看看 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í)間給出大小,就是常見(jiàn)的以空間換時(shí)間。字符串由字符組成,字符的底層由字節(jié)組成,而一個(gè)字符串在底層的表示是一個(gè)字節(jié)序列,這個(gè)字節(jié)序列就存儲(chǔ)在 Data 里,不過(guò)是只讀的。
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 中編譯器會(huì)將只讀數(shù)據(jù)標(biāo)記成 SRODATA。
再來(lái)看看 slice 的數(shù)據(jù)結(jié)構(gòu)
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
相比 string 多了個(gè) Cap,因此在 Golang 中,字符串實(shí)際上是只讀的字節(jié)切片。
那么對(duì)于只讀的 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 編碼方式
前面說(shuō)過(guò),字符串由字符組成,字符的底層由字節(jié)組成,而一個(gè)字符串在底層的表示是一個(gè)字節(jié)序列。在 Golang 中,字符可以被分成兩種類型處理:對(duì)占 1 個(gè)字節(jié)的英文類字符,可以使用 byte(或者 unit8);對(duì)占 1 ~ 4 個(gè)字節(jié)的其他字符,可以使用 rune(或者int32),如中文、特殊符號(hào)等。
// 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 長(zhǎng)度為 2 這個(gè)沒(méi)問(wèn)題,但 str2 的長(zhǎng)度不是 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)換成它對(duì)應(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 輸出亂碼了。
先來(lái)看看 rune 類型,是 int32 的別名,也就是說(shuō),一個(gè) rune 類型的值會(huì)由 4 個(gè)字節(jié)寬度的空間來(lái)存儲(chǔ)。它的存儲(chǔ)空間總是能夠存下一個(gè) UTF-8 編碼值。一個(gè) rune 類型的值在底層其實(shí)就是一個(gè) UTF-8 編碼值。前者是(便于我們?nèi)祟惱斫獾模┩獠空宫F(xiàn),后者是(便于計(jì)算機(jī)系統(tǒng)理解的)內(nèi)在表達(dá)。
Golang 中常用 rune 類型來(lái)處理中文。printChars 之所以輸出亂碼,是因?yàn)樵诘谝还?jié)中提到的在 UTF-8 中漢字是以三個(gè)字節(jié)存儲(chǔ)的,len() 是按單字節(jié)來(lái)計(jì)算長(zhǎng)度,因此對(duì)于三個(gè)字節(jié)的中文來(lái)說(shuō)輸出三分之鐵定亂碼。那么如何輸出才不亂碼呢?
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 來(lái)打印字符串里的中文。
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: 程
那為什么會(huì)這樣呢?原因就在 Golang 中,會(huì)把 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í)會(huì)逐個(gè)處理字符串中的 Unicode 碼點(diǎn)(rune),而不是字節(jié)。由于 Golang 的原生字符串類型是以 UTF-8 編碼的,UTF-8 是一種能夠表示 Unicode 碼點(diǎn)的變長(zhǎng)編碼方式,for range 循環(huán)能夠正確處理這種編碼。
通俗點(diǎn)就是 for range 會(huì)先把被遍歷的字符串值拆成一個(gè)字節(jié)序列,然后再試圖找出這個(gè)字節(jié)序列中包含的每一個(gè) UTF-8 編碼值,或者說(shuō)每一個(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 類型值會(huì)由若干個(gè) Unicode 字符組成,每個(gè) Unicode 字符都可以由一個(gè) rune 類型的值來(lái)承載。這些字符在底層都會(huì)被轉(zhuǎn)換為 UTF-8 編碼值,而這些 UTF-8 編碼值又會(huì)以字節(jié)序列的形式表達(dá)和存儲(chǔ)。因此,一個(gè)string 類型的值在底層就是一個(gè)能夠表達(dá)若干個(gè) UTF-8 編碼值的字節(jié)序列。
ok,到這里了,發(fā)現(xiàn)兩種不同的 for 循環(huán)在輸出字符串的字符時(shí)會(huì)有所不同,這里做個(gè)歸類
for-standalone會(huì)遍歷字符串的每一個(gè)字節(jié)(Byte類型),在遇到字符串中有漢字時(shí)會(huì)亂碼for-range會(huì)遍歷字符串的每一個(gè)Unicode字符(Rune類型) ,在遇到字符串中有漢字時(shí)不會(huì)亂碼
最后說(shuō)說(shuō) string、byte 和 rune 三者之間的關(guān)系。
string在底層的表示是由單個(gè)字節(jié)組成的只讀的字節(jié)序列,Golang的字符串是以UTF-8編碼存儲(chǔ)的,這意味著它們可以包含任意的Unicode字符。Golang把字符分byte和rune兩種類型處理。byte是類型unit8的別名,用于存放占1個(gè)字節(jié)的ASCII字符,如英文字符,返回的是字符原始字節(jié)。由于Golang的字符串是以UTF-8編碼的,一個(gè)byte可能表示一個(gè)字符的一部分(對(duì)于多字節(jié)字符如中文字符),也可能表示一個(gè)完整的字符(對(duì)于ASCII字符)。rune是類型int32的別名,用于存放多字節(jié)字符,如占3字節(jié)的中文字符,返回的是字符Unicode碼點(diǎn)值(或者說(shuō)它代表一個(gè)Unicode碼點(diǎn))。在處理字符串時(shí),rune用于表示字符串中的一個(gè)完整的Unicode字符,無(wú)論這個(gè)字符是由多少個(gè)字節(jié)組成的。rune類型的變量可以存儲(chǔ)任何Unicode字符,包括那些由多個(gè)字節(jié)表示的字符。
等等,等等,到這里,不妨再多看看。那么如果計(jì)算一個(gè)字符串的長(zhǎng)度呢,用自帶的 len() 函數(shù)對(duì)于單字節(jié)的字符串來(lái)說(shuō)是準(zhǔn)確的,若是帶有中文字符這種多字節(jié)的字符串就不準(zhǔn)確了,這時(shí)除了自己造輪子外,其實(shí)可以用 Golang 內(nèi)置的 utf8.RuneCountInString 來(lái)統(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang使用channel實(shí)現(xiàn)數(shù)據(jù)匯總的方法詳解
這篇文章主要為大家詳細(xì)介紹了在并發(fā)編程中數(shù)據(jù)匯總的問(wèn)題,并探討了在并發(fā)環(huán)境下使用互斥鎖和通道兩種方式來(lái)保證數(shù)據(jù)安全性的方法,需要的可以參考一下2023-05-05
Go應(yīng)該如何實(shí)現(xiàn)二級(jí)緩存
本文主要介紹了Go二級(jí)緩存,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
golang原生http包實(shí)現(xiàn)各種情況的get請(qǐng)求方式
這篇文章主要介紹了golang原生http包實(shí)現(xiàn)各種情況的get請(qǐng)求方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
Go語(yǔ)言實(shí)現(xiàn)統(tǒng)計(jì)字符串中每個(gè)字符出現(xiàn)的次數(shù)
這篇文章主要為大家詳細(xì)介紹了如何使用Go語(yǔ)言開(kāi)發(fā)一個(gè)簡(jiǎn)易頻率分析器,實(shí)現(xiàn)統(tǒng)計(jì)字符串中每個(gè)字符出現(xiàn)的次數(shù),感興趣的小伙伴可以了解一下2025-07-07
Go網(wǎng)絡(luò)編程TCP抓包實(shí)操示例探究
作為一名軟件開(kāi)發(fā)者,網(wǎng)絡(luò)編程是必備知識(shí),本文通過(guò)?Go?語(yǔ)言實(shí)現(xiàn)?TCP?套接字編程,并結(jié)合?tcpdump?工具,展示它的三次握手、數(shù)據(jù)傳輸以及四次揮手的過(guò)程,幫助讀者更好地理解?TCP?協(xié)議與?Go?網(wǎng)絡(luò)編程2024-01-01
Go Slice 實(shí)現(xiàn)原理從底層機(jī)制到工程實(shí)踐詳解
本文將從企業(yè)級(jí)開(kāi)發(fā)視角,深入剖析 Slice的底層實(shí)現(xiàn)原理,結(jié)合性能優(yōu)化實(shí)踐,幫助開(kāi)發(fā)者寫(xiě)出更高效、更安全的 Go 代碼,對(duì)go slice實(shí)現(xiàn)原理感興趣的朋友跟隨小編一起看看吧2025-11-11
Golang中List的實(shí)現(xiàn)方法示例詳解
最近決定復(fù)習(xí)下Go,所以下面這篇文章主要給大家介紹了關(guān)于Golang中List的實(shí)現(xiàn)方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09

