返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

5.1 字符串

发布于 2024-10-12 19:15:47 字数 7760 浏览 0 评论 0 收藏 0

字符串是 不可变 字节序列,其本身是一个复合结构。

 +-----------+          +---+---+---+---+---+
 |  pointer -|--------> | h | e | l | l | o |
 +-----------+          +---+---+---+---+---+
 |  len = 5  |          
 +-----------+          [...]byte, UTF-8
 
    header
 
// runtime/string.go

type stringStruct struct {
	str unsafe.Pointer
	len int
}

type stringStructDWARF struct {
	str *byte
	len int
}
  • 编码 UTF-8 ,无 NULL 结尾,默认值 ""
  • 使用 `raw string` 定义原始字符串。
  • 支持 !===<<=>=>++=
  • 索引访问字节数组(非字符),不能获取元素地址。
  • 切片返回子串,依旧指向原数组。
  • 内置函数 len 返回字节数组长度。
func main() {
	s := "雨痕\x61\142\u0041"

	bs := []byte(s)
    rs := []rune(s)     // rune/int32: unicode code point

    fmt.Printf("% X, %d\n", s,  len(s))
	fmt.Printf("% X, %d\n", bs, utf8.RuneCount(bs))
	fmt.Printf("%U, %d\n",  rs, utf8.RuneCountInString(s))
}

// E9 9B A8 E7 97 95 61 62 41, 9
// E9 9B A8 E7 97 95 61 62 41, 5
// [U+96E8 U+75D5 U+0061 U+0062 U+0041], 5
func main() {
	s := "雨痕 abc"

	fmt.Printf("%X\n", s[1])  // 9B
	
	// println(&s[1]) 
	// invalid operation: cannot take address of s[1]
}
func main() {
	var s string
	println(s == "")  // true

	// println(s == nil) 
	// invalid operation: mismatched types string and untyped nil
}

原始字符串(raw string)内的转义、换行、前置空格、注释等,都视作内容,不与处理。

func main() {
	s := `line\r\n,
  line 2`

	println(s)   // raw string
}

/*
line\r\n,
  line 2
*/

以加号连接字面量时,注意操作符位置。

func main() {
	s := "ab" +           // 跨行时,加法操作符必须在上行结尾。
		 "cd"

	println(s == "abcd")  // true
	println(s > "abc")    // true
}

子串内部指针依旧指向原字节数组。

func main() {
	s := "hello, world!"
	s2 := s[:4]

	p1 := (*reflect.StringHeader)(unsafe.Pointer(&s))
	p2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))

	fmt.Printf("%#v, %#v\n", p1, p2)
}

// &StringHeader{ Data:0x497208, Len:13 }
// &StringHeader{ Data:0x497208, Len:4 }

遍历,分 byterune 两种方式。

func main() {
	s := "雨痕"
    
    // byte
	for i := 0; i < len(s); i++ {         
		fmt.Printf("%d: %X\n", i, s[i])
	}
    
    // rune
	for i, c := range s {
		fmt.Printf("%d: %U\n", i, c)
	}
}

/*
0: E9
1: 9B
2: A8
3: E7
4: 97
5: 95

0: U+96E8
3: U+75D5
*/

可作 appendcopy 参数。

func main() {
	s := "de"

    bs := make([]byte, 0)
	bs = append(bs, "abc"...)
	bs = append(bs, s...)

	buf := make([]byte, 5)
	copy(buf, "abc")
	copy(buf[3:], s)

	fmt.Printf("%s\n", bs)   // abcde
	fmt.Printf("%s\n", buf)  // abcde
}

标准库相关:

  • bytes :字节切片。
  • fmt :格式化。
  • strconv :转换。
  • strings :函数。
  • text :文本(模版)。
  • unicode :码点。

转换

可在 runebytestring 间转换。

单引号字符字面量是 rune 类型,代表 Unicode 字符。

func main() {
	var r rune = '我'

	var s  string = string(r)
	var b  byte   = byte(r)
	var s2 string = string(b)
	var r2 rune   = rune(b)
    
	fmt.Printf("%c, %U\n", r, r)
	fmt.Printf("%s, %X, %X, %X\n", s, b, s2, r2)
}

// 我, U+6211
// 我, 11, 11, 11

要修改字符串,须转换为可变类型( []rune[]byte ),待完成后再转换回来。

但不管如何转换,都需重新分配内存,并复制数据。

字面量分配于 RODATA,其内存不能修改。

func main() {
	s := strings.Repeat("a", 1<<10)

    // 分配内存、复制。
	bs := []byte(s)
	bs[1] = 'B'

    // 分配内存、复制。
	s2 := string(bs)

	hs  := (*reflect.StringHeader)(unsafe.Pointer(&s))
	hbs := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	hs2 := (*reflect.StringHeader)(unsafe.Pointer(&s2))
	fmt.Printf("%#v\n%#v\n%#v\n", hs, hbs, hs2)
}

// &StringHeader{ Data:0xc0000a8000, Len:1024 }
// &SliceHeader { Data:0xc0000a8400, Len:1024, Cap:1024 }
// &StringHeader{ Data:0xc0000a8800, Len:1024 }

// 0x400 = 1024

验证编码是否正确。

func main() {
	s := "雨痕"
	s2 := string(s[0:2] + s[4:])  // 非法拼接。
    
    fmt.Printf("% X, % X\n", s, s2)
    fmt.Println(utf8.ValidString(s2))
}

// E9 9B A8 E7 97 95, E9 9B 97 95
// false

5.1.1 性能

使用 “不安全” 方法进行转换,改善性能。

  • 不动底层数组,直接构建 stringslice 头。
  • 如果修改,注意内存安全。
  • slice { data, len, cap } -> string { data, len }
  • slice { string.data, string.len, string.len }
package main

import (
	"testing"
	"strings"
	"unsafe"
)

var S = strings.Repeat("a", 100)

func normalConv() bool {
	b := []byte(S)
	s2 := string(b)
	return s2 == S
}

func unsafeConv() bool {
    
    // []byte(s)
    b := unsafe.Slice(unsafe.StringData(S), len(S))

    // string(b)
	s2 := unsafe.String(unsafe.SliceData(b), len(b))

	return s2 == S
}

// ----------------------------------

func BenchmarkNormal(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if !normalConv() { b.Fatal() }
	}
}

func BenchmarkUnsafe(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if !unsafeConv() { b.Fatal() }
	}
}
$ go test -bench . -benchmem -v

cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz

BenchmarkNormal-2    7285071   148 ns/op   224 B/op   2 allocs/op
BenchmarkUnsafe-2  274104459     2 ns/op     0 B/op   0 allocs/op

PASS

普通转换 normalConv 调用 runtime.stringtoslicebyteruntime.slicebytetostring

引发 mallocgcmemmove 等操作。

动态构建字符串也容易造成性能问题。

加法操作符拼接字符串,每次都需重新分配内存和复制数据。

改进方法是预分配内存,然后一次性返回。

package main

import (
	"testing"
	"bytes"
	"strings"
)

const N = 1000
const C = "a"
var S = strings.Repeat(C, N)

func concat() bool {
	var s2 string
	for i := 0; i < N; i++ {
		s2 += C
	}
	return s2 == S
}

func join() bool {
	b := make([]string, N)
	for i := 0; i < N; i++ {
		b[i] = C
	}

	return strings.Join(b, "") == S
}

func buffer() bool {
	var b bytes.Buffer
	b.Grow(N)
    
	for i := 0; i < N; i++ {
		b.WriteString(C)
	}
    
	return b.String() == S
}

// ----------------------------------

func BenchmarkConcat(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if !concat() { b.Fatal() }
	}
}

func BenchmarkJoin(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if !join() { b.Fatal() }
	}
}

func BenchmarkBuffer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if !buffer() { b.Fatal() }
	}
}
$ go test -bench . -benchmem -v

BenchmarkConcat-2      2524   419118 ns/op   530275 B/op   999 allocs/op
BenchmarkJoin-2      101426    11733 ns/op     1024 B/op     1 allocs/op
BenchmarkBuffer-2    148773     7334 ns/op     2048 B/op     2 allocs/op

PASS

查看 strings.Join 实现,和上面的 buffer 类似。

// strings/strings.go

func Join(elems []string, sep string) string {
	switch len(elems) {
	case 0:
		return ""
	case 1:
		return elems[0]
	}
    
    // 计算总长度。
	n := len(sep) * (len(elems) - 1)
	for i := 0; i < len(elems); i++ {
		n += len(elems[i])
	}

    // 预分配,循环写入。
	var b Builder
	b.Grow(n)
	b.WriteString(elems[0])
	for _, s := range elems[1:] {
		b.WriteString(sep)
		b.WriteString(s)
	}
	return b.String()
}

编译器对字面量 + 号拼接会优化。

如此之外,还可以用 fmt.Sprintftext/template 等方式进行。

字符串某些看似简单的操作,都可能引发堆内存分配和复制。

事实上,编译器和运行时也会采取非常规手段进行优化。

unsafe 的莫名抵触,大可不必!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文