关于golang:深入剖析go中字符串的编码问题特殊字符的string怎么转byte

40次阅读

共计 4822 个字符,预计需要花费 13 分钟才能阅读完成。

来自公众号:新世界杂货铺

前言

前段时间发表了 Go 中的 HTTP 申请之——HTTP1.1 申请流程剖析,所以这两天原本打算钻研 HTTP2.0 的申请源码,后果发现太简单就跑去逛知乎了,而后就发现了一个十分有意思的发问“golang 特殊字符的 string 怎么转成[]byte?”。为了转换一下情绪,便有了此篇文章。

问题

原问题我就不码字了,间接上图:

看到问题,我的第一反馈是 ASCII 码值范畴应该是 0~127 呀,怎么会超过 127 呢?直到理论运行的时候才发现上图的特殊字符是‘’(如果无奈展现,记住该特殊字符的 unicode 是\u0081),并不是英文中的句号。

unicode 和 utf- 8 的恩怨瓜葛

百度百科曾经把 unicode 和 utf- 8 介绍的很具体了,所以这里就不做过多的论述,仅摘抄局部和本文相干的定义:

  • Unicode 为每个字符设定了对立并且惟一的二进制编码,通常用两个字节示意一个字符
  • UTF- 8 是针对 Unicode 的一种可变长度字符编码。它能够用来示意 Unicode 规范中的任何字符。UTF- 8 的特点是对不同范畴的字符应用不同长度的编码。对于 0x00-0x7F 之间的字符,UTF- 8 编码与 ASCII 编码完全相同

go 中的字符

家喻户晓,go 中能示意字符的有两种类型,别离是 byterune,byte 和 rune 的定义别离是:type byte = uint8type rune = int32

uint8 范畴是 0 -255,只可能示意无限个 unicode 字符,超过 255 的范畴就会编译报错。根据上述对于 unicode 的定义,4 字节的 rune 齐全兼容两字节的 unicode。

咱们用上面的代码来验证:

var (
        c1 byte = 'a'
        c2 byte = '新'
        c3 rune = '新'
    )
    fmt.Println(c1, c2, c3)

上述的程序根本无法运行,因为第二行编译会报错,vscode 给到了非常具体的提醒:'新' (untyped rune constant 26032) overflows byte

接下来,咱们通过上面的代码来验证 字符 unicode和整型的等价关系:

    fmt.Printf("0x%x, %d\n", '', '') // 输入:0x81, 129
    fmt.Println(0x81 == '', '\u0081' == '', 129 == '') // 输入:true true true
    //\u0081 输入到屏幕上后不展现,所以换了大写字母 A 来输入
    fmt.Printf("%c\n", 65) // 输入:A

依据下面的代码输入的 3 个 true 能够晓得,字符和 unicode 和整形是等价,并且整型也能转回字符的表现形式。

go 中的字符串是 utf8 编码的

依据 golang 官网博客 https://blog.golang.org/strings 的原文:


Go source code is always UTF-8.
A string holds arbitrary bytes.
A string literal, absent byte-level escapes, always holds valid UTF-8 sequences.

翻译整顿过去其实也就是两点:

  1. go 中的代码总是用 utf8 编码,并且字符串可能存储任何字节。
  2. 没有通过字节级别的本义,那么字符串是一个规范的 utf8 序列。

有了后面的基础知识和字符串是一个规范的 utf8 序列这一论断后咱们接下来对字符串“”(如果无奈展现,记住该特殊字符的 unicode 是\u0081)手动编码。

Unicode 到 UTF- 8 的编码方对照表:

Unicode 编码(十六进制) UTF-8 字节流(二进制)
000000-00007F 0xxxxxxx
000080-0007FF 110xxxxx 10xxxxxx
000800-00FFFF 1110xxxx 10xxxxxx 10xxxxxx
010000-10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

字符‘’(如果无奈展现,记住该特殊字符的 unicode 是\u0081)的二进制示意为10000001,16 进制示意为0x81

依据 unicode 转 utf8 的对照表,0x7f < 0x81 < 0x7ff,所以此特殊字符需占两个字节,并且要套用的 utf8 模版是110xxxxx 10xxxxxx

咱们依照上面的步骤对 10000001 转为 utf8 的二进制序列:

第一步:依据 x 数量对特殊字符的高位补 0。x 的数量是 11,所以须要对特殊字符的高位补 3 个 0,此时特殊字符的二进制示意为:00010000001

第二步:x 有两个局部,且长度别离是 5 和 6,所以对 00010000001 由底位向高位别离截取 6 位和 5 位,失去 00000100010

第三步:将 00000100010由低位向高位填充至模版110xxxxx 10xxxxxx,可失去 utf8 的二进制序列为:11000010 10000001

咱们通过 go 对二进制转为整型:

fmt.Printf("%d, %d\n", 0b11000010, 0b10000001)
// 输入:194, 129

综上:当用字符转字节时输入的是字符自身的整型值,当用字符串转字节切片时,实际上是输入的是 utf8 的字节切片序列(go 中的字符串存储的就是 utf8 字节切片)。此时,咱们回顾一下最开始的问题,就会发现输入是完全符合预期的。

go 中的 rune

笔者在这里猜想提问者冀望的后果是“字符串转字节切片和字符转字节的后果保持一致”,这时 rune 就派上用场了,咱们看看应用 rune 的成果:

fmt.Println([]rune(""))
// 输入:[129]

由上可知用 rune 切片去转字符串时,它是间接将每个字符转为对应的 unicode。

咱们通过上面的代码模仿字符串转为[]rune 切片和[]rune 切片转为字符串的过程:

字符串转为 rune 切片:

    // 字符串间接转为[]rune 切片
    for _, v := range []rune("新世界杂货铺") {fmt.Printf("%x", v)
    }
    fmt.Println()
    bs := []byte("新世界杂货铺")
    for len(bs) > 0 {r, w := utf8.DecodeRune(bs)
        fmt.Printf("%x", r)
        bs = bs[w:]
    }
    fmt.Println()
    // 输入:
    // 65b0 4e16 754c 6742 8d27 94fa
    // 65b0 4e16 754c 6742 8d27 94fa

上述代码中 utf8.DecodeRune 的作用是通过传入的 utf8 字节序列转为一个 rune 即 unicode。

rune 切片转为字符串:

    // rune 切片转为字符串
    rs := []rune{0x65b0, 0x4e16, 0x754c, 0x6742, 0x8d27, 0x94fa}
    fmt.Println(string(rs))
    utf8bs := make([]byte, 0)
    for _, r := range rs {bs := make([]byte, 4)
        w := utf8.EncodeRune(bs, r)
        utf8bs = append(utf8bs, bs[:w]...)
    }
    fmt.Println(string(utf8bs))
    // 输入:
    // 新世界杂货铺
    // 新世界杂货铺

上述代码中 utf8.EncodeRune 的作用是将一个 rune 转为 utf8 字节序列。

综上:对于无奈确定字符串中仅有单字节的字符的状况,请应用rune,每一个 rune 类型代表一个 unicode 字符,并且它能够和字符串做无缝切换。

了解 go 中的字符串其实是字节切片

后面曾经提到了字符串可能存储任意字节数据,而且是一个规范的 utf8 格局的字节切片。那么本节将会通过代码来加深印象。

    fmt.Println([]byte("新世界杂货铺"))
    s := "新世界杂货铺"
    for i := 0; i < len(s); i++ {fmt.Print(s[i], " ")
    }
    fmt.Println()
    // 输入:// [230 150 176 228 184 150 231 149 140 230 157 130 232 180 167 233 147 186]
    // 230 150 176 228 184 150 231 149 140 230 157 130 232 180 167 233 147 186

由上述的代码可知,咱们通过游标按字节拜访字符串失去的后果和字符串转为字节切片是一样的,因而能够再次确认字符串和字节切片是等价的。

通常状况下咱们的字符串都是规范 utf8 格局的字节切片,但这并不是阐明字符串只能存储 utf8 格局的字节切片,go 中的字符串能够存储任意的字节数据


    bs := []byte{65, 73, 230, 150, 176, 255}
    fmt.Println(string(bs))         // 将随机的字节切片转为字符串
    fmt.Println([]byte(string(bs))) // 将字符串再次转回字节切片

    rs := []rune(string(bs)) // 将字符串转为字节 rune 切片
    fmt.Println(rs)          // 输入 rune 切片
    fmt.Println(string(rs))  // 将 rune 切片转为字符串

    for len(bs) > 0 {r, w := utf8.DecodeRune(bs)
        fmt.Printf("%d: 0x%x", r, r) // 输入 rune 的值和其对应的 16 进制
        bs = bs[w:]
    }
    fmt.Println()
    fmt.Println([]byte(string(rs))) // 将 rune 切片转为字符串后再次转为字节切片
    // 输入:// AI 新�
    // [65 73 230 150 176 255]
    // [65 73 26032 65533]
    // AI 新�
    // 65: 0x41 73: 0x49 26032: 0x65b0 65533: 0xfffd 
    // [65 73 230 150 176 239 191 189]

仔细阅读下面的代码和输入,前 5 行的输入应该是没有疑难的。然而第 6 行输入却和预期有出入。

后面提到了字符串能够存储任意的字节数据,那如果存储的字节数据不是规范的 utf8 字节切片就会呈现下面的问题。

咱们曾经晓得通过 utf8.DecodeRune 能够将字节切片转为 rune。那如果碰到不合乎 utf8 编码标准的字节切片时,utf8.DecodeRune 会返回一个容错的 unicode\uFFFD,这个 unicode 对应下面输入的 16 进制0xfffd

问题也就呈现在这个容错的 unicode\uFFFD上,因为字节切片不合乎 utf8 编码标准无奈失去正确的 unicode,既 \uFFFD 占据了本应该是正确的 unicode 所在的地位。这个时候再将曾经含有容错字符的 rune 切片转为字符串时,字符串存储的就是非法的 utf8 字节切片了,因而第六行输入的是含有 \uFFFD 的非法 utf8 字节切片,也就产生了和最初始的字节切片不统一的状况了。

⚠️:在平时的开发中要留神 rune 切片和 byte 切片的互相转换肯定要基于没有乱码的字符串(外部是合乎 utf8 编码规定的字节切片),否则容易呈现上述相似的谬误

字符串的多种示意形式

本节算是扩大了,在开发中还是尽量别用这种非凡的示意形式,尽管看起来很高级然而可读性太差。

上面间接看代码:

    bs := []byte([]byte("新"))
    for i := 0; i < len(bs); i++ {fmt.Printf("0x%x", bs[i])
    }
    fmt.Println()
    fmt.Println("\xe6\x96\xb0")
    fmt.Println("\xe6\x96\xb0 世界杂货铺" == "新世界杂货铺")
    fmt.Println('\u65b0' == '新')
    fmt.Println("\u65b0 世界杂货铺" == "新世界杂货铺")
    // 输入:
    // 0xe6 0x96 0xb0 
    // 新
    // true
    // true
    // true

目前笔者仅发现 unicode 和单字节的 16 进制能够间接用在字符串中,欢送读者提供更多的示意形式以供交换。

最初,祝大家读完此篇文章后可能有所播种。

正文完
 0