• Go 中的字符串相关操作


    string 与 UTF-8

    Go 中使用 UTF-8 对字符进行编码

    首先,我们需要对字符编码有一定相关的了解,并明白为什么 Go 选中 UTF-8 作为字符编码方式。

    ASCII 和 Unicode

    在计算机行业在美国兴起时,人们使用「ASCII」对字符集进行处理:ASCII 使用 7 位 128 个字符(大小写英文字母、数字、标点以及设备控制符)。这对当时的行业来说已经足够使用了,但随着计算机行业的兴起,世界上使用其他语言的人无法在计算机上使用自己的文书体系。

    为了解决这个问题,人们开始使用「Unicode」,如今已经定义到了第 8 版,定义了超过一百种语言文字的 12 万个字符的码点。Unicode 需要 32 位比特,也就是 4 个字节,计算机中的int32便很适合保存这种数据类型,Go 中便是这样认为的,因此为int32设置了别名rune

    但如果我们将所有的字符都按照「Unicode」进行编码,这种编码方式称为 UTF-32 或者 UCS-4,每个 Unicode 码点都需要占 4 个字节;但,大多数计算机的可读文本为 ASCII,只需要 1 个字节便可以满足编码要求,而广泛使用的字符也只需要 16 位字符即可,因此这种方式导致了不必要的存储空间消耗

    UTF-8

    UTF-8 以字节为单位对 Unicode 码点进行变长编码,是现行的一种 Unicode 标准。它每个符号用 1~4 个字节表示,例如 ASCII 的编码仅需 1 个字节,其他常用的文字编码是 2 或者 3 个字节。

    在 UTF-8 中,「首字节的最高位」指明后面还有多少字节:

    • 若最高位为 0,则表示它是 7 位的 ASCII 码,那么它只需要使用一个字节;

    • 若最高几位是 110,那么它占用了两个字节,则文字符号占用 2 个字节进行编码,第二个字节以 10 开始,更长的编码也是以此类推。

    因此,对于需要不同空间的字符,UTF-8 的编码方式如下:

    0xxxxxxx                            文字符号 0 ~ 127         ASCII
    110xxxxx 10xxxxxx                   128 ~ 2047              少于 128 个未使用的值
    1110xxxx 110xxxxx 10xxxxxx          2048 ~ 65535            少于 2048 个未使用的值
    11110xxx 1110xxxx 110xxxxx 10xxxxxx 65536 ~ 0x10ffff        其他未使用的值
    

    显然,对于 UTF-8,我们不能按下标直接访问第 n 个字符,以此为代价,我们得到了许多方便的特性:

    • UTF-8 编码紧凑,兼容 ASCII,且自同步:最多追溯 3 字节,就能定位一个字符的起始位置;

    • UTF-8是前缀编码,故能够从左往右解码而不产生歧义,也无需超前预读;

    • UTF-8 的编码顺序与字典序一致(Unicode 的码点顺序和字典序一致);

    • UTF-8编码本身不会嵌入 NUL 字节(0 值),因此我们可以使用 NUL 标记字符串结尾。

    Go 中的 UTF-8

    Go 的源文件总是以 UTF-8 进行编码,同时,其操作的文本字符串也是优先使用 UTF-8。

    如何表示 UTF-8 字符

    Go 中,string 字面量的转义让我们可以使用码点来指明 Unicode 字符。有两种形式:uhhhh表示 16 位码点,uhhhhhhhh表示 32 位码点(h 表示一个十六进制的数字),32 位的码点基本用不到。这两种形式都能用 UTF-8 表示给定的码点,因此,下面三个字符串表示的是长度为 6 的相同串:

    "世界"
    "xe4xb8x96xe7x95x8c"
    "u4e16u754c"
    "U00004e16U0000754c"
    

    「码点值小于 256 的文字符号」(也就是 ASCII 码)可以写成单个十六进制转义的形式,如将'A'写成'x41';更高的码点必须使用u或者U进行转义,这也导致前面的xe4xb8x96不是合法的文字符号。

    常用操作

    由于 UTF-8 的优良特性,许多字符串操作都无需解码,下面是strings包中一些源码。

    可以直接判断某个字符串是否为另一个前缀

    func HasPrefix(s, prefix string) bool {
        return len(s) >= len(prefix) && s[:len(prefix)] == prefix
    }
    

    或者判断是否为另一个字符串的后缀

    func HasSuffix(s, suffix string) bool {
        return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
    }
    

    或者是否为另一个字符串的字串(实际上的实现使用了散列让搜索更高效):

    func Contains(s, substr string) bool {
        for i := 0; i < len(s)-len(substr); i++ {
            if HasPrefix(s[i:], substr) {
                return true
            }
        }
        return false
    }
    

    处理 Unicode 字符

    Go 中的unicode包拥有对单个文字符号的函数(例如区分字母和数字,转换大小写),unicode/utf8包提供了按 UTF-8 编码和解码文字符号的函数。

    在实际处理 Unicode 字符时,我们需要注意它实际上的字节数;看下面的例子:

    import "unicode/utf8"
    
    s := "世界"
    fmt.Println(len(s)) // 输出:6
    fmt.Println(utf8.RuneCountInStrings(s)) // 输出:2
    

    可以看到,我们需要按做 UTF-8 解读,才能得到符合常规认知的字符长度。

    如果我们需要逐个处理这些字符,就需要使用 UTF-8 的解码器,例如unicode/utf8中的:

    s := "世界, hello"
    for i := 0; i < len(s) {
        r, size := utf8.DecodeRuneInString(s[i:])
        fmt.Printf("%d	%c
    ", i, r)
        i += size
    }
    

    每次调用DecodeRuneInString的调用都会返回 r(文字符号本身)和一个值 size(表示 r 按照 UTF-8 所占的字节数)。我们用 size 来更新 slice 的下标,这样就能够正确的打印字符:

    0	世
    3	界
    6	,
    7
    8	h
    9	e
    10	l
    11	l
    12	o
    

    幸好 Go 中的「range 循环」也适用于字符串,对 UTF-8 进行隐式解码,所以下述语句也能达到同样的效果:

    for i, r := range s {
        fmt.Printf("%d	%q	%d
    ", i, r, r)
    }
    

    这里的r可以用%q或者%d来表示,前者会打印字符(如),后者打印对应的 unicode(如19990)。

    也因为 range 循环有对 UTF-8 的隐式编码,因此我们可以直接使用它来统计字符串中的文字符号数:

    n := 0
    for range s {
        n++
    }
    

    Go 中的相关标准库

    Go 语言中 4 个标准包对字符串操作很重要:bytes、strings、strconv 与 unicode

    • 「strings」:提供用于搜索、替换、比较、修整、切分与连接字符串的函数

    • 「bytes」:用于操作字节slice([]byte 类型的某些属性和字符串相同)。例如可以使用bytes.Buffer高效地按增量方式构建字符串。

    • 「strconv」:主要用于 string 与布尔值、整数、浮点数之间的相互转换,或者是用于为字符串添加/去除引号。

    • 「unicode」:主要用于判别文字符号特性;例如IsDigitIsLetterIsUpperIsLower。这些函数以单个字符作为参数,并返回布尔值。

    下面我们用一些例子说明这些包的用法。

    移除文件的系统路径和后缀

    下例中,basename 函数模仿 UNIX shell 中的同名实用程序,移除文件的系统路径和可能存在的后缀:

    1.首先我们看看不依赖任何库的初版 basename:

    /* 
      basename 移除路径部分以及 .后缀
      e.g., a=>a, a.go=>a, a/b/c.go=>c
    */
    func basename(s string) string {
        for i := len(s) - 1; i >= 0; i-- {
            if s[i] == '/' {
                s = s[i + 1:]
                break
            }
        }
        for i := len(s) - 1; i >= 0; i-- {
            if s[i] == '.' {
                s = s[:i]
                break
            }
        }
        return s
    }
    

    2.接下来我们使用库函数string.LastIndex来简化代码:

    func basename(s string) string {
        slash := strings.LastIndex(s, "/") // 如果没找到"",slash 的取值为 -1
        s = s[slash+1:]
        if dot := string.LastIndex(s, "."); dot >= 0 {
            s = s[:dot]
        }
        return s
    }
    

    规范化整数字符串

    这个例子中,我们对子字符串进行操作:接受一个表示整数的字符串,如12345,从右侧开始每隔三个数字就插入一个逗号,形如12,345

    func comma(s string) string {
        n := len(3)
        if n <= 3 {
            return s
        }
        return comma(s[:n-3]) + "," + s[n-3:]
    }
    

    在 Go 语言中,字符串可以和字节 slice 相互转换:

    s := "abc"
    b := []byte(s)
    s2 := string(b)
    

    正常情况下,这种 string 和 slice 的相互转换都会进行拷贝,这样可以保证即使 b 的字节在转换后发生改变,s 也不会一起变化。

    但如果我们不需要这种特性,就会产生不必要的内存消耗,为了避免这种情况,bytesstrings包中都包含了相应的使用函数,它们两两对应。例如,string包中有下面 6 个函数:

    func Contains(s, substr string) bool
    func Count(s, sep string) bool
    func Fields(s string) []string
    func HasPrefix(s, prefix string) bool
    func Index(s, sep string) int
    func Join(a []string, sep string) string
    

    bytes包中的对应函数为:

    func Contains(b, subslice []byte) bool
    func Count(b, sep []byte) bool
    func Fields(b []byte) [][]byte
    func HasPrefix(b, prefix []byte) bool
    func Index(b, sep []byte) int
    func Join(a [][]byte, sep []byte) []byte
    

    唯一不同的是,操作对象由字符串变为了 slice

    bytes包为高效处理字节 slice 提供了「Buffer」类型。它起始为空,大小随着各种类型数据的写入而增长,如 string、byte 和 []byte。如下例,bytes.Buffer变量无需初始化,因为零值本来就有效:

    // intsToString 与 fmt.Sprintf(values) 类似,但插入了逗号
    func intsToString(values []int) string {
        var buf bytes.Buffer
        buf.WriteByte('[')
        for i, v := range values {
            if i > 0 {
                buf.WriteString(", ")
            }
            fmt.Fprintf(&buf, "%d", v)
        }
        buf.WriteByte(']')
    }
    
    func main() {
        fmt.Println(intsToString([]int{1, 2, 3})) // 输出: [1, 2, 3]
    }
    

    如果要在byte.Buffer变量后添加任意文字符号的 UTF-8 编码,最好使用WriteRune方法,而追加 ASCII 字符,则使用WriteByte即可。

    字符串和数字的相互转换

    通常,要将整数转换成字符串,一种选择是使用fmt.Sprintf,另一种做法是用函数strconv.Itoa

    x := 123
    y := fmt.Sprintf("%d", x)
    
    fmt.Println(y, strconv(x)) // 输出: 123 123
    

    FormatIntFormatUnit可以按不同的进位制格式化数字:

    fmt.Println(strconv.FormatInt(int64(x), 2)) // 输出 x 的二进制表示: 1111011
    

    golang字符串比较的三种常见方法

    // 1. 自建方法“==”,区分大小写,最简单的方法
    fmt.Println("go"=="go") // true
    fmt.Println("GO"=="go") // false
    
    // 2. Compare函数,区分大小写,比自建方法“==”的速度要快,下面是注释 
    // Compare is included only for symmetry with package bytes. 
    // It is usually clearer and always faster to use the built-in 
    // string comparison operators ==, <, >, and so on. 
    // func Compare(a, b string) int
    fmt.Println(strings.Compare("GO","go")) // -1 ,也就是 "GO" < "go" (因为是字典序)
    fmt.Println(strings.Compare("go","go")) // 0
    
    // 3. 比较UTF-8编码在小写的条件下是否相等,不区分大小写,下面是注释 
    // EqualFold reports whether s and t, interpreted as UTF-8 strings, 
    // are equal under Unicode case-folding. 
    // func EqualFold(s, t string) bool
    fmt.Println(strings.EqualFold("GO","go")) // true,因为不区分大小写
    

    输出:

    true
    false
    -1
    0
    true
    
  • 相关阅读:
    Jmeter之Constant Timer与constant throughput timer的区别(转)
    JMeter Exception: java.net.BindException: Address already in use: connect(转)
    jmeter的jtl日志转html报告常见报错笔记
    jmeter 启动jmeter-server.bat远程调用报错: java.io.FileNotFoundException: rmi_keystore.jks (系统找不到指定的文件。)
    jmeter5.0生成html报告 快速入门
    图片转字符画 【学习ing】
    python生成个性二维码学习笔记
    Processing 3!
    Python Selenium定位元素常用解决办法
    js 获取元素坐标 和鼠标点击坐标
  • 原文地址:https://www.cnblogs.com/Bylight/p/12090480.html
Copyright © 2020-2023  润新知