go底层系列-string底层实现原理与使用

我正在参加「掘金·启航计划」

1、string标准概念

在go中源代码位于src/builtin/builtin.go定义如下:

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
  • string是8bit字节的集合,通常是但并不一定非得是UTF-8编码的文本。
  • string可以为空(长度为0),但不会是nil。
  • string对象不可以修改。

2、string数据结构

源码包在src/runtime/string.go:stringStruct中定义了string的数据结构:

type stringStruct struct {
	str unsafe.Pointer		//字符串首地址,指向底层字节数组的指针
	len int					//字符串长度
}

对于字符串Hello,实际底层结构如下:

3、string操作

3.1、声明

声明一个string变量并赋值:

var str string
str = "Hello"

字符串构建过程是先根据字符串构建stringStruct,再转换成string,源码如下:

func gostringnocopy(str *byte) string {//根据字符串地址构建string
	ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} //先构造stringStruct
	s := *(*string)(unsafe.Pointer(&ss))//再将stringStruct转换成string
	return s
}

string在runtime包中就是stringStruct,对外呈现叫做string。

3.2、[]byte转string

[]byte切片转换成string很简单:

func GetStringBySlice(s []byte) string {
    return string(s)
}

需要注意的是这种转换需要一次内存拷贝。

扫描二维码关注公众号,回复: 14307007 查看本文章

转换过程如下:

  1. 根据切片的长度申请内存空间,假设内存地址为p,切片长度为len(s)
  2. 构建string(sting.str =p; string.len=len)
  3. 拷贝数据(切片中数据拷贝到新申请的内存空间)

3.2、string转[]byte

func GetSliceByString(str string) []byte {
    return []byte(str)
}

string转换成[]byte切片也需要一次内存拷贝。

  1. 申请切片内存空间
  2. 将string拷贝到切片

3.3、字符串拼接

在Go语言中,字符串是不可变得,拼接字符串事实上是创建了一个新的字符串,如果代码中存在大量的字符串拼接,对性能会产生影响。

func concatstrings(buf *tmpBuf, a []string) string {
	idx := 0
	l := 0 //拼接后的字符串总长度
	count := 0
	for i, x := range a {
		n := len(x)
		if n == 0 {
			continue
		}
		if l+n < l {
			throw("string concatenation too long")
		}
		l += n
		count++
		idx = i
	}
	if count == 0 {
		return ""
	}

	// If there is just one string and either it is not on the stack
	// or our result does not escape the calling frame (buf != nil),
	// then we can return that string directly.
	if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
		return a[idx]
	}
	s, b := rawstringtmp(buf, l)//生成指定大小的字符串,返回一个string和切片,二者共享内存
	for _, x := range a {
		copy(b, x)
		b = b[len(x):]//string无法修改,只能通过切片修改
	}
	return s
}

// 生成一个新的string,返回的string和切片共享相同的空间
func rawstring(size int) (s string, b []byte) {
	p := mallocgc(uintptr(size), nil, false)

	stringStructOf(&s).str = p
	stringStructOf(&s).len = size

	*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}

	return
}

3.3.1、常见拼接方式

  • 使用+
s1 + s2 + s3
  • 使用fmt.Sprintf
fmt.Sprintf("%s%s", s1, s2)
  • 使用strings.Builder
func BuilderConcat(n int, str string) string {
	var builder strings.Builder
	for i := 0; i < n; i++ {
		builder.WriteString(str)
	}
	return builder.String()
}
  • 使用bytes.Buffer
func bufferConcat(n int, s string) string {
	buf := new(bytes.Buffer)
	for i := 0; i < n; i++ {
		buf.WriteString(s)
	}
	return buf.String()
}
  • 使用[]byte
func byteConcat(n int, str string) string {
	buf := make([]byte, 0)
	for i := 0; i < n; i++ {
		buf = append(buf, str...)
	}
	return string(buf)
}

3.4、字符串截取

  • 截取普通英文字符串
str := "HelloWorld"
content := str[1 : len(str)-1] 
  • 截取带中文字符串

一个中文字符确定不止一个字节,需要先将其转为[]rune,再截取后,再转为string

strRune := []rune(str)
fmt.Println("string(strRune[:4]) = ",string(strRune[:4]))

4、为什么字符串不允许修改?

在go实现中,string不包含内存空间,只有一个内存的地址,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。

string通常指向字符串字面量,而字符串字面量存储存储位置是只读段,而不是堆或栈上,所以string不可修改。

修改字符串时,可以将字符串转换为 []byte 进行修改。

var str string = "hello"
strBytes := []byte(str)
strBytes[0] = 'H'
str = string(strBytes)
fmt.Println(str)

5、字符串截取函数封装

func SubString(str string, begin, length int) string { 
  rs := []rune(str)
  lth := len(rs) 
  if begin < 0 {
    begin = 0
  }
  if begin >= lth {
    begin = lth
  }
  end := begin + length

  if end > lth {
    end = lth
  } 
  return string(rs[begin:end])
}

猜你喜欢

转载自juejin.im/post/7111953294469267493