• golang中的字符串


    0.1、索引

    https://waterflow.link/articles/1666449874974

    1、字符串编码

    在go中rune是一个unicode编码点。

    我们都知道UTF-8将字符编码为1-4个字节,比如我们常用的汉字,UTF-8编码为3个字节。所以rune也是int32的别名。

    type rune = int32
    

    当我们打印一个英文字符hello的时候,我们可以得到s的长度为5,因为英文字母代表1个字节:

    package main
    
    import "fmt"
    
    func main() {
    	s := "hello"
    	fmt.Println(len(s)) // 5
    }
    

    但是当我们打印的时候,会打印3个字节。因为使用UTF-8,这个字符会被编码成3个字节:

    package main
    
    import "fmt"
    
    func main() {
    	s := "嗨"
    	fmt.Println(len(s)) // 3
    }
    

    所以,我们使用len内置函数输出的并不是字符数,而是字节数。

    下面看一个有趣的例子,我们都知道汉字符使用3个字节编码,分别是0xE5, 0x97, 0xA8。我们运行下面代码会得到汉字:

    package main
    
    import "fmt"
    
    func main() {
    	s := string([]byte{0xE5, 0x97, 0xA8})
    	fmt.Println(s) // 嗨
    }
    

    所以我们需要知道:

    • 字符集是一组字符,而编码描述了如何将字符集转换为二进制
    • 在 Go 中,字符串引用任意字节的不可变切片
    • Go 源码使用 UTF-8 编码。 因此,所有字符串文字都是 UTF-8 字符串。 但是因为字符串可以包含任意字节,如果它是从其他地方(不是源码)获得的,则不能保证它是基于 UTF-8 编码的
    • 使用 UTF-8,一个 Unicode 字符可以编码为 1 到 4 个字节
    • 在 Go 中对字符串使用 len 返回字节数,而不是字符数

    2、字符串遍历

    我们在开发中经常会用到对字符串进行遍历的场景。 也许我们想对字符串中的每个 rune 执行一个操作,或者实现一个自定义函数来搜索特定的子字符串。 在这两种情况下,我们都必须遍历字符串的不同字符。 但往往会得到让我们意想不到的结果。

    我们看下下面的例子,打印一个字符串中的不同字符和对应的位置:

    package main
    
    import "fmt"
    
    func main() {
    	s := "h嗨llo"
    	for i := range s {
    		fmt.Printf("字符位置 %d: %c\n", i, s[i])
    	}
    	fmt.Printf("len=%d\n", len(s))
    }
    
    go run 7.go
    字符位置 0: h
    字符位置 1: å
    字符位置 4: l
    字符位置 5: l
    字符位置 6: o
    len=7
    

    我们想要的效果是通过遍历字符串,打印出每个字符的索引。但是我们却得到了一个特殊的字符å,其实我们想要的是

    但是打印的字节数是符合我们的预期的,因为是一个中文占用了3个字节,所以len返回的是7。

    3、字符串中的字符数

    如果我们想要正确的获取字符串的字符数,可以使用go中的utf8包:

    package main
    
    import (
    	"fmt"
    	"unicode/utf8"
    )
    
    func main() {
    	s := "h嗨llo"
    
    	for i := range s {
    		fmt.Printf("字符位置 %d: %c\n", i, s[i])
    	}
    	fmt.Printf("len=%d\n", len(s))
    	fmt.Printf(" rune len=%d\n", utf8.RuneCountInString(s)) // 获取字符数
    }
    
    go run 7.go
    字符位置 0: h
    字符位置 1: å
    字符位置 4: l
    字符位置 5: l
    字符位置 6: o
    len=7
     rune len=5
    

    在这个例子中,可以看到,我们确实遍历了5次,也就是对应字符串的5个字符。但是我们获取到的索引其实是对应每个字符的起始位置。像下面这样
    http://image-1313007945.cos.ap-nanjing.myqcloud.com/image/1666449922.png

    那我们如何打印出正确的结果呢?我们稍微修改下代码:

    package main
    
    import (
    	"fmt"
    	"unicode/utf8"
    )
    
    func main() {
    	s := "h嗨llo"
    
    	for i, v := range s { // 此处改为获取v,可以获取到字符本身
    		fmt.Printf("字符位置 %d: %c\n", i, v)
    	}
    	fmt.Printf("len=%d\n", len(s))
    	fmt.Printf(" rune len=%d\n", utf8.RuneCountInString(s))
    }
    
    go run 7.go
    字符位置 0: h
    字符位置 1: 嗨
    字符位置 4: l
    字符位置 5: l
    字符位置 6: o
    len=7
     rune len=5
    

    另外一种方法就是把字符串转换成rune切片,这样也会正确打印结果:

    package main
    
    import (
    	"fmt"
    	"unicode/utf8"
    )
    
    func main() {
    	s := "h嗨llo"
    	b := []rune(s)
    
    	for i := range b {
    		fmt.Printf("字符位置 %d: %c\n", i, b[i])
    	}
    	fmt.Printf("len=%d\n", len(s))
    	fmt.Printf(" rune len=%d\n", utf8.RuneCountInString(s))
    }
    
    go run 7.go
    字符位置 0: h
    字符位置 1: 嗨
    字符位置 2: l
    字符位置 3: l
    字符位置 4: o
    len=7
     rune len=5
    

    下面是rune切片遍历的过程(中间省略了将字节转换为rune的过程,需要遍历字节,复杂度为O(n))
    http://image-1313007945.cos.ap-nanjing.myqcloud.com/image/1666449943.png

    4、字符串trim

    开发中我们经常会遇到去除字符串头部或者尾部字符的操作。比如我们现在有个字符串xohelloxo,现在我们想去除尾部的xo,可能我们会像下面这样写:

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func main() {
    	s := "xohelloxo"
    	s = strings.TrimRight(s, "xo")
    	fmt.Println(s)
    }
    
    go run 7.go
    xohell
    

    可以看到这不是我们期望的结果。我们可以看下TrimRight的工作原理:

    1. 从右侧取出第一个字符o,判断是否在xo中,在就移除
    2. 重复步骤1,知道不符合条件

    所以就可以解释通了。当然和它相似的TrimLeft和Trim也是一样的原理。

    如果我们只想删除最后xo可以使用TrimSuffix函数:

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func main() {
    	s := "xohelloxo"
    	s = strings.TrimSuffix(s, "xo")
    	fmt.Println(s)
    }
    
    go run 7.go
    xohello
    

    当然也有对应的从前面删除的函数TrimPrefix。

    5、字符串连接

    开发中我们经常会用到连接字符串的操作,在go中我们一般有2种方式。

    我们先看下+号连接的方式:

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func implode(values []string, operate string) string {
    	s := ""
    	for _, value := range values {
    		s += operate
    		s += value
    	}
    	s = strings.TrimPrefix(s, operate)
    	return s
    }
    
    func main() {
    	a := []string{"hello", "world"}
    	s := implode(a, " ")
    	fmt.Println(s)
    }
    
    go run 7.go
     hello world
    

    这种方式的缺点就是,由于字符串的不变性,每次+号赋值的时候s不会被更新,而是重新分配内存,所以这种方式对性能有很大影响。

    还有一种方式就是使用strings.Builder:

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func implode(values []string, operate string) string {
    	sb := strings.Builder{}
    	for _, value := range values {
    		_, _ = sb.WriteString(operate)
    		_, _ = sb.WriteString(value)
    	}
    	s := strings.TrimPrefix(sb.String(), operate)
    	return s
    }
    
    func main() {
    	a := []string{"hello", "world"}
    	s := implode(a, " ")
    	fmt.Println(s)
    }
    
    go run 7.go
    hello world
    

    首先,我们创建了一个 strings.Builder 结构。 在每次遍历中,我们通过调用 WriteString 方法构造结果字符串,该方法将 value 的内容附加到其内部缓冲区,从而最大限度地减少内存复制。

    WriteString 的第二个参数返回的是error,但是error的值会一直为nil。 之所以有第二个error参数是因为我 strings.Builder 实现了 io.StringWriter 接口,它包含一个方法:WriteString(s string) (n int, err error)。

    我们看下WriteString的内部是什么样的:

    func (b *Builder) WriteString(s string) (int, error) {
    	b.copyCheck()
    	b.buf = append(b.buf, s...)
    	return len(s), nil
    }
    

    我们可以看到b.buf是一个字节切片,而里面的实现是使用了append方法。我们知道如果切片很大,使用append会让底层数组不断扩容,影响代码执行效率。

    我们知道解决这个问题的方法是,如果事先知道切片的大小,我们可以在初始化的时候就分配好切片的容量。

    所以上面的字符串连接还有一种优化方案:

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func implode(values []string, operate string) string {
    	total := 0
    	for i := 0; i < len(values); i++ {
    		total += len(values[i])
    	}
    	total += len(operate) * len(values)
    	sb := strings.Builder{}
    	sb.Grow(total) // 这里会重新分配b.buf的长度和容量
    	for _, value := range values {
    		_, _ = sb.WriteString(operate)
    		_, _ = sb.WriteString(value)
    	}
    	s := strings.TrimPrefix(sb.String(), operate)
    	return s
    }
    
    func main() {
    	a := []string{"hello", "world"}
    	s := implode(a, " ")
    	fmt.Println(s)
    }
    
    go run 7.go
    hello world
    

    6、字节切片转字符串

    需要明确的是,字节切片转换成字符串,需要复制一份副本出来。可以通过下面的代码做验证:

    b := []byte{'a', 'b', 'c'}
    s := string(b)
    b[1] = 'x'
    fmt.Println(s)
    

    事实上,上面将会输出abc而不是axc。所以字节切片到字符串的转换是有开销的。

    但是我们开发中经常用到的包iio.Read之类的,入参或者返回经常是字节切片类型。而我们调用这些函数时经常是以字符串的形式,导致我们不得不做一些字节切片刀字符串的转换。

    所以结论是,当我们需要使用字符串作为入参或者返回时,我们首先要考虑的是能用字节切片的就用字节切片。

  • 相关阅读:
    Android 10.0 Launcher3 app图标长按去掉应用信息按钮
    Redis数据类型之Stream系列一
    【APUE】文件系统 — 类 du 命令功能实现
    No boot device found快速解决方案
    MySQL数据库面试题总结(2022最新版)
    ubuntu 16.04.5 安装 vivado 2019.1 完整编译AD9361的环境
    竞赛trick-AWP对抗训练的即插即用实现
    (学习笔记)SpringCloud微服务快速入门实战课程【2020版】
    金融数字化转型这场硬仗,如何才能做到“有底”和“有数”?
    redis 常见的异常
  • 原文地址:https://www.cnblogs.com/liuyuede123/p/16817547.html