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. PPT 商务报告,如何去表现客户LOGO

    PPT 商务报告,如何去表现客户LOGO LOGO 如何下载 LOGO 如何展示 矩阵排列 删除背景,变成白色 删除背景 设置透明度 AI 软件做成矢量图 LOGO 转色法

  2. Windows 2016 安装 Docker

    打开 PowerShell Windows PowerShell 版权所有 (C) 2016 Microsoft Corporation.保留所有权利. PS C:\Users\Administrat ...

  3. 关于 Jupyter 导出 PDF/Latex 格式报错的简单解决方法

    利用 Jupyter 提供的 Print Preview 功能,然后鼠标右键点击打印,就能导出PDF了,而且不会出问题,中文,图片都可以

  4. Codeforces Round #653 (Div. 3) 题解

    记第一场CF赛,完成3道题 代码更新(降低时间复杂度和修改写法):21.1.23 A题 Required Remainder 题意:给你 x.y.n 求最大的k (k<=n) 使得k%x==y ...

  5. L2-016 愿天下有情人都是失散多年的兄妹 (25分) (简单递归判断)

    L2-016 愿天下有情人都是失散多年的兄妹 (25分) 呵呵.大家都知道五服以内不得通婚,即两个人最近的共同祖先如果在五代以内(即本人.父母.祖父母.曾祖父母.高祖父母)则不可通婚.本题就请你帮助一 ...

  6. SAE 联合乘云至达与谱尼测试携手共同抗疫

    作者 | 营火.计缘.张祖旺 前言 当前疫情形势依然严峻,各行各业众志成城,携手抗疫.新冠病毒核酸检测筛查是疫情防控的重要一环,如何应对疫情的不断反复,以及每日数以万计的核酸检测结果成为每个检测公司的 ...

  7. vue <a>标签 href 是参数的情况下如何使用

    想在页面中使用a标签打开一个新页面进行跳转 例如:msgZi.blogAddress 的值是 https://www.baidu.com 正确的写法: <a :href="goBlog ...

  8. vue结合element-ui实现多层复选框checkbox

    1.需求如上图所以: html相关代码如下: 1 <div class="intent-course-wrapper"> 2 <div class="c ...

  9. gitlab安装,移库,升级

    概述 最近因为机房原因,需要把我们的本地代码库做移库操作. 针对gitlab的安装升级操作重新进行了梳理,记录一下. 环境 CENTOS6 CENTOS7 gitlab-ce-8.14.2 GITLA ...

  10. 无向图求桥 UVA 796

    ***桥的概念:无向连通图中,如果删除某边后,图变 成不连通,则称该边为桥.*** ***一条边(u,v)是桥,当且仅当(u,v)为树枝边,且 满足dfn(u)<low(v)(前提是其没有重边) ...