go中string是如何实现的呢

前言

go中的string可谓是用到的最频繁的关键词之一了,如何实现,我们来探究下

实现

// 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我们看起来是一个整体,但是本质上是一片连续的内存空间,我们也可以将它理解成一个由字符组成的数组,相比于切片仅仅少了一个Cap属性。

src/reflect/value.go

type StringHeader struct {
Data uintptr
Len int
}

切片的数据结构

type SliceHeader struct {
Data uintptr
Len int
Cap int
}

1、相比于切片少了一个容量的cap字段,就意味着string是不能发生地址空间扩容;

2、可以把string当成一个只读的切片类型;

3、string本身的切片是只读的,所以不会直接向字符串直接追加元素改变其本身的内存空间,所有在字符串上的写入操作都是通过拷贝实现的。

go语言中的string是不可变的

func main() {
s := "212"
fmt.Println(&s)
fmt.Println(len(s))
s = "33hhhhhhhhhnihihfnHSIHISASIASJAISJAISJAISJAISJAISA"
fmt.Println(&s)
fmt.Println(len(s))
}

上面的例子,s发生了,两次赋值,里面的值发生了改变,好奇怪,明明是不可修改的

go中的字符串底层也是引用类型,类似切片,只是对比切片少了一个cap字段,也就是不能发生扩容。也包含一个指针,指向它引用的字节系列的数组[]byte,所以当改变一个字符串的值,原来指针指向的[]byte将被丢弃,字符串包含的指针,将指向一个新的值转换而来的字节数组。所以说字符串的值是不能被修改的,对于重新赋值,老的值没有被修改,只是被弃用了。

[]byte转string

src/runtime/string.go

type stringStruct struct {
str unsafe.Pointer
len int
} func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
l := len(b)
if l == 0 {
return ""
}
if l == 1 {
stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
stringStructOf(&str).len = 1
return
}
var p unsafe.Pointer
// 判断传入的缓冲区大小,决定是否重新分配内存
if buf != nil && len(b) <= len(buf) {
p = unsafe.Pointer(buf)
} else {
// 重新分配内存
p = mallocgc(uintptr(len(b)), nil, false)
}
// 将输出的str转化成stringStruct结构
// 并且赋值
stringStructOf(&str).str = p
stringStructOf(&str).len = len(b)
// 将[]byte中的内容,复制到内存空间p中
memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
return
} // 转换成
func stringStructOf(sp *string) *stringStruct {
return (*stringStruct)(unsafe.Pointer(sp))
}

总结下流程:

1、根据传入的内存大小,判断是否需要分配重新分配内存;

2、构建stringStruct,分类长度和内存空间;

3、赋值[]byte里面的数据到新构建stringStruct的内存空间中。

string转[]byte

src/runtime/string.go

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
} // 为[]byte重新分配一段内存
func rawbyteslice(size int) (b []byte) {
cap := roundupsize(uintptr(size))
p := mallocgc(cap, nil, false)
if cap != uintptr(size) {
memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
} *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
return
}

1、判断传入的缓存区大小,如果内存够用就使用传入的缓冲区存储 []byte;

2、传入的缓存区的大小不够,调用 runtime.rawbyteslice创建指定大小的[]byte;

3、将string拷贝到切片。

字符串的拼接

+方式进行拼接

func main() {
s := "hai~"
s += "hello world"
fmt.Println(s)
}

一个拼接语句的字符串编译时都会被存放到一个切片中,拼接过程需要遍历两次切片,第一次遍历获取总的字符串长度,据此申请内存,第二次遍历会把字符串逐个拷贝过去。

所以,这种方式拼接只要性能问题是在copy上,适合短小的、常量字符串(明确的,非变量)。

fmt 拼接

func main() {
s := fmt.Sprintf("%s%s%d", "hello", "world", 2021)
fmt.Println(s)
}

fmt可以方便对各种类型的数据进行拼接,转换成string,具体详见printf的用法

Join 拼接

func main() {
var s []string s = append(s, "hello")
s = append(s, "world") fmt.Println(strings.Join(s, ""))
}

buffer 拼接

func main() {
var b bytes.Buffer
b.WriteString("hello")
b.WriteString("world")
fmt.Println(b.String())
}

builder 拼接

func main() {
var b bytes.Buffer
b.WriteString("hello")
b.WriteString("world")
fmt.Println(b.String())
}

测试下几种方法的性能

压力测试

package main

import (
"bytes"
"fmt"
"strings"
"testing"
) func String() string {
var s string
s += "hello" + "\n"
s += "world" + "\n"
s += "今天的天气很不错的"
return s
} func BenchmarkString(b *testing.B) {
for i := 0; i < b.N; i++ {
String()
}
} func StringFmt() string {
return fmt.Sprintf("%s %s %s", "hello", "world", "今天的天气很不错的")
} func BenchmarkStringFmt(b *testing.B) {
for i := 0; i < b.N; i++ {
StringFmt()
}
} func StringJoin() string {
var s []string
s = append(s, "hello ")
s = append(s, "world ")
s = append(s, "今天的天气很不错的 ") return strings.Join(s, "")
} func BenchmarkStringJoin(b *testing.B) {
for i := 0; i < b.N; i++ {
StringJoin()
}
} func StringBuffer() string {
var s bytes.Buffer
s.WriteString("hello ")
s.WriteString("world ")
s.WriteString("今天的天气很不错的 ") return s.String()
} func BenchmarkStringBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
StringBuffer()
}
} func StringBuilder() string {
var s strings.Builder
s.WriteString("hello ")
s.WriteString("world ")
s.WriteString("今天的天气很不错的 ") return s.String()
} func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
StringBuilder()
}
}

看下执行的结果

$ go test string_test.go  -bench=. -benchmem -benchtime=3s
goos: darwin
goarch: amd64
BenchmarkString-4 28862838 115 ns/op 64 B/op 2 allocs/op
BenchmarkStringFmt-4 20697505 169 ns/op 48 B/op 1 allocs/op
BenchmarkStringJoin-4 11304583 293 ns/op 160 B/op 4 allocs/op
BenchmarkStringBuffer-4 31151836 104 ns/op 112 B/op 2 allocs/op
BenchmarkStringBuilder-4 29142151 120 ns/op 72 B/op 3 allocs/op
PASS
ok command-line-arguments 17.740s

ns/op 平均一次执行的时间

B/op 平均一次真申请的内存大小

allocs/op 平均一次,申请的内存次数

从上面我们就能直观的看出差距,不过差距不大,当然具体的性能信息要结合当前go版本,具体讨论。

看上去很low的+拼接方式,在性能上倒是还不错。

go101中评论

标准编译器对使用+运算符的字符串衔接做了特别的优化。 所以,一般说来,在被衔接的字符串的数量是已知的情况下,使用+运算符进行字符串衔接是比较高效的。

我的版本是go version go1.13.15 darwin/amd64

字符类型

我们在go中经常遇到rune和byte两种字符串类型,作为go中字符串的两种类型:

byte

byte 也叫 uint8。代表了 ASCII 码的一个字符。

对于英文,一个ASCII表示一个字符,根据ASCII表。我们知道

A对应的十进制编码是65。我们看看byte打印的结果

func main() {
s := "A"
fmt.Print("打印下[]byte(s),结果十进制:")
fmt.Println([]byte(s)) fmt.Print("打印下[]byte(s)中存储的类型,存储的是十六进制:")
fmt.Printf("%#v\n", []byte(s)) s1 := "世界"
fmt.Print("打印下[]byte(s1),结果十进制:")
fmt.Println([]byte(s1)) fmt.Print("打印下[]byte(s1)中存储的类型,存储的是十六进制:")
fmt.Printf("%#v\n", []byte(s1)) fmt.Print("打印下s1的十六进制:")
fmt.Printf("%x\n", s1)
}

结果

打印下[]byte(s),结果十进制:[65]
打印下[]byte(s)中存储的类型,存储的是十六进制:[]byte{0x41}
打印下[]byte(s1),结果十进制:[228 184 150 231 149 140]
打印下[]byte(s1)中存储的类型,存储的是十六进制:[]byte{0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c}
打印下s1的十六进制:e4b896e7958c

对于ASCII一个索引位表示一个字符(也就是英文)

对于非ASCII,索引更新的步长将超过1个字节,中文的是三个字节表示一个中文。

rune

rune 等价于 int32 类型,UTF8编码的Unicode码点。

func main() {
s := "哈哈"
fmt.Println([]rune(s)) s1 := "A"
fmt.Println([]rune(s1))
}

打印下结果

[21704 21704]
[65]

我们可以看到里面对应的是UTF-8的十进制数字。对于英文来讲UTF-8的码点,就是对应的ASCII。

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8 // rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

关于Unicode和UTF8的区别和联系,以及ASCII码的联系,参考字符编码-字库表,字符集,字符编码

内存泄露的场景

不正确的使用会导致短暂的内存泄露

var s0 string // 一个包级变量

func main() {
s := bigString() // 申请一个大string
// 如果f函数的执行链路很长,申请的大s的内存将得不到释放,直到f函数执行完成被gc
f(s)
} func f(s1 string) {
s0 = s1[:50]
// 目前,s0和s1共享着承载它们的字节序列的同一个内存块。
// 虽然s1到这里已经不再被使用了,但是s0仍然在使用中,
// 所以它们共享的内存块将不会被回收。虽然此内存块中
// 只有50字节被真正使用,而其它字节却无法再被使用。
} func bigString() string {
var buf []byte
for i := 0; i < 10; i++ {
buf = append(buf, make([]byte, 1024*1024)...)
}
return string(buf)
}

上面的例子,一个大的变量s1,另一个字符串s0对这个变量进行了部分引用,新申请的变量s0和老的变量s1共用同一块内存。所以虽然大变量s1不用了,但是也不能马上被gc。

解决方法

思路是发生一次copy,类似go中闭包的解决方法

strings.Builder

func f(s1 string) {
var b strings.Builder
b.Grow(50)
b.WriteString(s1[:50])
s0 = b.String()
}

感觉有点繁琐,当前也可以使用strings.Repeat, 从Go 1.12开始,此函数也是用strings.Builder实现的。

func Repeat(s string, count int) string {
if count == 0 {
return ""
} if count < 0 {
panic("strings: negative Repeat count")
} else if len(s)*count/count != len(s) {
panic("strings: Repeat count causes overflow")
} n := len(s) * count
var b Builder
b.Grow(n)
b.WriteString(s)
for b.Len() < n {
if b.Len() <= n/2 {
b.WriteString(b.String())
} else {
b.WriteString(b.String()[:n-b.Len()])
break
}
}
return b.String()
}

使用demo

func main() {
res := strings.Repeat("你好", 1)
fmt.Println(res)
}

string和[]byte如何取舍

  • string 擅长的场景:

需要字符串比较的场景;

不需要nil字符串的场景;

  • []byte擅长的场景:

修改字符串的场景,尤其是修改粒度为1个字节;

函数返回值,需要用nil表示含义的场景;

需要切片操作的场景;

所以底层实现会发现有很多[]byte

总结

1、字符串不允许修改,string不包含内存空间,只有一个内存的指针,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝;

2、我们阅读go源码,会发现很多地方使用[]byte,使用[]byte能更方便的修改字符串;

3、go中的[]byte存储的是十六进制的ascii码,对于英文一个ascii码表示一个字母,对于中文是三个ascii码表示一个字母;

4、对于[]rune来讲,里面存储的是UTF8编码的Unicode码点,关于UTF8Unicode区别,Unicode是字符集,UTF8是字符集编码,是Unicode规则字库的一种实现形式;

5、字符串的不正确使用会发生内存泄露;

6、对于字符串的拼接,+拼接是一种看上去low,但是对于短小的、常量字符串(明确的,非变量),效率还不错的方法;

参考

【字符串】https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-string/

【字符串】https://chai2010.gitbooks.io/advanced-go-programming-book/content/ch1-basic/ch1-03-array-string-and-slice.html

【Golang中的string实现】https://erenming.com/2019/12/11/string-in-golang/

【字符串】https://gfw.go101.org/article/string.html

【Go string 实现原理剖析(你真的了解string吗)】https://my.oschina.net/renhc/blog/3019849

【Go语言字符串高效拼接(一)】https://cloud.tencent.com/developer/article/1367934

【Go语言字符类型(byte和rune)】http://c.biancheng.net/view/18.html

【go 的 [] rune 和 [] byte 区别】https://learnku.com/articles/23411/the-difference-between-rune-and-byte-of-go

【go语言圣经】http://books.studygolang.com/gopl-zh/ch3/ch3-05.html

【【Go语言踩坑系列(二)】字符串】https://www.mdeditor.tw/pl/pCg8

【为什么说go语言中的string是不可变的?】https://studygolang.com/topics/3727

go中string是如何实现的呢的更多相关文章

  1. BCL中String.Join的实现

    在开发中,有时候会遇到需要把一个List对象中的某个字段用一个分隔符拼成一个字符串的情况.比如在SQL语句的in条件中,我们通常需要把List<int>这样的对象转换为“1,2,3”这样的 ...

  2. C#中string.format用法详解

    C#中string.format用法详解 本文实例总结了C#中string.format用法.分享给大家供大家参考.具体分析如下: String.Format 方法的几种定义: String.Form ...

  3. java中string内存的相关知识点

    (一):区别java内存中堆和栈: 1.栈:数据可以共享,存放基本数据类型和对象的引用,其中对象存放在堆中,对象的引用存放在栈中: 当在一段代码块定义一个变量时,就在栈中 为这个变量分配内存空间,当该 ...

  4. java中String的相等比较

    首先贴出测试用例: package test; import org.junit.Test; /** * Created by Administrator on 2015/9/16. * */ pub ...

  5. java中String、StringBuffer、StringBuilder的区别

    java中String.StringBuffer.StringBuilder是编程中经常使用的字符串类,他们之间的区别也是经常在面试中会问到的问题.现在总结一下,看看他们的不同与相同. 1.可变与不可 ...

  6. Java中String类的方法及说明

    String : 字符串类型 一.      String sc_sub = new String(c,3,2);    //      String sb_copy = new String(sb) ...

  7. JDK6与JDK7中String类subString()方法的区别

    1.subString()方法的作用 subString(int beginIndex, int endIndex)方法的返回的是以beginIndex开始到 endIndex-1结束的某个调用字符串 ...

  8. java中String类型变量的赋值问题

    第一节 String类型的方法参数 运行下面这段代码,其结果是什么? package com.test; public class Example { String str = new String( ...

  9. java中String的常用方法

    java中String的常用方法1.length() 字符串的长度 例:char chars[]={'a','b'.'c'}; String s=new String(chars); int len= ...

  10. Java 中String常用方法

    java中String的常用方法 1.length() 字符串的长度 例:char chars[]={'a','b'.'c'}; String s=new String(chars); int len ...

随机推荐

  1. 基于rest_framework的ModelViewSet类编写登录视图和认证视图

    背景:看了博主一抹浅笑的rest_framework认证模板,发现登录视图函数是基于APIView类封装. 优化:使用ModelViewSet类通过重写create方法编写登录函数. 环境:既然接触到 ...

  2. 详解 SSL(二):SSL 证书对网站的好处

    在如今谷歌.百度等互联网巨头强制性要求网站 HTTPS 化的情况下, 网站部署 SSL 证书已然成为互联网的发展趋势.而在上一篇< 详解 SSL(一):网址栏的小绿锁有什么意义?>中,我们 ...

  3. EXECL函数

    1 COUNTIF 对比两列数据,有相同的即计为1 找一列空白列,输入=COUNTIF(范围,条件),按回车,然后再点击表格右下角的"+" 就可以拉动持续执行这个函数 2 CONC ...

  4. awk 文本编辑器

    1.简介 文本编辑器 非交互式的编辑器 编程语言 功能:对文本数据进行汇总和处理 是一个报告生成器 能够对数据进行排版 工作模式:行工作模式 读入一行 将整行内容存在$0里,一行等于一个记录 记录分隔 ...

  5. Vue中如何使用sass实现换肤(更换主题)功能

    Vue中如何使用sass实现换肤(更换主题)功能 https://blog.csdn.net/m0_37792354/article/details/82012278

  6. Vue插件—vant当中van-list的使用

    https://www.cnblogs.com/xbxxf/p/12889843.html 注意:父级元素不能加overflow:auto: 1 getPendingWorkList() { 2 co ...

  7. P1164-DP【橙】

    这道题让我更深入的理解了记忆化搜索的过程,既然记忆化搜索的结果要靠返回值来传递,那么记忆化搜索解决问题的必须是倒序的,即记忆化搜索是一个简化问题倒序解决的过程,普通搜索是一个复杂化问题逐步尝试并记录尝 ...

  8. Autowired注入Service变成了biaomidou的Mapper代理

    问题概述 一个Springboot工程,使用Mybatis-plus作为数据层框架 使用@MapperScan注解扫描Mapper接口 @MapperScan("org.net5ijy.cl ...

  9. 23- 数码管动态显示02-转换BCD码

    1.BCD码 数码管动态显示的data[19:0]使用二进制数表示的多位十进制数,不能直接生成段选和片选信号,需要使用BCD码表示的十进制数 BCD码(Binary-Coded Decimal),又称 ...

  10. QT启动问题--找不到python36.dll-cnblog

    1.报错:找不到python36.dll 2.解决 通过该查询CSDN下载相应的python36.dll放到C:\Windows\System32目录下即可 https://blog.csdn.net ...