Tips of Go - string

215 阅读3分钟

string 结构

Go 的字符串,本质结构是reflect.StringHeader

该结构位于源码 go1.16:reflect/value.go:1983

type StringHeader struct {
	Data uintptr
	Len  int
}

此结构是运行时字符串的表示,其中包含指向字节数组的指针Data和数组的大小Len,Data需要保留一个单独的、正确类型的指向底层数据的指针,防止垃圾回收。

虽然相同的字符串,指针可以指向不同的地址,但是他们的reflect.StringHeader,其内部Data指针实际上都指向相同的字节数组。

用下面的代码来说明:

package main

import (
	"fmt"
	"runtime/debug"
)

func main() {
	a := "a"
	b := "a"
	// 两个相同字符串地址并不相同
	fmt.Printf("a: %p, b %p\n", &a, &b)
	// 两个相同字符串Data地址相同
	printStack(a)
	printStack(b)
}

func printStack(s string) {
	println(string(debug.Stack()))
}


➜  string go run -gcflags="-N -l" main.go 
a: 0xc000096220, b 0xc000096230
goroutine 1 [running]:
runtime/debug.Stack(0x0, 0x0, 0xc000092ea8)
        /usr/local/go/src/runtime/debug/stack.go:24 +0x9f
main.printStack(0x10c9655, 0x1)
        /Users/mygo/ch/gist/g21/string/main.go:19 +0x26
main.main()
        /Users/mygo/ch/gist/g21/string/main.go:14 +0x196

goroutine 1 [running]:
runtime/debug.Stack(0xc0000bc000, 0x113, 0x113)
        /usr/go/src/runtime/debug/stack.go:24 +0x9f
main.printStack(0x10c9655, 0x1)
        /Users/mygo/ch/gist/g21/string/main.go:19 +0x26
main.main()
        /Users/mygo/ch/gist/g21/string/main.go:15 +0x1bd

string interning

String Interning是一种在内存中存储每个唯一字符串的一个副本的技术。 它可以显着降低存储许多重复字符串的应用程序的内存用法。

需要注意的是 Go 的 string intern 仅仅针对的是编译期可以确定的字符串常量,如果是运行期间产生的字符串则不能被内部化。

// 可以被 intern
s1 := "7"

// 不能被 intern
s2 := strconv.Itoa(7)

因为string的指针指向的内容是不可以更改的,所以每更改一次字符串,就得重新给StringHeader.Data分配一次内存,之前分配空间需要由GC判断回收,这是导致大量string操作低效的根本原因。

我们可以试着来绕过其限制,来完成一个可以内部化所有字符串的实现。首先我们需要一个 pool,把所有的字符串都放到这个 pool 里,只要字符串在这个 pool 里只有一份(例如 Map 就是一个非常好的选择),就可以认为已经被 intern 了。参考:String interning in Go

package main

import (
   "fmt"
   "reflect"
   "strconv"
   "unsafe"
)

// stringptr returns a pointer to the string data.
func stringptr(s string) uintptr {
   return (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
}

type stringInterner map[string]string

func (si stringInterner) Intern(s string) string {
   if interned, ok := si[s]; ok {
      return interned
   }
   si[s] = s
   return s
}

func main() {
   si := stringInterner{}
   str1 := "7"
   str2 := strconv.Itoa(7)
   fmt.Println(stringptr(str1) == stringptr(str2)) // false
   s1 := si.Intern("12")
   s2 := si.Intern(strconv.Itoa(12))
   fmt.Println(stringptr(s1) == stringptr(s2)) // true
}

string常见用法

字符串常量是不可修改的,字符串变量是可以修改的。

func main() {
	const c = "c"
	a := "a"
	c = a // Cannot assign to c
	a = c
}

比较大小

可以有=!= 操作

实际上 Go 对比两个字符串是否相等,首先会对比其长度,长度不同自然是不同的串,时间复杂度为 O(1);如果长度相同,再对比其底层字节数组地址,地址相同肯定是相同的串,时间复杂度仍然可以认为是 O(1);如果地址不同,则需要逐个对比字节,那么时间复杂度也就退化为了 O(N)。

相加操作

  • 使用+
  • strings Join/Buffer
  • fmt Sprint/Sprintf
  • bytes.Buffer

转换操作

string and byte

a:="hello world"
b:=[]byte(a)

a=string(b)

string and rune

a:="hello world"
b:=[]rune(a)

a=string(b)

string to int

使用strconv的方法