1、 golang 字符编码
golang
的代码是由 unicode
字符组成的,并由 unicode
编码规范中的 utf-8
编码格式进行编码并存储。
unicode
是编码字符集,囊括了当今世界使用的全部语言和符号的字符。有三种编码形式:utf-8
,utf-16
,utf-32
。(utf: unicode transformation format
,统一码转换格式)
在这几种编码格式的名称中,-
右边的整数的含义是,以多少个比特作为一个编码单元。以 utf-8
为例,它会以 8
个比特也就是一个字节,作为一个编码单元。并且,它与标准的 ascii
编码是完全兼容的。也就是说,在 [0x00, 0x7f]
的范围内,这两种编码表示的字符都是相同的,这也是 utf-8
编码格式的一个巨大优势(这里不探讨 utf-16
及 utf-32
)。
utf-8
是一种可变长的编码方案。换句话说,它会用一个或多个字节来表示某个字符,最多使用四个字节。比如,对于一个英文字符,它仅用一个字节就可以表示,而对于一个中文字符,它需要使用三个字节才能够表示。不论怎样,一个受支持的字符总是可以由 utf-8
编码为一个字节序列。以下会简称后者为 utf-8
编码值。
从上图可知 utf-8
的编码方式:
- 什么时候读
1
个字节的字符?
字节的第一位为0
,后面7
位为符号的unicode
码。所以这样看,英语字母的utf-8
和ascii
一致。
什么时候读多个字节的字符?
对于有n
个字节的字符,(n>1
)…. 其中第一个字节的高n位就为1,换句话说:
- 第一个字节读到
0
,那就是读1
个字节 - 第一个字节读到
n
个1
,就要读n
个字节
0xxxxxxx # 读1个字节 110xxxxx 10xxxxxx # 读2个字节 1110xxxx 10xxxxxx 10xxxxxx #读3个字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx #读4个字节 unicode符号范围 | utf-8编码方式 (十六进制) | (二进制) ------------------ -+--------------------------------------------- 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
各个字节的呢?
比如 码
这个汉字,对应的 unicode
编码为 u+7801
- 对应的十六进制处于
0000 0800-0000 ffff
中,也就是3
个字节,相应的二进制为1110xxxx 10xxxxxx 10xxxxxx
码
的unicode
编码为u+7801
对应的二进制为111100000000001
,为了和接下来填充字节方便,这里做个格式优化111 100000 000001
- 从后向前填充,高位不够的补
0
000001
填充第三个字节(从左往右数)10000001
100000
填充第二个字节10100000
111
填充第一个字节,高位不够的就补0
,为11100111
- 最终结果为
11100111 10100000 10000001
(对应的十六进制分别对应e7 a0 81
)
func testint(t *testing.t) { s1 := "码" for i := 0; i < len(s1); i++ { fmt.printf("%x ", s1[i]) } }
打印的结果为 e7 a0 81
,和上面演算的一致。
2、string 数据结构
先来看看 golang
的 string
的数据结构
type stringheader struct { data uintptr len int }
其中包含指向字节数组的指针 data
和数组的大小 len
,后者 len
方便在 len()
时可以 o(1)
时间给出大小,就是常见的以空间换时间。字符串由字符组成,字符的底层由字节组成,而一个字符串在底层的表示是一个字节序列,这个字节序列就存储在 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
旁有一个 srodata
的标记,在 golang
中编译器会将只读数据标记成 srodata
。
再来看看 slice
的数据结构
type sliceheader struct { data uintptr len int cap int }
相比 string
多了个 cap
,因此在 golang
中,字符串实际上是只读的字节切片。
那么对于只读的 string
,若是想要改值应该怎么弄呢?
func testmodifystring(t *testing.t) { str := "golang编程" l := []byte(str) l[0] = 'g' fmt.println(string(l)) // golang编程 }
转成相应的字节数组,然后以索引的形式更新值。
3、string 编码方式
前面说过,字符串由字符组成,字符的底层由字节组成,而一个字符串在底层的表示是一个字节序列。在 golang
中,字符可以被分成两种类型处理:对占 1
个字节的英文类字符,可以使用 byte
(或者 unit8
);对占 1 ~ 4
个字节的其他字符,可以使用 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
其实分别就是 uint8
、int32
的别名,byte
占 1
个字节, rune
占 4
个字节。
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)) }
运行后,发现 str1
长度为 2
这个没问题,但 str2
的长度不是 4
而是 8
,这是什么原因呢?
先不着急找答案,看看下面的代码
func printbytes(s string) { fmt.printf("bytes: ") for i := 0; i < len(s); i++ { fmt.printf("%x ", s[i]) // 按十六进制输出 } fmt.printf("\n") } func printchars(s string) { fmt.printf("charaters: ") for i := 0; i < len(s); i++ { fmt.printf("%c ", s[i]) // 将数字转换成它对应的 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) }
运行后打印如下
s1: go编程, bytes len(s1)=8
s1: go编程, rune len(s1)=4
bytes: 67 6f e7 bc 96 e7 a8 8b
charaters: g o ç ¼ – ç ¨
仔细看,发现 rune
类型的输出了 4
,另外 printchars
输出乱码了。
先来看看 rune
类型,是 int32
的别名,也就是说,一个 rune
类型的值会由 4
个字节宽度的空间来存储。它的存储空间总是能够存下一个 utf-8
编码值。一个 rune
类型的值在底层其实就是一个 utf-8
编码值。前者是(便于我们人类理解的)外部展现,后者是(便于计算机系统理解的)内在表达。
golang
中常用 rune
类型来处理中文。printchars
之所以输出乱码,是因为在第一节中提到的在 utf-8
中汉字是以三个字节存储的,len()
是按单字节来计算长度,因此对于三个字节的中文来说输出三分之铁定乱码。那么如何输出才不乱码呢?
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 编 程
。
当然了,还可以使用 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
结构转换成如下所示的形式
// 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
循环在迭代字符串时会逐个处理字符串中的 unicode
码点(rune
),而不是字节。由于 golang
的原生字符串类型是以 utf-8
编码的,utf-8
是一种能够表示 unicode
码点的变长编码方式,for range
循环能够正确处理这种编码。
通俗点就是 for range
会先把被遍历的字符串值拆成一个字节序列,然后再试图找出这个字节序列中包含的每一个 utf-8
编码值,或者说每一个 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
字符的索引值不一定是连续的。 这取决于前一个 unicode
字符是否为单字节字符(byte
)。golang
中的一个 string
类型值会由若干个 unicode
字符组成,每个 unicode
字符都可以由一个 rune
类型的值来承载。这些字符在底层都会被转换为 utf-8
编码值,而这些 utf-8
编码值又会以字节序列的形式表达和存储。因此,一个string
类型的值在底层就是一个能够表达若干个 utf-8
编码值的字节序列。
ok,到这里了,发现两种不同的 for
循环在输出字符串的字符时会有所不同,这里做个归类
for-standalone
会遍历字符串的每一个字节(byte
类型),在遇到字符串中有汉字时会乱码for-range
会遍历字符串的每一个unicode
字符(rune
类型) ,在遇到字符串中有汉字时不会乱码
最后说说 string
、byte
和 rune
三者之间的关系。
string
在底层的表示是由单个字节组成的只读的字节序列,golang
的字符串是以utf-8
编码存储的,这意味着它们可以包含任意的unicode
字符。golang
把字符分byte
和rune
两种类型处理。byte
是类型unit8
的别名,用于存放占1
个字节的ascii
字符,如英文字符,返回的是字符原始字节。由于golang
的字符串是以utf-8
编码的,一个byte
可能表示一个字符的一部分(对于多字节字符如中文字符),也可能表示一个完整的字符(对于ascii
字符)。rune
是类型int32
的别名,用于存放多字节字符,如占3
字节的中文字符,返回的是字符unicode
码点值(或者说它代表一个unicode
码点)。在处理字符串时,rune
用于表示字符串中的一个完整的unicode
字符,无论这个字符是由多少个字节组成的。rune
类型的变量可以存储任何unicode
字符,包括那些由多个字节表示的字符。
等等,等等,到这里,不妨再多看看。那么如果计算一个字符串的长度呢,用自带的 len()
函数对于单字节的字符串来说是准确的,若是带有中文字符这种多字节的字符串就不准确了,这时除了自己造轮子外,其实可以用 golang
内置的 utf8.runecountinstring
来统计。
func testcountstr(t *testing.t) { str := "golang编程" fmt.println(utf8.runecountinstring(str)) // 8 }
有兴趣的读者可以看看其内部实现。
// 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 }
到此这篇关于golang字符编码的实现的文章就介绍到这了,更多相关golang字符编码内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论