string、byte、rune和字符在go中的区别的

    // \xe4\xb8\xad  utf8: e4b8ad, U+4E2D, 中
	// \xe5\x9b\xbd  utf8: e59bbd, U+56FD, 国
	// \xe4\xba\xba  utf8: e4baba, U+4EBA, 人
    // \xe2\x8c\x98  utf8: e28c98, U+2318, ⌘
    const sample = "\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba\xbd\x20\x3d\xe2\x8c\x98"
	fmt.Printf("len: %d \n", len(sample))
	fmt.Println(sample)
	for i := 0; i < len(sample); i++ {
		fmt.Printf("%U index: %d\n", sample[i], i)
	}
	for i, v := range sample {
		fmt.Printf("%#U index: %d\n", v, i)
	}
	fmt.Println()

疑问

本文将围绕上述代码进行讨论,如果你能准确的答出上述代码结果,则可直接看文末总结了;看完本文我们可以解答如下知识点:

  • rune类型是什么,和string有什么关联关系?
  • string通过下标index访问和for range 访问的区别?
  • s[i]是指向字符串s的第n个字符?

字符串是什么

在go语言中,字符串是一个只读的字节数组([]byte),字符串里面可以包含任意的字节,它不需要包含unicode、utf8或者其他预定义的格式;
如下是一个包含\x转义字符的字符串字面常量,稍微作一点简单说明,前三个字节utf8格式编码为为0xe4b8ad, unicode码是U+4E2D,中文字符是‘中’;其他字符可以将行首的代码实例运行起来观察;

    const sample = "\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba\xbd\x20\x3d\xe2\x8c\x98"

字符串打印输出技巧

由于在上述字符串常量中包含无效ASCII和无效的UTF-8,因此直接打印字符串的话,会包含一些迷惑的输出:

    fmt.Println(sample)
    // 输出
    // 中国人� =⌘

对于字符串而言,最常见的方式则为通过下标索引的方式打印,每个独立的索引处都是一个byte,这里注意,输出的字节和字符串字面常量中定义的转义字节一一对应:

    for i := 0; i < len(sample); i++ {
        fmt.Printf("%x ", sample[i])
    }
    // 输出
    // e4 b8 ad e5 9b bd e4 ba ba bd 20 3d e2 8c 98

同样,对于字符串和字节切片来说,可以通过%x的方式便捷的打印字符串为十六进制的byte序列,

    fmt.Printf("%x\n", sample)
    // 输出
    // e4b8ade59bbde4bababd203de28c98

一个不错的技巧是使用该格式的“空格”标志,在 % 和 x 之间放置一个空格。 将此处使用的格式字符串与上面的格式字符串进行比较:

    fmt.Printf("% x\n", sample)
    // 输出
    // e4 b8 ad e5 9b bd e4 ba ba bd 20 3d e2 8c 98

更多的,%q 的打印方式将字符串以unicode方式输出,其他无效的uft8格式将进行转义字符串字节序列的方式打印:

    fmt.Printf("%q\n", sample)
    // 输出
    // "中国人\xbd =⌘"

同样,在%q之间放置 +号,将字符串除了ASCII外的其他byte序列以utf8的方式输出,具体的结果为unicode字符集;结果中可以看到除了倒数第二三两个字符是 =,其他的字符都是以unicode字符集打印;


    fmt.Printf("%+q\n", sample)
    // 输出
    // "\u4e2d\u56fd\u4eba\xbd =\u2318"

值得指出的是,所有这些方法对字节切片的行为与对字符串的行为完全相同;可以自行将sample变量强转位[]byte类型进行上述打印测试;

字符串字面量和utf8的关系

从上面的验证来看,字符串通过下标索引的是byte,而不是对应的字符,这就证明了字符串本质是只读的字节数组;

如下是一个简单的程序,它以三种不同的方式打印带有单个字符的字符串常量,一种作为纯字符串,一种作为仅打印 ASCII 字符的字符串,一种作为十六进制的单个字节。 为了避免混淆,我们创建了一个“原始字符串”,用反引号括起来,因此它只能包含文字文本不包含转义字符。 (用双引号括起来的常规字符串可以包含如上所示的转义序列。)

func main() {
    const placeOfInterest = `⌘`

    fmt.Printf("plain string: ")
    fmt.Printf("%s", placeOfInterest)
    fmt.Printf("\n")

    fmt.Printf("quoted string: ")
    fmt.Printf("%+q", placeOfInterest)
    fmt.Printf("\n")

    fmt.Printf("hex bytes: ")
    for i := 0; i < len(placeOfInterest); i++ {
        fmt.Printf("%x ", placeOfInterest[i])
    }
    fmt.Printf("\n")

    // 输出
    // plain string: ⌘
    // quoted string: "\u2318"
    // hex bytes: e2 8c 98
}

这结果表明,Unicode 字符值 U+2318,即符号⌘,由字节 e2 8c 98 表示,并且这些字节是十六进制值 0x2318 的 UTF-8 编码。

简而言之,Go 源代码是以 UTF-8 编码的 Unicode 字符,因此字符串字面量的源代码是 UTF-8 文本;如果字符串字面量不包含转义的字符,则字符串一定是有效的utf8格式,

码点(unicode字符集)、字符和rune的关系

上面一直在讲述string的本质问题,还未描述byte和character的区别;像小写字母a是一个字符,unicode为U+00E0,也是一个字符,unicode为U+2318;因此这里我理解字符即为unicode字符集种的字符;
Unicode 标准使用术语“代码点”来指代由单个值表示的项目。 码点 U+2318,十六进制值为 2318,代表符号⌘。代码点读起来有点令人费解,在go语言中,int32的别名rune替代代码点,因此go中int32类型即可表示uncode字符集的字符数值;'⌘'rune类型,十六进制为0x2318;

总而言之,以下是要点

  • Go 源代码始终是 UTF-8。
  • 字符串包含任意字节。
  • 没有包含转义字节的字符串字面量始终是有效的 UTF-8 序列。
  • 这些序列代表 Unicode 代码点,称为字符。
  • Go 不保证字符串中的字符被规范化,即拥有不符合unicode、ASCII的无效任意转义字符。

for loop 范围for循环

for range 循环是唯一可以在go中以UTF-8 的方式遍历字符串的;
我们已经看到了下标索引 for 循环是遍历字节。 相比之下,for range 循环在每次迭代中解码一个 UTF-8 编码的符文。 每次循环,循环的索引是当前字符的起始位置,以字节为单位,代码点是它的unicode值。 如下,它显示了代码点的 Unicode 值及其打印表示:

    const sample = "中国人"
    for i, v := range sample {
		fmt.Printf("%#U start index: %d\n", v, i)
	}
    // 输出
    // U+4E2D '中' start index: 0
    // U+56FD '国' start index: 3
    // U+4EBA '人' start index: 6

还有种方法可以快速的访问字符串中的utf8字符,即将字符串转化为rune切片,如下代码所示:

    const sample = "中国人"
    var sampleSlice = []rune(sample)
    for i, v := range sampleSlice {
		fmt.Printf("%#U start index: %d\n", v, i)
	}

    // 输出
    // U+4E2D '中' start index: 0
    // U+56FD '国' start index: 1
    // U+4EBA '人' start index: 2

utf8

go中unicode/utf8库提供了utf格式相关的操作,它包含用于验证、反汇编和重新组合 UTF-8 字符串的辅助例程。 如下是一个与上面的 for range 示例等效的程序,但使用该包中的 DecodeRuneInString 函数来完成这项工作。 该函数的返回值是符文及其 UTF-8 编码字节的宽度

    const nihongo = "中国人"
    for i, w := 0, 0; i < len(nihongo); i += w {
        runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
        fmt.Printf("%#U start index: %d\n", runeValue, i)
        w = width
    }
    // 输出
    // U+4E2D '中' start index: 0
    // U+56FD '国' start index: 3
    // U+4EBA '人' start index: 6

总结

问题解答:

rune类型是什么,和string有什么关联关系?

rune本质是int32类型,代表string中的字符,即utf8形式的unicode数值

string通过下标index访问和for range 访问的区别?

下标访问取的是单个的字节,for range访问取的是单个 utf-8 字符

s[i]是指向字符串s的第n个字符?

s[i]指向的是第n个字节,

参考链接:

  • https://go.dev/blog/strings
  • https://util.unicode.org/UnicodeJsps/character.jsp?a=%E4%B8%AD&B1=Show
  • https://go.dev/ref/spec#Rune_literals
  • http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值