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

前言

前段时间发表了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-00007F0xxxxxxx
000080-0007FF110xxxxx 10xxxxxx
000800-00FFFF1110xxxx 10xxxxxx 10xxxxxx
010000-10FFFF11110xxx 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进制能够间接用在字符串中, 欢送读者提供更多的示意形式以供交换。

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