6

Go tips-笔记: string 36-41 mistakes

 1 year ago
source link: https://weedge.github.io/post/notions/go-tips/go-tips-05-strings/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Go tips-笔记: string 36-41 mistakes

2023-02-14

在 Go 中,string是一种不可变的数据结构,包含以下内容:

  • 指向不可变字节序列的指针,指向一个byte类型的数组
  • 此序列中的总字节数

string在Go中的内部结构是reflect.StringHeader位于reflect/value.go

// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
	Data uintptr
	Len  int
}
//uintptr  an unsigned integer large enough to store the uninterpreted bits of a pointer value

已通过unsafe.Poniter显示的将 string转换成reflect.StringHeader 结构,进而可以获取结构中的Data指正,然后通过unsafe.Poniter显示转成数组,比如[5]byte, 数组大小不一定等于原始string长度,即使越界访问,因为是只读,如果写的话会出现panic,所以可以越界这样获取; 代码如下:

func ViewStringStruct() {
	s := "hello"
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	fmt.Printf("0x%x\\n", sh.Data)
	fmt.Println(sh.Len) // 5
	ptr := unsafe.Pointer(sh.Data)
	//arrPtr := (*[]byte)(ptr) // panic
	//arrPtr := (*[3]byte)(ptr)
	//arrPtr := (*[100]byte)(ptr) // access violation, string just only read, so is ok
	arrPtr := (*[5]byte)(ptr)
	fmt.Println(*arrPtr) // [104 101 108 108 111]
	fmt.Printf("%s\\n", *arrPtr)
	//arrPtr[3] = 100 // panic, string cann't change
}

[]byte 和 string的相互转换,在读取字符串的场景下经常使用到:

func String(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}
func Str2Bytes(s string) []byte {
	x := (*[2]uintptr)(unsafe.Pointer(&s))
	h := [3]uintptr{x[0], x[1], x[1]}
	return *(*[]byte)(unsafe.Pointer(&h))
}

36.不理解rune的概念 (重要)

字符集和编码之间的区别:

  • 字符集(charset)是一组字符。例如,Unicode 字符集包含 2^21 个字符。
  • 编码(encoding)是字符列表的二进制转换。例如,UTF-8 是一种编码标准,能够将所有 Unicode 字符编码为可变字节数(从 1 到 4 字节)。

UTF-8 将字符编码为 1 到 4 个字节,因此最多为 32 位, rune 是 int32的别名;需要清楚以下概念:

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

如果想准确获取到有符文(rune)字符串的长度,可以使用utf8.RuneCountInString 函数

37.不准确的字符串迭代

for i := range s {
    fmt.Printf("position %d: %c\\n", i, s[i])
}

以上代码,没有遍历每个rune,而是迭代rune的每个起始索引;

如果想遍历字符串的符文(rune),可以使用range直接在字符串上循环,必须记住,**索引对应的不是符文索引,而是符文字节序列的起始索引;**因为一个符文可以由多个字节组成,如果要访问符文本身,应该使用 的值变量range,而不是字符串中的索引;

如果想获取第i个字符串的符文(rune),在大多数情况下应该将字符串转换为一段 runes。

38.滥用 trim 函数

Go中的strings包,开发者可能经常混淆使用TrimRight 和 TrimSuffix, 或者 TrimLeft 和 TrimPrefix

TrimRight向后遍历每个符文;如果符文是提供的集合的一部分,则该函数将其删除。如果不是,该函数将停止迭代并返回剩余的字符串。TrimLeft 向前遍历同理。 Trim 函数 两边遍历也是一样。

如果想匹配整体的字符串进行删除的话, 应该使用 TrimSuffix 和 TrimPrefix,如果两边移除分别调用这两个方法。

39.优化不足的字符串连接

如果使用 s += str 的方式连接字符串,会有性能问题,因为字符的数据是不可变的,每次字符串 + 连接操作都会重新分配一次内存空间(allocator);

应该使用 strings.Builder 结构来拼接字符串, Builder结构中有一个byte切片 buf []byte用于数据拼接,WriteString 内部使用append来操作,前面提到append操作会出发自动扩容,这样不用每次拼接的时候分配一次新的内存空间,提高了性能; 如果可以获取到拼接字符串的长度,那就可以直接通过 Grow函数来一次初始化拼接buf byte切片内存空间,这样性能可以得到进一步的提升(与 21. 切片初始化效率低下 分析一样)。类似WriteString方法对应string 类型,还有一下三总方法:

  • 字节切片使用Write
  • 单字节使用WriteByte
  • 单个符文使用WriteRune

当然如果拼接的短字符串就那么几个,则没有必要使用strings.Builder 结构来拼接字符串,性能提升可以忽略,但是代码量可读性方面就降低了,可读性不如使用运算符+=fmt.Sprintf

40.无用的字符串转换

比如 string 转[]byte, []byte 转string, 如果是直接 []byte(string) 或者 string([]byte),转换都会有额外的内存分配,而且转换后的string是不可变的; 所以在进行字符串操作的时候,尽量都使用[]byte类型,避免转换string带来的额外操作,strings包也有替代品包bytes,大多数 I/O Buffer 都是操作 []byte,字符串的拼接Builder结构也是对[]byte的操作。

41.子字符串和内存泄漏

在 Go 中使用子字符串操作时,字符串的结构可知:

  1. 提供的间隔是基于字节数,而不是符文数。
  2. 子字符串操作可能会导致内存泄漏,因为生成的子字符串将与初始字符串共享相同的底层数组。

防止这种情况发生的解决方案是手动执行字符串复制,或者从 Go 1.18 开始引入strings.Clone

  • 了解符文对应于 Unicode 码位(code point)的概念,它可以由多个字节组成,应该是 Go 开发人员准确处理字符串的核心知识的一部分。
  • 使用range运算符迭代字符串会迭代符文,其索引对应于符文字节序列的起始索引。要访问特定的符文索引(例如第三个符文),请将字符串转换为[]rune.
  • strings.TrimRight/strings.TrimLeft删除给定集合中包含的所有向后/向前符文返回,而strings.TrimSuffix/strings.TrimPrefix删除提供后缀/前缀的字符串返回
  • 应该连接字符串列表strings.Builder以防止在每次迭代期间分配新字符串。
  • 记住bytes包提供与包相同的操作strings可以帮助避免额外的字节/字符串转换。
  • 使用副本而不是子字符串可以防止内存泄漏,因为子字符串操作返回的字符串将由相同的字节数组支持。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK